2、基于redis实现分布式锁

目录

    • [2.1. 基本实现](#2.1. 基本实现)
    • [==2.2. 防死锁==](#==2.2. 防死锁==)
    • [==2.3. 防误删==](#==2.3. 防误删==)
    • [2.4. redis中的lua脚本](#2.4. redis中的lua脚本)
      • [2.4.1 redis 并不能保证一组命令的原子性](#2.4.1 redis 并不能保证一组命令的原子性)
      • [2.4.2 lua介绍](#2.4.2 lua介绍)
      • [2.4.3. lua基本语法](#2.4.3. lua基本语法)
      • [2.4.4. redis执行lua脚本 - EVAL指令](#2.4.4. redis执行lua脚本 - EVAL指令)
    • [2.5. 使用lua保证删除原子性](#2.5. 使用lua保证删除原子性)

2.1. 基本实现

借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。

  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑,执行完成释放锁(del)
  3. 其他客户端等待重试

改造StockService方法:

java 复制代码
 /**
     * redis setnx实现分布式锁,最基本的哪一种 !!!
     */
    public void deduct() {
        // 加锁setnx
        Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if (!lock) {
            // 没有获取到锁,进行重试!!
            try {
                Thread.sleep(50);
                this.deduct();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 1. 查询库存信息
                String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
                // 2. 判断库存是否充足
                if (stockStr != null && stockStr.length() != 0) {
                    Long stock = Long.parseLong(stockStr);
                    if (stock > 0) {
                        redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
                    }
                }
            } finally {
                // 解锁
                this.redisTemplate.delete("lock");
            }
        }
    }

使用 jmeter 进行压测

查看库存数量

上述代码优化,不断重试的过程中一直进行递归,最终导致栈的溢出。

解决

java 复制代码
    /**
     *  while循环代替递归,解决不断重试可能导致的栈溢出的问题
     */
    public void deduct() {
        // 加锁setnx
        while (this.redisTemplate.opsForValue().setIfAbsent("lock1", "1")) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // 1. 查询库存信息
                String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
                // 2. 判断库存是否充足
                if (stockStr != null && stockStr.length() != 0) {
                    Long stock = Long.parseLong(stockStr);
                    if (stock > 0) {
                        redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
                    }
                }
            } finally {
                // 解锁
                this.redisTemplate.delete("lock1");
            }
        }
    }

2.2. 防死锁

问题:setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)

解决:给锁设置过期时间,自动释放锁。

设置过期时间两种方式:

  • 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  • 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)

2.3. 防误删

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决 : setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

问题:删除操作缺乏原子性。

场景:

  1. index1执行删除时,查询到的lock值确实和uuid相等
  2. index1执行删除前,lock刚好过期时间已到,被redis自动释放
  3. index2获取了lock
  4. index1执行删除,此时会把index2的lock删除

解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本

2.4. redis中的lua脚本

2.4.1 redis 并不能保证一组命令的原子性

redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性

如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户端指令打断。Redis 也保证脚本会以原子性的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI/ EXEC 包围的事务很类似。

2.4.2 lua介绍

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

2.4.3. lua基本语法

lua 复制代码
a = 5               -- 全局变量
local b = 5         -- 局部变量, redis只支持局部变量
a, b = 10, 2*x      -- 等价于       a=10; b=2*x

流程控制:
if( 布尔表达式 1)
then
   --[ 在布尔表达式 1 为 true 时执行该语句块 --]
elseif( 布尔表达式 2)
then
   --[ 在布尔表达式 2 为 true 时执行该语句块 --]
else 
   --[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end

2.4.4. redis执行lua脚本 - EVAL指令

在redis中需要通过eval命令执行lua脚本。

格式:

复制代码
EVAL script numkeys key [key ...] arg [arg ...]
script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。
numkeys:lua脚本中KEYS数组的大小
key [key ...]:KEYS数组中的元素
arg [arg ...]:ARGV数组中的元素

案例1:基本案例

shell 复制代码
EVAL "return 10" 0

输出:(integer) 10

案例2:动态传参

shell 复制代码
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 10 20 30 40 50 60 70 80 90
# 输出:10 20 60 70

EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
# 输出:0

EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 20 10
# 输出:1

传入了两个参数10和20,KEYS的长度是1,所以KEYS中有一个元素10,剩余的一个20就是ARGV数组的元素。

案例3:执行redis类库方法

redis.call()中的redis是redis中提供的lua脚本类库,仅在redis环境中可以使用该类库

shell 复制代码
set aaa 10  -- 设置一个aaa值为10
EVAL "return redis.call('get', 'aaa')" 0
# 通过return把call方法返回给redis客户端,打印:"10"

注意:**脚本里使用的所有键都应该由 KEYS 数组来传递。**但并不是强制性的,代价是这样写出的脚本不能被 Redis 集群所兼容

案例4:给redis类库方法动态传参 ```shell EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 bbb 20 ``` ![在这里插入图片描述](https://img-blog.csdnimg.cn/e796a1bee9f34997a98481f32a019467.png) 学到这里基本可以应付redis分布式锁所需要的脚本知识了。

2.5. 使用lua保证删除原子性

删除LUA脚本:

lua 复制代码
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

代码实现 StockService:

java 复制代码
    /**
     *  解决锁的误删问题
     */
    public void deduct() {
        String uuid = UUID.randomUUID().toString();
        // 加锁setnx
        while (this.redisTemplate.opsForValue().setIfAbsent("lock1", uuid, 20,TimeUnit.SECONDS)) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // 1. 查询库存信息
                String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
                // 2. 判断库存是否充足
                if (stockStr != null && stockStr.length() != 0) {
                    Long stock = Long.parseLong(stockStr);
                    if (stock > 0) {
                        redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
                    }
                }
            } finally {
                // 判断锁是不是被当前线程所持有,是的话,则删除锁
                /*if(uuid.equals(redisTemplate.opsForValue().get("lock1"))) {
                    // 解锁
                    this.redisTemplate.delete("lock1");
                }*/

                // 通过lua脚本来释放锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                        "then " +
                        "   return redis.call('del', KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock"), uuid);
            }
        }
    }
相关推荐
忘忧人生4 天前
Redisson 实现分布式锁
分布式锁·redisson·
morris13110 天前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
Amd79412 天前
FastAPI中Pydantic异步分布式唯一性校验
redis·fastapi·分布式锁·多级缓存·pydantic·唯一性校验·异步校验
小小工匠16 天前
Redisson - 分布式锁和同步器
分布式锁·redisson·同步器
东阳马生架构1 个月前
分布式锁—6.Redisson的同步器组件
分布式锁
东阳马生架构1 个月前
分布式锁—5.Redisson的读写锁一
分布式·分布式锁·redisson
东阳马生架构1 个月前
分布式锁—5.Redisson的读写锁
分布式锁
东阳马生架构1 个月前
分布式锁—4.Redisson的联锁和红锁二
分布式锁·redisson