1. 今天做了什么
- 实现秒杀券下单功能
- 分析高并发下的超卖问题
- 使用悲观锁(synchronized)解决超卖问题
- 学习了编程式事务 TransactionTemplate 的用法
2. 核心知识点
秒杀券表设计
- 普通券和秒杀券都存在同一张表,用一个字段做区分
- 秒杀券还有一张独立表(tb_seckill_voucher)存秒杀券特有的信息:库存、开始时间、结束时间
秒杀下单流程
- 查询秒杀券信息
- 判断时间是否在秒杀时间内(开始时间、结束时间校验)
- 判断库存是否足够
- 扣减库存
- 创建订单
- 返回订单id
超卖问题
- 现象:卖出超过库存数量的商品
- 原因:线程1判断库存后、尚未完成扣减之前,线程2也进行了判断;高并发下很多线程几乎同时判断库存,都认为充足,导致超卖
悲观锁
- 认为线程安全问题一定会发生,加锁让线程串行执行
synchronized、Lock以及 Redis 实现的都是悲观锁- 保证了线程安全,但串行执行影响性能
正确实现:锁 + 编程式事务
1 |
|
- 关键点:锁的释放一定要晚于事务提交
- 更新操作添加
.gt("stock", 0)也可防止超卖(CAS 乐观锁兜底)
错误示范1:锁释放早于事务提交
1 |
|
- 错误原因:锁的释放早于事务的提交,释放锁之后才会提交事务,所以在提交事务之前,其他线程查询到的都是未扣减的旧库存
错误示范2:同类调用导致 @Transactional 失效
1 |
|
- 错误原因:
@Transactional是基于动态代理实现的,其他类注入的实际是代理对象,所以可以进行事务管理;而在同一个类中,seckillVoucher中调用doSeckillVoucher实际上是this.doSeckillVoucher(voucherId),this 不是代理对象,导致@Transactional失效
事务的两种写法
- 声明式事务:
@Transactional - 编程式事务:
1 | TransactionStatus status = transactionManager.getTransaction(...); // 开启 |
- Spring 提供了
TransactionTemplate来封装以上代码:
1 | Result result = transactionTemplate.execute(status -> { |
- 编程式事务,只要有
spring-boot-starter-jdbc或mybatis-plus-boot-starter,就已经包含了spring-tx和事务相关类,不需要额外加依赖,只要配置好数据库的数据源即可
3. 深度思考
4. 踩过的坑
- 锁的释放时机必须晚于事务提交,否则释放锁后其他线程能读到未扣减的旧库存,仍会超卖
- 同一个类中方法调用
@Transactional会失效,因为 this 不是代理对象