超卖问题一直是面试的常见问题之一
这里刚刚好系统性的学习了
如何通过加锁的方式来防止超卖问题的发生
总而言之就是在业务逻辑方法内部用相对宽松的乐观锁
而方法调用时用严格的悲观锁
来实现乐观与悲观的相互结合协调
下面先给出一段代码示例:
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//拿到当前对象的代理对象,即实现的接口的代理对象
return proxy.createVoucherOrder(voucherId);}
}
@Transactional//此处业务逻辑牵扯到了两张表,所以最好加上事务注解
public Result createVoucherOrder(Long voucherId) {
//一人一单,防止超卖
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("一人仅限一单哦~");
}}
/**
* gt加了一个乐观锁,设置的条件比较宽松只要stock大于0
* 如果设置stock=stock,也就是确认stock没有被其他线程修改
* 会导致过于严格而导致大多数都会失败
*/
//扣减库存
boolean success = seckillVoucherService.update()// 1. 开始构建更新操作 相当于 update seckill_voucher
.setSql("stock=stock-1")// 2. 设置要更新的SQL片段相当于set stock=stock-1
.eq("voucher_id", voucherId).gt("stock", 0) // 3. 设置更新条件 where voucher_id=#{voucherId},stock大于0
.update(); // 4. 执行更新 .update() 的返回值是true
if (!success) {
return Result.fail("库存不足");
}
想必你也会有诸多疑问,比如:
- 悲观锁为什么是加在这?
- 乐观锁为什么是这样设置的?
- proxy代理对象是为什么?
- intern()是什么东西?
下面逐一说说吧:
悲观锁:
首先悲观锁加在这肯定不是乱加的,有说法的。
悲观锁认为所有线程都是互相不可信的,都是不安全的
如果想当然的直接在createVoucherOrder加上 synchronized关键字
那的确是在整个业务方法上确确实实的上了悲观锁
但是会导致不同ID的UserID都需要等待前一位锁的释放(而我们的目的是阻止相同ID的用户多买)
进而导致整个方法都是串行执行的,是十分严重的性能问题
所以这边应该把悲观锁锁在UserID中
这样才能阻止相同ID的用户进入逻辑执行
//识别userId,若ID相同就卡在这里,等待这个用户的上个请求被执行完毕。
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//而进入方法中会检测你的购买数是不是大于1,然后叫你滚蛋
return proxy.createVoucherOrder(voucherId);}
乐观锁:
乐观锁认为所有线程都是比较安全的,不会被其他线程入侵
即使发现数据被修改也只需要更新数据就可以了
boolean success = seckillVoucherService.update()// 1. 开始构建更新操作 相当于 update seckill_voucher
.setSql("stock=stock-1")// 2. 设置要更新的SQL片段相当于set stock=stock-1
.eq("voucher_id", voucherId).gt("stock", 0) // 3. 设置更新条件 where voucher_id=#{voucherId},stock大于0
.update(); // 4. 执行更新 .update() 的返回值是true
if (!success) {
return Result.fail("库存不足");
}
在上面的代码实现中,这里的乐观锁其实就是加了个gt(“stock”,0)。
就是判断库存是否大于0,为True的话才能购买,这是一种更为宽松的锁形式
也能判断库存值是否等于道歉库存值,也就是stock=getStock();
这种是更为严格的锁形式,高并发状态下会导致阻止过多购入
所以无论哪种,单一的乐观锁无法解决超买超卖的问题,都需要配合悲观锁才行
proxy代理对象是为什么?
首先因为业务方法中设置了事务注解,只有动态代理对象才能应用于事务注解
正常来说代码应该是这样的无动态代理
return this.createVoucherOrder(voucherId);
但是这里的this是一个实例化对象,实例化对象无法应用于事务
这也是Spring事务失效的几种原因之一
当然事务注解是不能删去的,不然会导致大问题
所以这里我们必须引入动态代理对象
也就是这一句:
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
AopContext.currentProxy();将会拿到当前对象的代理对象
也就是当前类所实现的接口的代理对象
Idea出于甩锅考虑,所以会默认给一个最为保守的Object类
但是我们知道所实现的类是IVoucherOrderService,所以我们就强转过来。
intern()是什么东西?
简单来说就是Java自带的toString()方法
其实是new了一个新的对象出来
即使名称相同,但是内存地址是截然不同的,所以其实还是不同的对象
但是悲观锁是一个高冷的总裁大人,他管你这的那的
他不关心你的名字是什么,也不关心你的是不是那个对象
反正不是同一个人,就直接放行
所以为了防止高傲的总裁大人任性,就有了intern()方法的出现
他会严格按照字符串来去转换,而不是创建一个新的对象
总裁大人依旧不管这那,看见了名称是一样的UserID,就卡死
所以在这里intern()是必要的。
还有一些唧唧歪歪的东西:
值得留意的是return上面有一个:Long userId = UserHolder.getUser().getId();
而后续的方法体中也有一个:Long userId = UserHolder.getUser().getId();
就会让人留意:后面重新拿了一个新的user,不会拿错吗?
实际上是不会的,这就是LocalThread的特性
一次业务,代码从上到下执行一次,都是单独的线程池,都是单独的线程
拿也是拿的刚刚进去的
就这样吧,后面有新的知识再补充吧