Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁

Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁

上集回顾

Lua的简单介绍 

redis调用函数  

set name jack 

set name Rose,再执行get name 

redis的 EVAL 命令 

Lua脚本解决unLock业务流程 

代码实现 

unLock.lua 

RedisTemplate调用Lua脚本的API 

Lua 解决unLock问题


Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁

上集回顾

Redis的分布式锁问题(八)基于Redis的分布式锁_面向鸿蒙编程的博客-CSDN博客icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_43715214/article/details/127967364我们在上一个章节中解决了“分布式锁误删问题”,改进后的代码逻辑如下所示:

但是这仍然不是最佳的实现方案,它在极端的情况下还是会发生问题!

public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

如上述代码,如果“判断锁标识”和“释放锁”,之间发生了阻塞呢?JVM触发FULL GC

那么之前一节所讲的“锁误删”就有可能发生!!!

所以,我们的解决思路是要保证这两个操作的原子性

我们可以使用Redis的事务+乐观锁来解决这个问题,但是这样子做非常复杂!这里我们使用 Lua 脚本来实现分布式锁

Lua的简单介绍 

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性 

Lua 教程 | 菜鸟教程 (runoob.com)icon-default.png?t=M85Bhttps://www.runoob.com/lua/lua-tutorial.html

if(0)
then
    print("0 为 true")
end

redis调用函数  

set name jack 

set name Rose,再执行get name 

redis的 EVAL 命令 

如果脚本中的keyvalue不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYSARGV数组获取这些参数:

在Lua中,数组的下标是从1开始的!!!

Lua脚本解决unLock业务流程 

  1. 获取锁中的线程标示
  2. 判断是否与指定的标示(当前线程标示)一致
  3. 如果一致则释放锁(删除)
  4. 如果不一致则什么都不做

代码实现 

unLock.lua 

-- 获取锁标识,是否与当前线程一致?
if(redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 一致,删除
    return redis.call('del', KEYS[1])
end
-- 不一致,直接返回
return 0

RedisTemplate调用Lua脚本的API 

Lua 解决unLock问题

/**
 * redis的分布式锁
 * 实现ILock接口
 */
public class SimpleRedisLock implements ILock {

    // 不同的业务有不同的锁名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    // DefaultRedisScript,
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 初始化 UNLOCK_SCRIPT,用静态代码块的方式,一加载SimpleRedisLock有会加载unlock.lua
    // 避免每次调unLock() 才去加载,提升性能!!!
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // setLocation() 设置脚本位置
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 返回值类型
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 获取锁
     */
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        // set lock thread1 nx ex 10
        // nx : setIfAbsent(如果不存在) , ex : timeoutSec(秒)
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 自动拆箱(Boolean -> boolean)!!!可能有风险
        return Boolean.TRUE.equals(success);
    }

    /**
     * 解决判断(锁标识、释放锁)这两个动作,之间产生阻塞!!!
     * JVM的 FULL GC
     * 要让这两个动作具有原子性
     */
    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}
posted @ 2022-11-22 16:38  金鳞踏雨  阅读(110)  评论(0编辑  收藏  举报  来源