分布式锁

分布式场景下锁失效的问题

单体锁

我们在库存中有五千个商品,然后我们的业务代码模拟一下从redis中获取库存数据,入库有库存则执行库存-1并设置回redis中。
image

    @GetMapping("/test")
    public String test() {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            stock--;
            stringRedisTemplate.opsForValue().set("stock", stock + "");
            System.out.println("stock:" + stock);

        }

        return "";
    }

然后使用jemter模拟并发场景。 在执行的过程就会出现问题,如下图所示,可以发现有很多库存重复消费了。
image
image
解决方案:
在单进程的场景下我们可以使用synchronized语句块加锁只能线程进入一次,避免其他线程重复消费,如下图所示,重复消费问题就解决了。

    @GetMapping("/test")
    public String test() {
        synchronized (this) {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("stock:" + stock);

            }
        }

        return "";
    }

image

分布式锁

但是以上解决方案只能应用于单体项目,如果是分布式场景下,项目部署了多个实例,由于锁无法跨进程共享,依旧会出现重复消费的问题,如下图所示,4905这个库存被两个进程重复消费了一次。
image
image
这时候就需要引入分布式锁来解决这个问题了,在redis中有个命令叫SETX,当SETX设置成功值之后会返回true,失败则会返回false,而redis执行命令又是单线程执行的,它命令执行是原子性的,那我们可以根据这条命令做为锁来使用,当SETX成功表示拿到锁了,当setx失败表示没拿到锁。spring data redis的setIfAbsent就相当于setx命令
如下代码所示:

    @GetMapping("/test")
    public String test() {
        // 获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock", "");

        while (!flag) {
            try {
                // 如果没获取到锁线程睡一秒等待下一次获取锁
                TimeUnit.SECONDS.sleep(1);
                flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock", "");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("stock:" + stock);

            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("stockLock");
        }


        return "";
    }

image
image
但是以上代码依旧会出现问题,就是业务代码处理的过程中,服务器宕机、断电了,导致代码无法执行到finally语句块,导致锁会一直无法释放,要解决这个问题可以给锁加个超时时间,即便是宕机、断电了到超时时间了也会自动释放锁,如下代码所示,给锁设置了20秒的超时时间:

     // 获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock", "", 20, TimeUnit.SECONDS);

        while (!flag) {
            try {
                // 如果没获取到锁线程睡一秒等待下一次获取锁
                TimeUnit.SECONDS.sleep(1);
                flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock", "",  20, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("stock:" + stock);

            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("stockLock");
        }

这个代码依旧有问题,如果出现以下场景,例如并发1进入了方法在执行,但是执行业务比较慢,这时候到期时间到了会自动将分布式锁的key删除了,这时候并发1还未执行完成,由于锁key过期失效的原因这时候并发2又进来了,
然后处理业务代码中,这时候并发1执行完了,由于并发1的锁已经失效自动解除了,那么在并发1的过程中会自动将并发2的锁直接删除,然后这时候并发3有可以拿到锁然后执行业务代码了,这样会实现锁失效的问题。
解决方案:
每次执行前生成唯一的id,此次并发的锁只能由此次并发删除,不允许其他并发删除,如下所示,在加锁时生成一个唯一id,解锁时根据这个唯一id删除锁:

   @GetMapping("/test")
    public String test() {
        // 获取锁
        String lockId = UUID.randomUUID().toString();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock:" + lockId, "", 20, TimeUnit.SECONDS);

        while (!flag) {
            try {
                // 如果没获取到锁线程睡一秒等待下一次获取锁
                TimeUnit.SECONDS.sleep(1);
                flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock:" + lockId, "",  20, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("stock:" + stock);

            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("stockLock:" + lockId);
        }


        return "";
    }

以上代码可以解决当前锁被其他并发解锁的问题但是无法解决锁失效的问题,要想解决锁失效的问题,那么可以执行业务代码的过程中,定时给key续期,避免出现业务代码没执行完成了,但是锁自动失效的问题,如下代码所示


    @GetMapping("/test")
    public String test() {
        // 获取锁
        String lockId = UUID.randomUUID().toString();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock:" + lockId, "", 20, TimeUnit.SECONDS);

        while (!flag) {
            try {
                // 如果没获取到锁线程睡一秒等待下一次获取锁
                TimeUnit.SECONDS.sleep(1);
                flag = stringRedisTemplate.opsForValue().setIfAbsent("stockLock:" + lockId, "", 20, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 新建线程定时给锁续期,每十秒续期一次,直到锁被自动释放
        new Thread(() -> {
            while (stringRedisTemplate.opsForValue().get("stockLock:" + lockId) != null) {
                stringRedisTemplate.expire("stockLock:" + lockId, 20, TimeUnit.SECONDS);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                stringRedisTemplate.opsForValue().set("stock", stock + "");
                System.out.println("stock:" + stock);

            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("stockLock:" + lockId);
        }


        return "";
    }
posted @ 2025-09-02 13:42  RainbowMagic  阅读(4)  评论(0)    收藏  举报