一、背景
在日常开发中,对不同事务之间的隔离情况等理解如果不够清晰,很容易导致代码的效果和预期不符。因而在这对一些存在疑问的场景进行模拟。
下面的例子全部基于innoDB存储引擎。
二、场景:
2.1、两个事务修改同一行记录
正常来说,两个事务修改相同的记录,肯定会相互阻塞,排队执行的。
一开始号码为13827622366的客户的名称为哈哈哈。A事务先进入事务,但未执行到变更号码为13827622366的客户记录的操作(睡眠实现),B事务开启事务执行变更号码为13827622366的客户记录。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");Thread.sleep(8000);//其他业务LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")@Transactional(rollbackFor = Exception.class)public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}
执行结果
最后该客户的name是“事务1name”。结合下图可以看到,事务1先开启了事务然后睡眠了,接着事务2开启事务,执行查询然后更新记录,接着事务1睡眠完毕,执行查询,查到了事务2提交之后的数据,然后更新记录。也就是说,开启事务之后,在还没有执行到更新操作之前,其他事务还是可以更新该数据并且不会被阻塞。
把睡眠放到update后面,再来验证一下。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);Thread.sleep(8000);//其他业务System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")@Transactional(rollbackFor = Exception.class)public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}
执行结果
最后该客户的name是“事务2name”。结合下图,事务1开始执行查询,并执行更新数据的操作,然后进入睡眠。这个时候事务2开始执行,也查询(因为事务1还没提交,所以查到的也还是原来的值),尝试执行更新数据操作,但这次被阻塞了,一直到事务1提交了事务之后才能继续执行update语句后面的代码。
结论
不同事务更新同一条记录,假如A先执行到更新该行记录的事务,A会阻塞其他想要更新该记录的事务;假如B事务在(A事务执行了更新操作但未提交事务之前)也执行到更新该记录,B事务的代码会被阻塞,必须等A事务提交或回滚了之后,B事务的代码才能继续往下执行。
另外,因为在MySQL中,一个SQL也相当于一个事务,所以一个事务一个非事务修改同一行记录的执行结果和上面也是一样的。
2.2、两个事务修改同一个表的不同行记录
事务1开启事务,修改号码为13827622377的记录的名称,然后睡眠;事务2开启事务,修改号码为13827622366的记录,看看事务2是否还会被阻塞。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);Thread.sleep(8000);//其他业务System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}
执行结果
两个事务都成功提交了,从下图结果来看,事务2并没有因为事务1还未提交而被阻塞,说明开启事务的时候修改不同的行记录不会互相影响。(这样事务执行的效率更高了)
2.3、上面几种场景得出的结论
从上面的几个例子可以看出,事务执行到更新记录操作之后,该行记录暂时不可被该事务之外的操作更改,无论是开启事务来变更记录还是直接变更记录,都会被阻塞。要等待事务1执行完毕提交或回滚事务之后才可以进行记录更新并继续往下执行。(阻塞的位置在更新记录的代码处)
2.4、A事务第一次查询数据,B事务更新数据,A事务再次查询数据
同一条记录,两次查询有什么区别?
innoDB的默认隔离级别是可重复读,这意味着从第一次查询数据开始,这条数据就被记录下来了,只要当前事务没有更改该记录,并且还在当前事务内,无论查询多少次,该条记录的值都是一样的,相当于后续查到的都是记录的一个快照。(这就是事务之间的数据隔离,自己事务更新的数据是可以看到更新之后的值的)
号码为13827622366的记录的name一开始的值是“哈哈哈4”。事务1先开启事务并进行第一次查询,然后睡眠;这时事务2开启事务,并更新该记录的name为“事务2name”;接着事务1睡眠完毕进行第二次查询。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询:"+list.get(0).getName());}Thread.sleep(8000);//其他业务list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询:"+list.get(0).getName());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}
执行结果
在事务1还在睡眠的时候,在系统查询该记录,该记录的name已经更新为“事务2name”。但当事务1第二次查询的时候查询出的结果还是“哈哈哈4”,和第一次查询的结果保持一致,符合可重复读。
解析
innoDB的默认隔离级别是可重复读,要求在一个事务内多次读取同一条记录的结果保持一致。MySQL是通过快照读来实现的,在事务内第一次查询数据的时候,记录所有行记录当前最新的已提交的事务版本号,并形成一个视图。该事务内的后续查询都要和视图内的数据进行比对,只能查询出记录的事务版本号及以前版本的数据,从而实现行记录的快照读。(快照是整个表那一刻的快照,下两个例子验证)
2.5、A事务第一次查询数据,B事务插入数据,A事务再次查询数据
两次查询记录的数量有什么不同?记录的数量上也是实现了可重复读。
号码为13827622366的记录一开始只有一条。事务1开启事务,并第一次查询号码为1382762236的记录个数,然后睡眠;接着事务2开启事务,新插入一条号码为13827622366的记录;接着事务1睡眠结束,进行第二次查询号码为1382762236的记录个数。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询数量:"+list.size());}Thread.sleep(8000);//其他业务list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询数量:"+list.size());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");SdSchoolCustomer customer=new SdSchoolCustomer();customer.setCustomerNo(RandomUtil.randomString(10));customer.setPhone("13827622366");sdSchoolCustomerService.save(customer);System.out.println("事务2结束");return Result.ok();}
执行结果
在事务1还在睡眠的时候,在系统查询号码为1382762236的记录,能查到两条记录,说明事务2所插入的新数据已经生效了。但事务1第二次查到的数量却还是1,说明在事务内,数据在数量上也是存在快照读的。
2.6、A事务查询甲记录,B事务修改乙记录,A事务接着查询乙记录
上述的甲记录和乙记录属于同一个表,看看A事务第一次查询所记录的快照是针对整个表还是仅针对查到的记录。
一开始号码为13827622377的记录的名称为“哈哈哈5”。事务1先开启事务,查询号码为13827622366的记录,接着睡眠;这时候事务2开启事务,更新号码是13827622377的记录的名称为“事务2name”;然后事务1睡眠结束,查询号码为13827622377的记录,看看查到的记录是事务2更新前还是更新后的数据。
代码
@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询:"+list.get(0).getName());}Thread.sleep(8000);//其他业务queryWrapper.clear();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询:"+list.get(0).getName());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2第一次查询:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2第二次查询:"+list.get(0).getName());}System.out.println("事务2结束");return Result.ok();}
执行结果
事务1还在睡眠的时候,在系统查询号码为13827622377的记录,该记录的name已经更新为“事务2name”。事务1第一次查询号码为13827622366的记录的名称并打印只是用来代表查到了该表的数据;接着事务2开启,更新号码为13827622377的记录的名称;事务1睡眠完毕,查询号码为13827622377的记录的名称,发现查询到的结果是事务2修改之前的结果。和从系统直接查询到的结果不一致,说明事务1在第一次查询的时候保存的快照是针对整个表的快照。
三、总结
- 事务之间的互相阻塞是在执行到更新操作代码并且更新到相同表的相同行记录情况下才会触发的。(相当于需要顺序执行)
- MySQL innoDB存储引擎 可重复读隔离级别下,事务在第一次查询表记录的时候记录的是整个表的快照,后续查询无论是数据上,还是数据的量上都是快照读。
- 可重复读隔离级别下,依旧存在幻读问题。可重复读的隔离级别要求事务内多次查询同一个表的数据和数据的量保持一致,这意味着事务内读取到的数据量和实际的数据量可能是不一致的,也就是可能读取到不存在的数据或者读取不到已插入的数据,从而出现幻读问题。
四、实际开发中使用事务的一些见解
- 一些业务如果需要同时用到锁和事务,一般锁加在事务外层。
- 不同事务方法之间的互相影响一般情况下不需要太过考虑。(真需要可以考虑用乐观锁)
五、底层原理
未完待续~