假设有这样一个场景,在一个购票软件上买一张票,但是此时剩余票数只有一张或几张,这个时候有几十个人都在同时使用这个软件购票。在不考虑任何影响下,正常的逻辑是首先判断当前是否还有剩余的票,如果有,那么就进行购买并扣减库存数,否则就会提示票数不足,购买失败。伪代码如下:void buyTicket() {
int stockNum = byTicketMapper.selectStockNum();
if(stockNum>0){
//TODO 买票流程…
byTicketMapper.reduceStock(); // 扣减库存
}else{
log.info(“=>票卖完了<”);
}
}
复制代码这段代码在逻辑上没有问题,但是在并发场景下,可能会存在一个严重的问题。当剩余票数为1时,有A,B两个用户同时点击了购买按钮,A用户通过了库存大于0的校验并开始执行购票逻辑,但是由于一些原因造成A用户的购票线程有短暂的阻塞。而在这个阻塞的过程中,用户B发起了购买请求,并且也通过了库存大于0的校验,直到整个购买流程执行完成并且扣减了库存。那么这个时候剩余库存刚好为0,不会再有用户发起购买请求,这时用户A的购买请求阻塞被唤醒,因为在此之前已经校验过库存大于0,所以执行完购买流程后,库存还会被扣减一次。那么此时的库存为-1,这就是常听到的超卖问题。
图片
为了避免这个问题,我们可以通过加锁了方式,来保证并发的安全性。像JVM提供的内置锁synchronized,JUC提供的重入锁ReentrantLock,但是这两种锁只能保证单机环境下并发安全问题,一般在实际工作中很少会部署单节点的项目,通常都是多节点集群部署,这两个锁就失去了意义。这个时候就可以借助redis来实现分布式锁。setnx在集群部署的情况下,通常使用redis来实现分布式锁。其中redis提供了setnx命令,标识只有key不存在时才能设值成功,从而达到加锁的效果。下面通过redis来改造上述的代码,其方式是购票线程首先获取锁,如果获取锁成功,那么继续执行购票业务流程,直到所有流程执行完成并扣减库存后,最终在释放锁。如果获取锁失败,那么就给出一个友好的系统提示。void buyTicket() {
// 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(“lock”, “1”);
if (lock) {
int stockNum = byTicketMapper.selectStockNum();
if(stockNum>0){
//TODO 买票流程…
byTicketMapper.reduceStock(); // 扣减库存
}else{
log.info(“=>票卖完了<”);
}
// 释放锁
redisTemplate.delete(“lock”);
} else {
log.info(“=>系统繁忙,请稍后!<”);
}
}
复制代码问题1:死锁问题通过上面的一顿梭哈,你以为这样就可以了吗,其实不然。设想一下,如果线程A在获取锁成功后,在执行购票的逻辑中出现了异常,那么这个时候就会造成锁得不到释放,其他线程始终获取不到锁,这就造成严重的死锁问题。为了避免死锁问题的出现,我们可以对异常进行捕获,在finally中去释放锁,这样不管业务执行成功或失败,最后都会去释放锁。void buyTicket() {
Boolean lock = redisTemplate.opsForValue