1.超卖问题
出现原因:
假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁
悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
原理: 悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等
乐观锁:如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,,此时可以重试或异常。
原理:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过 程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas
2.乐观锁解决
解决原理:
CAS操作基于比较当前值与期望值的结果来确定操作是否成功。在多线程情况下,如果多个线程同时使用CAS操作修改库存信息,只有一个线程的CAS操作能够成功,其他线程的CAS操作会失败。当一个线程成功执行CAS操作后,它会修改库存信息,并更新版本号。其他线程在进行CAS操作时会发现版本号不匹配,CAS操作会失败,这样就能防止两个线程同时修改库存信息的情况发生。
因此,使用CAS乐观锁可以通过比较并交换的方式来保证库存信息的修改在并发情况下的安全性。只有一个线程能够修改成功,其他线程会失败并重新尝试,直到成功为止。这样就能有效避免多个线程同时修改库存信息的问题。
业务场景:
只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败.
代码片段
弊端:成功率很低 eg:库存有100 有几百个请求访问 由于成功率低 访问结束 库存依然还有。
3.悲观锁解决(synchronized)
@Transactional
public Result createVoucherOrder(Long voucherId){
Long userId = UserHolder.getUser().getId();
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
注意点
1.锁的对象需要调用intern()
原因:
当我们使用字符串常量或者调用String
类的intern()
方法创建字符串时,会将字符串添加到字符串常量池中。而当我们使用new String()
创建字符串时,会在堆内存中创建一个新的字符串对象。
在多线程环境下,如果多个线程同时访问到相同的字符串,但是创建了不同的字符串对象,这会导致锁失效的问题。也就是说,如果多个线程使用不同的字符串对象作为锁,那么它们实际上是持有不同的锁对象,无法起到同步的作用。
2.如果将业务封装在A方法,并在A方法使用锁,在B方法调用A方法的情况下
在线程1释放锁之后提交事务的瞬间 仍然会出现线程2获得锁并在线程1提交事务之前售卖库存,导致超卖。因此最好在B方法调用A方法 且将A整个锁住。
3.若A与B方法都在同一个类中,且B调用A的情况下需要通过手动获取动态代理对象去提交B的事务。
原因:在seckillVoucher方法中调用了createVoucherOrder方法,但是该方法是在同一个类中被调用的,并不会触发Spring的代理机制,导致事务注解@Transactional无效。为了解决这个问题,使用了AopContext.currentProxy()来获取代理对象,通过代理对象调用createVoucherOrder方法,这样事务注解才会生效。
如果要使用AopContext对象获得代理对象:
1.添加依赖
2.启动类注解