目录
前言
场景
代码复现
提出疑问
该怎么解决呢
1.使用编程式事务
2.将事务独立出一个方法
前言
很多时候我们谈起事务都是如虎色变,一想起来都是脑袋懵懵的
- 事务的隔离级别及传播机制是什么
- Spring的事务底层实现原理了解吗
- 哪几种情况下事务会失效
锁相关的更是让人如临大敌
- 可重入锁ReetrantLock和synchronized的区别
- 分布式锁的实现
- 轻量级锁volatile关键字的实现
- 说一说synchronized的锁升级流程
当然了,大家都很厉害,上面这些稍微有点难度,仍可一力当之
但是当事务遇上了锁,难上加难,阁下该如何应对呢。
没开玩笑。
在日常开发中,事务必不可少,锁也一样,那事务碰上锁,我们该怎么办呢
场景
我举一个经典的下单场景
1. 扣减库存
2. 生成订单
首先我们会考虑加事务,防止以上哪步数据库操作失败,回滚数据
再次考虑并发问题,加锁,防止超卖
代码复现
我这里有个库存表product,一共10件商品
模拟多线程调用,抢购这些商品,并创建订单。
我们预期商品库存会变为0,并且生成10笔订单数据。
我们根据直觉很容易写出如下代码,在方法上使用事务注解,用于异常回滚,使用锁(此处为了方便使用ReentrantLock,多服务一般都是使用分布式锁)用于并发控制。
抢单逻辑如下:
下面这段代码,方法上加了@Transactional(rollbackFor = Exception.class)
方法的开启和结束也使用Lock加上了锁。
给你几秒,好好想下,输出会是什么?
订单表会生成多少条数据?
“创建订单成功了”这个日志输出会打印多少次?
没错,生成了20笔订单!!!!!
订单表中一共20笔交易数据。
怎么会这样!!!
日志输出也是整整20次
如果是实际商品售卖场景,结果就是我们只有10件库存,由于我们这段代码有问题,多生成了一倍订单,生成了20笔订单。
呐,货发不发出去是小事,工作可是要没了呀,这,这以后,以后还怎么带薪摸鱼呢
我们把@Transactional(rollbackFor = Exception.class)这行代码先拿掉再重新看下执行结果。当然实际情况下肯定需要加事务的,此处只是为了对比排错。
可以看到只生成了10笔订单数据
提出疑问
1.为什么会发生超卖,是锁使用的有问题还是事务使用的有问题
2.为什么是正正好好多卖了一倍
先看第一个问题,为什么会超卖呢?
先问下,上述代码锁释放的代码,在try代码块执行完之后,finally代码块里执行,那么事务什么时候提交的呢,是在 this.orderMapper.insert(order);这条插入语句之后吗?
很明显不是的。
本文示例中事务是在方法执行结束之后提交的,熟悉Spring事务的同学们肯定知道,声明式事务@Transactional注解是基于代理的方式进行的。
打上断点分析下
可以看到在TransactionAspectSupport类中,invokeWithinTransaction()方法先对实际@Transactional注解修饰的方法代理执行后,最后才提交的事务。
也就是说在锁释放之后,事务还没有提交,中间是有程序在执行的那么一小段时间的,在这段时间内,如果新的线程进来,查询到库存还是原值,这个时候就会发生超卖。
虽然这个时间点很小,但是并发量稍微起来点,谁也不能保证什么都不会发生。
正如我们实例中展示,很容易就发生了超卖
我这儿有篇收藏已久的Spring事务秘籍,v我50可点击查看
死磕Spring之AOP篇 - Spring事务详解 - 月圆吖 - 博客园 (cnblogs.com)
第二个问题,为什么一直是正好是创建了20条数据订单呢
其实不会一直是正好20条,只是大概率会20条。
看下图,我们来分下下执行流程。
假设这个时候库存还是10个
1.线程1释放锁的时候,扣减库存之后,事务还没来的及提交
2.这个时候线程2拿到了锁,由于线程1的事务还没有提交,线程2读到的库存数据还是10个,这个时候很大几率就会产生超卖了,注意哦,这里并不是一定会产生。
3.关键点是线程3这个时候是参与不进来的,它很想进来,但法律不允许3p,不对。。是没有锁的权限,它只能等着线程2去释放锁。
所以,超卖的情况下最多只能有两个线程,大概率是库存的两倍 20个。
当然,我们可以尝试复现下创建订单数不是20个的场景。
我们在每个线程加锁之前先让线程睡一会儿,目的是让之前的线程事务提交完毕再去获取锁,再去查询库存,这样就有几率避免超卖,要根据你系统运行环境的性能来调试。
可以看到结果创建订单数就不会是20个了,而是13个
该怎么解决呢
1.使用编程式事务
为了方便,我这里使用 TransactionTemplate。
2.将事务独立出一个方法
注意哦,这里是会有坑的,不要定义成private方法,不要同类中直接调用。
抽出doSell()方法,将事务操作独立出来,直接调用是不会生效为了演示方便,我这里当前类自己注入自己。