【Redis】Redis 分布式锁误删问题

时间:2022-12-13 13:05:17

本文主要介绍 Redis 分布式锁误删问题的解决

场景一

1. 问题的产生情况一

因为业务阻塞,导致别人的锁被误删

【Redis】Redis 分布式锁误删问题

2. 解决思路

获取锁的时候存入标识,释放锁的时候判断标识是否一致,一致可以释放锁,不一致不释放锁。

【Redis】Redis 分布式锁误删问题

3. 解决代码

总体思路:

  • 获取锁的时候存入线程标识 , 用 UUID 表示 ()
  • 释放锁的时候,判断标识是否一致
public class SimpleRedisLock implements ILock{

    // 锁的 key 前缀
    private static final String KEY_PREFIX = "lock:";
    // 线程 id 的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
    // 锁的名字
    private String lockName;
    // 传入的 StringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;


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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+lockName, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 防止自动拆箱时候出现空指针问题
    }

    @Override
    public void unlock() {
        // 获取当前锁的线程 id
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + lockName);
        // 获取当前请求释放锁的线程 id
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 判断两者是否一致,一致给释放锁
        if(threadId.equals(id)){
            stringRedisTemplate.delete(KEY_PREFIX + lockName);
        }
    }
}



场景二

1. 问题的产生情况二

获取锁标识并判断一致后被阻塞导致误删问题

【Redis】Redis 分布式锁误删问题

2. 解决思路

保证判断锁标识一致和删除锁这一操作的原子性

  1. Redis 事务:
  • 支持原子性,不支持一致性
  • 批处理操作,最终一致性 (基于乐观锁)
  1. Lua 脚本

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

https://www.runoob.com/lua/lua-tutorial.html

Redis 调用函数:

redis.call('命令名称', 'key', '其他参数')

Redis 执行脚本的命令:

【Redis】Redis 分布式锁误删问题

eval "脚本语句"

eval 支持带参数脚本:

eval "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

等价于 set name Rose

3. 解决代码

SpringBoot 整合 Lua 脚本实现锁的释放

释放锁的 Lua 脚本:

【Redis】Redis 分布式锁误删问题

-- 获取锁中的线程标识
local id = redis.call('get, KEYS[1])
-- Redis中存入的线程标识和传入的参数一致可以删除
if(id == ARGV[1]) then
	return redis.call('del', KEYS[1])
end
return 0

Java 执行 Lua 脚本:

public class SimpleRedisLock implements ILock{

    // 锁的 key 前缀
    private static final String KEY_PREFIX = "lock:";
    // 线程 id 的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
    // 锁的名字
    private String lockName;
    // 传入的 StringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 设置脚本位置
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }


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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+lockName, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 防止自动拆箱时候出现空指针问题
    }


    @Override
    public void unlock() {
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + lockName), ID_PREFIX + Thread.currentThread().getId());
    }
}



总结

Redis 分布式锁实现思路:

  • 利用 setnx 获取锁,设置过期时间,保存 value 为线程标识
  • 释放锁时先判断线程标识是否一致,一致则删除锁

特性:

  • setnx 保证互斥性
  • 通过设置过期时间的方式,保证出现故障时锁仍然能释放,避免了死锁
  • 利用 Redis 集群保证高可用性和并发现