分布式锁全家桶
Redis
1. 基本实现
借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发 送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑,执行完成释放锁(del)
3. 其他客户端等待重试
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private LockMapper lockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
public void checkAndLock() {
// 加锁,获取锁失败重试
while (!this.redisTemplate.opsForValue().setIfAbsent("lock","xxx")){
try {
// 避免栈溢出
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
this.redisTemplate.delete("lock");
}
}
2. 防死锁
解决:给锁设置过期时间,自动释放锁。
设置过期时间两种方式:
1. 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
2. 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)
问题:可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
1. index1业务逻辑没执行完,3秒后锁被自动释放。index2请求就会执行
2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。index3请求就会执行
3. index3获取到锁,执行业务逻辑
4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只 执行1s就被别人释放。 最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的 锁
3. 防误删
实现如下:
问题:删除操作缺乏原子性。
场景:
1. index1执行删除时,查询到的lock值确实和uuid相等
2. index1执行删除前,lock刚好过期时间已到,被redis自动释放
3. index2获取了lock
4. index1执行删除,此时会把index2的lock删除
解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)
4. redis中的lua脚本 保证原子性
1. 现实问题
redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。 例如:
在串行场景下:A和B的值肯定都是3
在并发场景下:A和B的值可能在0-6之间。
极限情况下1:
则A的结果是0,
B的结果是3
极限情况下2:
则A和B的结果都是6
如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户 端指令打断。Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有 其他脚本或 Redis 命令被执行。 这和使用 MULTI/ EXEC 包围的事务很类似。
但是MULTI/ EXEC方法来使用事务功能,将一组命令打包执行,无法进行业务逻辑的操作。这期间有某 一条命令执行报错(例如给字符串自增),其他的命令还是会执行,并不会回滚。