思路
- 所有响应获取锁的线程都先尝试往redis中创建一个缓存数据,所有线程的key必须相同。使用的是redis的setnx命令。就只有一个线程能够创建成功,创建成功的线程就成功获取锁。
- 没有获取锁的线程就循环去获取锁。
- 获取锁的线程在执行完业务后释放锁,也就是删除该key,然后后面的线程重新获取锁时就会有线程能够获取锁成功。
可能出现的问题:
- 假如线程在创建锁以后,出现了异常,走不到释放锁的那一步,那么这个锁就会变成死锁。所以解决方案是给锁(也就是给redis的key加个过期时间,过期了锁还存在的话就自动释放锁),这里注意的是,创建锁跟给锁设置过期时间必须是一个原子操作,否则在创建锁之后,还没来得及设置过期时间就出现异常,一样会造成死锁。
- 有这么一种情况,假如锁设置的超时时间为10秒中,但是线程A执行业务逻辑代码执行了很长时间,超过十秒,那么十秒后,锁自动释放,新的线程B重新获取了该锁。然后原本的线程A业务逻辑执行完了,释放了锁,但是此时线程A释放的却是业务线程B获取的锁,这是不对的。所以要给锁的值添加一个唯一Id(比如一个UUID)。线程释放锁时要判断是不是自己获取的锁(根据唯一Id),是就进行释放(删除Key),否则旧不释放。注意此时的获取唯一Id跟删除必须是原子性操作,redis没有提供相关命令,我们可以使用官网的一段Lua脚本实现。
实现:
springboot整合redis的细节就不说了。直接上代码
//这是从官方文档拿到的一段判断值并删除的LUA脚本,用于实现该操作的原子性。
//地址http://redis.cn/topics/distlock.html
String DISTRIBUTED_LOCK_LUA_SCRIPT = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
public void DistributedLockDemo() {
//获取一个ValueOperations,用于操作redis
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
//获取一个UUID作为此线程锁的标志,防止其他线程释放该锁
String uuid = UUID.randomUUID().toString();
//设置锁,过期时间为1分钟。
//尝试获取锁,setIfAbsent就是与setnx命令,如果键已经存在就会设置失败,返回false,否则返回true。
Boolean succeed = stringStringValueOperations.setIfAbsent(lockKeyName, uuid, 1, TimeUnit.MINUTES);
if(succeed){
//获取锁成功,就执行业务代码
try{
//......业务代码
}finally{
stringRedisTemplate.execute(new DefaultRedisScript<Integer>(DISTRIBUTED_LOCK_LUA_SCRIPT, Integer.class),
Arrays.asList(lockKeyName), uuid);
}
}else{
//获取锁失败
//我这里是循环获取锁,因为redis没有能像Zookeeper那样能够监听一个节点的生命周期,所以这里就循环获取锁,直到成功,可以加一个线程睡眠时间。
//也可以使用redis发布订阅功能来代替轮询。但是这里就不做了,因为这里主要是理解redis实现分布式锁的一个思想,真正生产上通常会使用Redisson框架来实现分布式锁。
while(true){
Boolean succeed = stringStringValueOperations.setIfAbsent(lockKeyName, uuid, 1, TimeUnit.MINUTES);
if(succeed)
//获取锁成功,退出循环。
break;
}
try{
//......业务代码
}finally{
stringRedisTemplate.execute(new DefaultRedisScript<Integer>(DISTRIBUTED_LOCK_LUA_SCRIPT, Integer.class),
Arrays.asList(lockKeyName), uuid);
}
}
}