实现秒杀下单
我们点击限时抢购
,然后查看发送的请求
请求网址: http://localhost:8080/api/voucher-order/seckill/13
请求方法: POST
看样子是VoucherOrderController
里的方法
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return Result.fail("功能未完成");
}
}
那我们现在来分析一下怎么抢优惠券
首先提交优惠券id,然后查询优惠券信息
之后判断秒杀时间是否开始
开始了,则判断是否有剩余库存
有库存,那么删减一个库存
然后创建订单
无库存,则返回一个错误信息
没开始,则返回一个错误信息
这种情况如果代码有问题,当遇到高并发场景时,会出现超卖现象
假设现在只剩下一个库存,线程1过来查询库存,判断库存数大于1,但还没来得及去扣减库存,此时库线程2也过来查询库存,发现库存数也大于1,那么这两个线程都会进行扣减库存操作,最终相当于是多个线程都进行了扣减库存,那么此时就会出现超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:而对于加锁,我们通常有两种解决方案
悲观锁/乐观锁
悲观锁
悲观锁认为:线程安全问题一定会发生,所以在操作数据之前先获取锁,保证线程串行执行。(synchronize和Lock)
乐观锁
乐观锁认为:线程安全问题不一定会执行,因此不加锁,他会让每个线程都进来操作数据,只是在更新数据的时候再去判断有没有其他线程对数据进行了修改
如果已经被其他线程修改,则说明发生线程安全问题,当前操作需要失败或重试。
悲观锁:悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等
乐观锁:每次操作数据时,会读取当前的版本号。
在提交数据时,检查当前数据库中的版本号是否与最初读取的版本号一致。 如果一致,说明数据未被其他事务修改,可以安全更新,并将版本号加1(或其他增量)。 如果不一致,说明数据已被其他事务修改,当前操作需要失败或重试。
update seckill_voucher set stock = stock -1 , version = version +1 where voucher_id = ? and stock > 0 and version = ?
当然乐观锁还有一些变种的处理方式比如CAS
CAS 的核心思想: 读取:获取当前数据的值(如库存或版本号)。 比较:在更新时,检查当前数据的值是否与最初读取的值一致。 交换:如果一致,则更新数据;否则,操作失败或重试。
CAS 适用于高并发场景,但需要注意 ABA 问题和自旋开销。
UPDATE seckill_voucher
SET stock = stock - 1
WHERE voucher_id = #{voucherId}
AND stock = #{expectedStock};
但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败
那么我们继续完善代码,修改我们的逻辑,在这种场景,我们可以只判断是否有剩余优惠券,即只要数据库中的库存大于0,都能顺利完成扣减库存操作