尚品汇-redis实现分布式锁、优化之设置锁的过期时间、优化之UUID防误删、优化之LUA脚本保证删除的原子性(二十一)
目录:
(1)使用redis实现分布式锁
(2)优化之设置锁的过期时间
(3.)优化之UUID防误删
(4)优化之LUA脚本保证删除的原子性
(5) 总结
(1)使用redis实现分布式锁
(解决集群服务设置Reids的问题)




1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试
更改前面的代码

@Override
public void testLock() {
// 1. 从redis中获取锁,setnx
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
// 查询redis中的num值
String value = (String)this.redisTemplate.opsForValue().get("num");
// 没有该值return
if (StringUtils.isBlank(value)){
return ;
}
// 有值就转成成int
int num = Integer.parseInt(value);
// 把redis中的num值+1
this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 2. 释放锁 del
this.redisTemplate.delete("lock");
} else {
// 3. 每隔1秒钟回调一次,再次尝试获取锁
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
重启,服务集群,通过网关压力测试:


基本实现。
问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
解决:设置过期时间,自动释放锁。
(2)优化之设置锁的过期时间
setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
设置过期时间,自动释放锁
设置过期时间有两种方式:
1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
2. 在set时指定过期时间(推荐)


设置过期时间:

压力测试肯定也没有问题。自行测试
问题:可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
- index1业务逻辑没执行完,3秒后锁被自动释放。
- index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
- index3获取到锁,执行业务逻辑
- index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
(3.)优化之UUID防误删


问题:删除操作缺乏原子性。
场景:
- 1 index1执行删除时,查询到的lock值确实和uuid相等
![]()
- 2. index1执行删除前,lock刚好过期时间已到,被redis自动释放
在redis中没有了锁。
![]()
- 3 index2获取了lock,index2线程获取到了cpu的资源,开始执行方法
1.4 index1执行删除,此时会把index2的lock删除
index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行
![]()
删除的index2的锁!
(4)优化之LUA脚本保证删除的原子性
@Override
public void testLock() {
// 设置uuId
String uuid = UUID.randomUUID().toString();
// 缓存的lock 对应的值 ,应该是index2 的uuid
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid,1, TimeUnit.SECONDS);
// 判断flag index=1
if (flag){
// 说明上锁成功! 执行业务逻辑
String value = redisTemplate.opsForValue().get("num");
// 判断
if(StringUtils.isEmpty(value)){
return;
}
// 进行数据转换
int num = Integer.parseInt(value);
// 放入缓存
redisTemplate.opsForValue().set("num",String.valueOf(++num));
// 定义一个lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 准备执行lua 脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 将lua脚本放入DefaultRedisScript 对象中
redisScript.setScriptText(script);
// 设置DefaultRedisScript 这个对象的泛型
redisScript.setResultType(Long.class);
// 执行删除
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
}else {
// 没有获取到锁!
try {
Thread.sleep(1000);
// 睡醒了之后,重试
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Lua 脚本详解:http://doc.redisfans.com/string/set.html

(5) 总结

1、加锁
// 1. 从redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
2.使用lua释放锁
// 2. 释放锁 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
3.重试
Thread.sleep(500);
testLock();
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性
redis集群状态下的问题:
1. 客户端A从master获取到锁
2. 在master将锁同步到slave之前,master宕掉了。
3. slave节点被晋级为master节点
4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。
安全失效!
解决方案:了解即可!

更多推荐
所有评论(0)