之前接手个项目,记录一下踩坑经历,共勉。(急的同学可以直接拿底部代码)
先看一下前辈的原始代码:方便理解,我这里简化了下逻辑:
public String redisLock() {
String lockKey = "ke";
String clintId = UUID.randomUUID().toString();
try {
// 加锁,返回true, 加锁成功, false, 加锁失败, redis原生setNx的操作
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.SECONDS);
if (!flag) {
return "锁占用中";
}
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
// 若程序执行时间超过lock的过期时间,那么就会造成锁自己删除,业务代码就不再是原子执行了
// try {
// Thread.sleep(11000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 减库存
if (stock > 0) {
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", stock.toString());
log.info("库存扣减成功,剩余库存:{}", stock);
} else {
log.info("库存不足!!!");
}
} catch (Exception e) {
log.info("处理异常", e);
} finally {
// 释放锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
代码很简单,却隐藏了两个问题:
1、锁是固定,也就是往redis存的值是死的,这会存在持有线程的锁被其他线程误删,导致新线程重新获取锁成功。
2、在网络极端条件下,如果业务代码执行的时间超过了10秒,也就是锁超时时间,那么锁会被redis清掉,导致新线程重新获取锁成功(这边可以强制让当前线程睡一段时间,模拟极端条件的情况)。
下面是我改动之后的代码:
// 过期时间
public static final Integer EXPIRETIME = 10;
// 守护线程,类似与看门狗,续命过期的redis的键
@Autowired
private DaemonThread daemonThread;
private static Thread thread;
@RequestMapping("lock")
public String redisLock() {
String lockKey = "ke";
String clintId = UUID.randomUUID().toString();
try {
Boolean flag =
stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clintId, EXPIRETIME, TimeUnit.SECONDS);
// 开启守护线程去判断锁是否被持有,持有就增加过期时间,一般判断周期可以设置为过期时间的一半
if (thread != null) {
boolean isAlive = thread.isAlive();
if (!isAlive) {
thread = new Thread(daemonThread);
thread.setDaemon(true);
thread.start();
}
} else {
thread = new Thread(daemonThread);
thread.setDaemon(true);
thread.start();
}
if (!flag) {
return "锁占用中";
}
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
// 若程序执行时间超过lock的过期时间,那么就会造成锁自己删除,业务代码就不再是原子执行了
// try {
// Thread.sleep(11000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (stock > 0) {
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", stock.toString());
log.info("库存扣减成功,剩余库存:{}", stock);
} else {
log.info("库存不足!!!");
}
} catch (Exception e) {
log.info("处理异常", e);
} finally {
// 避免锁被持有锁的线程误删
if (clintId.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
解决的思路是这样的:
1、设置每个线程获取的锁都是唯一的(至于唯一算法,大家可以自行发挥,这里只做演示),就是这边的clientId。删除锁的时候判断删除的锁,就是之前设置的锁。
2、开启一个守护线程去判断锁的超时时间,如果锁存活并且超时时间不多了,就重新设置锁的超时时间,保证锁不被超时过期了。
总结,这段逻辑其实有现成的开源实现,Redisson,编码简单暴力,其实原理就是第二部分,有兴趣的同学可以翻看一下源码:
@Autowired
private RedissonClient redissonClient;
@RequestMapping("lock")
public String redisLock() {
String lockKey = "ke";
String clintId = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock(lockKey);
try {
lock.tryLock(10, TimeUnit.SECONDS);
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", stock.toString());
log.info("库存扣减成功,剩余库存:{}", stock);
} else {
log.info("库存不足!!!");
}
} catch (Exception e) {
log.info("处理异常", e);
} finally {
lock.unlock();
}
return "end";
}