设计思路
假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20。
获取锁
判断lock是否存在 EXISTS lock
- 不存在,则自己获取锁,记录重入层数为1.
- 存在,说明有人获取锁了,继续判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
- 不存在,说明锁已经有了,且不是自己获取的,锁获取失败.
- 存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20
释放锁
判断当前线程id作为hashKey是否存在: HEXISTS lock threadId
- 不存在,说明锁已失效
- 存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
- 获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock
因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的 key-value 结构, 这里推荐使用 hash 结构。而且要让所有指令都在同一个线程中操作,那么使用 lua 脚本。
lua 脚本
lock.lua
local key = KEYS[1]; -- 第1个参数,锁的key local threadId = ARGV[1]; -- 第2个参数,线程唯一标识 local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间 if(redis.call('exists', key) == 0) then -- 判断锁是否已存在 redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁 redis.call('expire', key, releaseTime); -- 设置有效期 return 1; -- 返回结果 end; if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己 redis.call('hincrby', key, threadId, '1'); -- 如果是自己,则重入次数+1 redis.call('expire', key, releaseTime); -- 设置有效期 return 1; -- 返回结果 end; return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
unlock.lua
-- 锁的 key local key = KEYS[1]; -- 线程唯一标识 local threadId = ARGV[1]; -- 判断当前锁是否还是被自己持有 if (redis.call('hexists', key, threadId) == 0) then -- 如果已经不是自己,则直接返回 return nil; end; -- 是自己的锁,则重入次数减一 local count = redis.call('hincrby', key, threadId, -1); -- 判断重入次数是否已为0 if (count == 0) then -- 等于 0,说明可以释放锁,直接删除 redis.call('del', key); return nil; end;
在项目中集成
编写 RedisLock 类
@Getter @Setter public class RedisLock { private RedisTemplate redisTemplate; private DefaultRedisScript<Long> lockScript; private DefaultRedisScript<Object> unlockScript; public RedisLock(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; // 加载释放锁的脚本 this.lockScript = new DefaultRedisScript<>(); this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua"))); this.lockScript.setResultType(Long.class); // 加载释放锁的脚本 this.unlockScript = new DefaultRedisScript<>(); this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua"))); } /** * 获取锁 * @param lockName 锁名称 * @param releaseTime 超时时间(单位:秒) * @return key 解锁标识 */ public String tryLock(String lockName, long releaseTime) { // 存入的线程信息的前缀,防止与其它JVM中线程信息冲突 String key = UUID.randomUUID().toString(); // 执行脚本 Long result = (Long)redisTemplate.execute( lockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId(), releaseTime); // 判断结果 if(result != null && result.intValue() == 1) { return key; }else { return null; } } /** * 释放锁 * @param lockName 锁名称 * @param key 解锁标识 */ public void unlock(String lockName, String key) { // 执行脚本 redisTemplate.execute( unlockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId(), null); } }
