前言:
业务要求一个简单的并发控制,使得一条数据只被确认一次,我的方案是 悲观锁,就是在事务中先对数据行加锁(MySQL InnoDB 行锁基于索引),判断是否已经确认过,未确认的情况下确认,已确认则事务提交释放锁。代码写完,结果发现未生效,就开始了满脑子问号的排查过程。
业务代码结构如下:
//不要这么做
//一没对异常进行处理
//二事务的范围太大包含很多不需要在事务中的代码
@Transactional
public void dangerConfirm(ConfirmDangerRequest request) throws CommonException {
String zhiXinBianHao = request.getZhiXinBianHao();
//此处方法中包含一次查询操作 查询表 A数据
CodeWxid codeWxid = hisDaoService.getCode(request.getConfirmUserId());
if (codeWxid == null || codeWxid.getWxId() == null){
throw new CommonException("工号或企业微信id有问题", ResultStatusCode.INVALID_CAPTCHA);
}
//在此行打断点先阻塞
//悲观锁 锁表B中的一行数据 mybatis
String lock = someMapper.lock("AED5ADC3C67E4A89AB7161DA84DC1FC1");
System.out.println(lock);
//用JPA查同一条数据 偷懒 表B
BaseInfo baseInfo = baseInfoRepository.findByZhiXinBianHao("AED5ADC3C67E4A89AB7161DA84DC1FC1");
//用mybatis查询同一行的某个字段 表B
String te = someMapper.te("AED5ADC3C67E4A89AB7161DA84DC1FC1");
情况描述:
在MySQL命令行直接开事务,锁同一行,此时上文代码断点往下执行会等待锁的释放,正常。在命令行事务中更新数据中的某个字段,后提交。此时,调试代码获取到锁,向下执行时 发现问题:最后两行,均未查出命令行已经提交的字段的值,即 无法读到其它事务已经提交的数据。这和我所掌握的知识不符。
分析:
数据库MySQL的隔离级别时 RR,不会出现脏读和不可重复读。问题是现在其它事务提交的都读不到,但是数据库软件是可以查到的。没办法了,排除法,把所有与事务无关的注释掉,一执行,好了。。。。。。可以正常查到其它事务已提交的数据。
那么,自然地就定位到 下面这行的问题
//此处方法中包含一次查询操作 查询表 A数据
CodeWxid codeWxid = hisDaoService.getCode(request.getConfirmUserId());
加上上面这行,又不行了。。。。。除了悲观锁那行外,后面又读不到其它事务提交的数据了。。
发现了这个现象,下面就开始做实验(隔离级别为 REPEATABLE-READ):
起两个MySQL命令行客户端A、B,两边都 set autocommit = 0; start transaction;
实验一:A更新一条数据行 id = 1,值更新为 99,此时A不提交,B是查不到新值99的。A提交,B直接查询此条数据,可以查到值99。注意,在此之前B从未执行过查询操作。
实验一:A更新一条数据行 id = 1,值更新为 99,此时A不提交,B是查不到新值99的。A提交,B先任意执行一条查询,再查询此条id=1的数据,就不可以查到值99,查到的是之前的旧值。
这好像很符合 REPEATABLE-READ隔离级别的定义
使用 set @@session.tx_isolation='read-committed'; 将AB会话的隔离级别调整为 read-committed。发现:B事务任何情况下都可以读到A事务刚提交的最新数据。
总结:
MySQL默认的隔离级别为 REPEATABLE-READ,这个隔离级别使得 前后读取同一条的值是相同的,不会受其它事务的影响,除非它自己改变的。