1、前言
平时开发我们经常使用 Spring 事务,而 Spring 默认使用 mysql 的事务。mysql 事务默认的隔离级别为:可重复读。我们就以可重复读为例子看一下代码(事务的隔离性是用锁做的,如果不是操作同一行数据就不会锁)。
@Transactional
public void testTransaction(String name) {
// select * from user where 'delete' = 0 order by id asc limit 1;
User user = userMapper.selectUser();
if(user != null){
Boolean oldStatus = user.getStatus();
user.setName(name);
user.setStatus(Boolean.TRUE);
// 乐观锁更新 update user set name = #{user.name} and status = #{user.status} where id = #{user.id} and status = #{oldStatus}
userMapper.updateUser(user, oldStatus);
}
User user1 = userMapper.select(user.getId());
logger.info(user1.getName());
}
2、分析
上面代码的意思是:查询最早的一条状态为0的数据,然后设置状态为1,最后使用乐观锁更新,提交事务。
这种代码在我们平时开发中非常常见,有时候因为可能有多个数据库操作,这个方法会加上 @Transactional 注解来使用事务,保证事务的原子性。我们可能太注意原子性,而忽略了事务的隔离性。在默认情况下,mysql 的隔离性为可重复读(一个事务在最开始读到的数据在事务执行过程中不随着其他事务的操作而改变)。
这段代码在实际运行中会有什么问题?我听到了以下几个不同的意见:
- 1.这段代码会锁表
- 2.在事务1执行 userMapper.updateUser(user, oldStatus) 后事务没提交之前,事务2执行 userMapper.selectUser() 是不能防止查找同一条数据
- 3.在事务1、2查到同一条数据,事务1执行 userMapper.updateUser(user, oldStatus) 没提交之前,事务2执行 userMapper.updateUser(user, oldStatus) 会卡住,但是1执行完毕之后,最后事务2什么都不会更新
上面三个说法哪个正确呢?
- 1.说法1是错误的,一般锁表的情况下很多,除非表就一条数据,否则的话,在可重复读的情况下,mysql 对于程序不是修改同一条数据不会锁住(亲测),修改同一行数据只会锁行,更不可能锁表。
- 2.说法2是正确的,因为事务1没提交之前(虽然已经执行了 update),可重复读的情况下,数据修改对于其他事务是不可见的,事务2仍然能够查询相同的数据
- 3.说法3是正确的,在同一条数据的情况下,修改同一条数据会卡住,修改不同数据不会。因为先修改的数据会拿到行锁,直到提交才会释放。后面拿到行锁的事务,因为我这边有一个乐观锁修改,前面事务已经修改状态,而这个事务会查不到改状态的数据,从而不修改数据。
说了那么多其实就像说明一点,而平时开发中,如果用到了事务,针对数据库状态的问题要多多考虑。我为什么会说这话,因为就在周五我们讨论面单池申请修改的问题,我说:只要我先拿到这条数据,后更新状态为其他,别人就拿不到了。但是被别人当场反驳:你的事务没提交,不能防止别人查不到这条数据。所以让我的方案顿时失效,虽然后面可以重新开一个事务,在另外的事务中做这个事,或者将锁提升到事务外部,用 redis 来控制取面单。但主要我是没有考虑好事务的问题,所以导致我想问题异常简单。。。。。
3、幻读
在这里说一个幻读的定义的矫正。
很多人说幻读是可重复读的情况下,事务1执行事务,先 select 没有,后 insert,但是事务1还未提交;事务2也是先 select 没有,后 select 发现多一条数据。这里以 A(id, name) 操作如下:
事务1 | 事务2 |
---|---|
开启事务 | 开启事务 |
select * from A where id = 1(啥数据都没有) | select * from A where id = 1 (啥数据都没有) |
insert into A values(1, 'ppp') | |
select * from A where id = 1 (突然发现多了一条数据) | |
事务提交 | 事务提交 |
说实话,上面的说法无比扯淡。既然都是可重复读了,在事务2在一个事务中怎么读取跟原来不同的结果呢?都违背了可重复读的定义(否管 innerdb 咋实现的)。
正确的结果如下:
事务1 | 事务2 |
---|---|
开启事务 | 开启事务 |
select * from A where id = 1(啥数据都没有) | select * from A where id = 1 (啥数据都没有) |
insert into A values(1, 'ppp') | |
select * from A where id = 1 (还是啥数据都没有) | |
insert into A values(1, 'ppp') (报主键冲突,插入失败) | |
事务提交 | 事务提交 |
所以,幻读并不是指同一个事务执行两次相同的select语句得到的结果不同, 而是指select时不存在某记录,但准备插入时发现此记录已存在,无法插入,这就产生了幻读。
4、资料
这里有一篇美团的文章,事务讲的特别好:https://tech.meituan.com/2014/08/20/innodb-lock.html