目录
- 2.1. 基本实现
- ==2.2. 防死锁==
- ==2.3. 防误删==
- 2.4. redis中的lua脚本
- 2.4.1 redis 并不能保证
- 2.4.2 lua介绍
- 2.5. 使用lua保证删除原子性
2.1. 基本实现
借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑,执行完成释放锁(del)
- 其他客户端等待重试
改造StockService方法:
/**
* redis setnx实现分布式锁,最基本的哪一种 !!!
*/
public void deduct() {
// 加锁setnx
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (!lock) {
// 没有获取到锁,进行重试!!
try {
Thread.sleep(50);
this.deduct();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
// 1. 查询库存信息
String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
// 2. 判断库存是否充足
if (stockStr != null && stockStr.length() != 0) {
Long stock = Long.parseLong(stockStr);
if (stock > 0) {
redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
}
}
} finally {
// 解锁
this.redisTemplate.delete("lock");
}
}
}
使用 jmeter 进行压测
查看库存数量
上述代码优化,不断重试的过程中一直进行递归,最终导致栈的溢出。
解决
/**
* while循环代替递归,解决不断重试可能导致的栈溢出的问题
*/
public void deduct() {
// 加锁setnx
while (this.redisTemplate.opsForValue().setIfAbsent("lock1", "1")) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 1. 查询库存信息
String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
// 2. 判断库存是否充足
if (stockStr != null && stockStr.length() != 0) {
Long stock = Long.parseLong(stockStr);
if (stock > 0) {
redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
}
}
} finally {
// 解锁
this.redisTemplate.delete("lock1");
}
}
}
2.2. 防死锁
问题:setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)
解决:给锁设置过期时间,自动释放锁。
设置过期时间两种方式:
- 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
- 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)
2.3. 防误删
持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明
解决: setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
问题:删除操作缺乏原子性。
场景:
- index1执行删除时,查询到的lock值确实和uuid相等
- index1执行删除前,lock刚好过期时间已到,被redis自动释放
- index2获取了lock
- index1执行删除,此时会把index2的lock删除
解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)
2.4. redis中的lua脚本
2.4.1 redis 并不能保证
redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。
如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户端指令打断。Redis 也保证脚本会以原子性的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI/ EXEC 包围的事务很类似。