public String deductStockOriginal() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
问题说明:此时查询和扣减库存不是原子性操作,并发场景会出现超卖现象。此时需要增加分布式锁,减少超卖现象发生。
1、第一种情况redis分布式锁未设置超时时间
public String deductStockIfNoTime() {
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockValue"); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
问题描述:如果没有设置超时时间,则出现宕机问题时,会出现死锁,这个锁将一直不能释放,影响其他请求获取锁。
2、将设置缓存和超时时间分开未做到原子性
问题描述:如果将设置锁和设置超时时间分开,则存在原子性问题,如果设置锁后,过期时间还没设置好,此时宕机,则锁没有超时时间将一直存在
3、设置了超时时间,但是没有续期逻辑,会存在逻辑没执行完锁就过期的情况
/* *
* @description: 使用stringRedisTemplate的IfAbsent实现分布式锁
* 此时如果1号线程10s未执行完,锁超时过期,2号线程可以获取到锁,从而执行扣减库存操作
*
* @author: quwuju
* @date: 2023/12/22 14:27
* @param 【null】
* @return: null
*/
@RequestMapping("/deduct_stock1")
public String deductStockIfAbsent() {
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockValue", 10, TimeUnit.SECONDS); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
问题说明:此时存在一个问题是如果当前1号线程执行时间较长,过了redis缓存的时间了,那新的2号线程进来就能加锁成功,此时1号线程业务逻辑结束之后,删除锁,删除的是2号线程的锁,很有可能因为某种原因2号线程的业务逻辑还没结束。那3号线程再来加锁也可以加锁成功,线程2结束时删除的是线程3的锁。。。。。。
设置一个clientId作为标识,每个线程只能删除自己的锁
/* *
* @description: 使用stringRedisTemplate实现分布式锁 setnx
* 升级,设置一个clientId作为标识,每个线程只能删除自己的锁
*
* @author: quwuju
* @date: 2023/12/22 14:27
* @param 【null】
* @return: null
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
// 产生一个uuid来区分
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
问题描述:升级,设置一个clientId作为标识,每个线程只能删除自己的锁。但是超时时间不易确定是第一个问题,第二个问题是假设判断cliedntId的时候刚判断完,卡顿了1s,锁过期了,新的线程又加锁成功,此时执行到删除的逻辑,会将新线程的锁删除掉,此时又不是删除了自己的锁,所以此时就引入redisson来做看门狗锁续期。
1、线程加锁续命逻辑分析
public String deductStockRedisson(String a, int b) {
String lockKey = "lock:product_101";
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock(); // .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//解锁
redissonLock.unlock();
}
return "end";
}
使用lua脚本保证原子性,使用脚本加锁设置超时时间,设置完成之后会回调续期代码逻辑
加锁完成后执行续期,递归续期
2、线程抢夺锁自旋逻辑分析
getLatch方法里面是获取一个信号量,如果未获取到锁,会等待上一个线程还剩余的超时时间,到时间后继续循环获取锁 信号量
在redisson的解锁逻辑中会通过redis的发布订阅功能。在抢锁过程中会订阅一个channel,解锁的时候会发布消息给订阅的队列,通知等待的线程继续抢锁。
红锁的实现方式是整多个节点(非主从节点,各自独立),每次加锁必须半数以上节点返回成功才算加锁成功。一般也会给每个节点配置从节点。
@RequestMapping("/redlock")
public String redlock() {
String lockKey = "product_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
return "end";
}
如果宕机的slave节点重启之后也不存在当前这个key,那2号节点和3号节点都可以加锁成功,此时就是半数以上节点成功,所以红锁失效。
红锁存在的问题可参考文章:红锁存在的问题
红锁相关分析及问题,暂不做赘述:引用别人的文章 红锁相关分析及问题
1.锁的粒度一定要小
2.可参考concurrentHashMap分段锁实现,比如有1000个商品要加锁,可以分10个锁,每100个商品一个锁。