首先我们来了解一下事务的四大特性(ACID):
1、原子性(Atomicity):
原子是不可分割的执行单元,要么一起执行,要么不执行
2、一致性(Consistency):
事务执行前后,数据的完整约束性不被破坏,典型的例子:银行转账
3、隔离性(Isolation):
事务之间完全独立,互不影响,但是在事务四大特性中,隔离性并不是严格意义上的隔离,数据库定义了四种隔离级别,隔离级别越高,数据越准确,相反的性能越低。
4、持久性(Durability):
事务执行完成之后,会持久保存,即使数据库崩溃,也不会丢失事务信息。
接下来我们看一下,在web开发环境中,如何重现不同隔离级别下出现并发的问题,以及如何解决,我们使用Thread.sleep的形式模拟事务的并发运行,并发问题包括:更新丢失、脏读、不可重复读、幻读。
一、更新丢失
事务A、B同时更新一行数据,事务B把事务A的更新给覆盖掉。数据库在没有加任何锁的情况下会发生,事实上,一般数据库(例如mysql)事务都会默认加锁,所以这种情况不会发生,了解一下即可。
问题重现:
当两个事务提交之后,事务A就相当于没有执行过一样,数据被事务B覆盖了,这种情况叫做更新丢失。
二、脏读
事务A的更新没commit之前,事务B读取到了事务A要提交的数据,此时事务A可能会回滚,导致事务B拿到错误的数据进行操作。
问题重现:
事务A:
@Transactional
public int updateUserPassWorld(Long id, String password) {
User user = userMapper.findById(id);
System.out.println("update before:"+user);
int update = userMapper.updateUserPassWorld(id,password);
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("commit update password="+password);
return update;
}
事务B:
//注意这里定义了隔离级别:READ_UNCOMMITTED
@Transactional(isolation=Isolation.READ_UNCOMMITTED)
public User fingyId(Long id) {
User user2 = userMapper.findById(id);
System.out.println("find:"+user2);
return user2;
}
在事务A还没有commit之前调用事务B,我们会发现事务B就已经读取到事务A还没有commit的修改数据(password=456),控制台打印如下:
update before:User(id=6, username=王五, password=123)
find:User(id=6, username=王五, password=456)
commit update password=456
脏读的解决方案:将事务隔离级别上升至READ_COMMITTED
三、不可重复读
事务A对同一条数据读取两次,在两次读取过程中,事务B修改了该数据,导致事务A查询同一个数据出现两次不一样的结果。
问题重现:
事务A:
//这里将隔离级别设置为READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public User fingyId(Long id) {
User user1 = userMapper.findById(id);
System.out.println("find user1:"+user1);
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
User user2 = userMapper.findById(id);
System.out.println("find user2:"+user2);
return user2;
}
事务B
@Transactional
public int updateUserPassWorld(Long id, String password) {
int update = userMapper.updateUserPassWorld(id,password);
System.out.println("update id ="+id+",password="+password);
return update;
}
在事务A执行第一次查询后但还没有commit之前,调用事务B将password进行修改并提交,事务A第二次查询的时候发现数据不一致,控制台打印如下:
find user1:User(id=6, username=王五, password=123)
update:1;id =6,password=456
find user2:User(id=6, username=王五, password=456)
这里需要注意的是,mybatis会默认开启一级缓存,你的第二次查询user2.password可能显示的是123,所以,为了重现不可重复读现象,请将mybatis一级缓存关闭:flushCache="true"
这里我们可以看到,不可重复读和脏读有点相似,它们的区别在于脏读是未提交读,不可重复读是提交之后读取。
不可重复读解决方法:将隔离等级上升至REPEATABLE_READ及其以上即可,mysql默认隔离等级就是REPEATABLE_READ
四、幻象读:
事务A对同一条数据读取两次,在两次查询过程中,事务B插入、删除了数据,导致事务A查询到的两次结果不一致。不可重复读和幻象读也有点相似,只不过前者值的是修改,后者是新增或者删除,就感觉突然出现/消失一样。
事务A:
//这里设置隔离级别为REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<User> listAll() {
List<User> users1 = userMapper.listAll();
System.out.println("users1:"+users1);
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
List<User> users2 = userMapper.listAll();
: System.out.println("users2:"+users1);
return users2;
}
事务B:
@Transactional
public int deleteUser(Long id) {
int delete = userMapper.deleteById(id);
System.out.println("delete user id:"+id);
return delete;
}
在事务A没有commit之前调用事务B,控制台打印如下:
users1:[User(id=1, username=张三, password=123), User(id=9, username=王五, password=123)]
delete user id:9
users2:[User(id=1, username=张三, password=123), User(id=9, username=王五, password=123)]
嗯?为什么和和预期的不一样?我们预期的结果users2的结果不应该包含user id为9的数据,其实这里是因为mysql的InnoDB的可重复读隔离级别REPEATABLE_READ和其他数据库的可重复读是有区别的,不会造成幻象读。
我们把REPEATABLE_READ改为READ_COMMITTED就可以了可以重现幻象读了:
users:[User(id=1, username=张三, password=123), User(id=9, username=王五, password=123)]
delete user id:9
users2:[User(id=1, username=张三, password=123)]
幻读的解决方案:将数据库隔离级别设置为SERIALIZABLE(mysql InnoDB只需要设置到REPEATABLE_READ),但是设置到SERIALIZABLE是不可取的,会极大影响并发性能,效率低。
到这里我们已经将并发问题都通过代码重现了,上面所说的READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE就是数据库的四种隔离级别。
1、未提交读(READ_UNCOMMITTED):
写事务不允许另一个事务对该数据进行修改,但允许读。 因此该级别下,不会出现更新丢失,但会出现脏读、不可重复读。
2、提交读(READ_COMMITTED):
写事务不允许其他事务访问该行,但是读事务允许其他事务的访问该行数据,因此不会出现脏读,但是会出现不可重复读的情况。
3、可重复读(REPEATABLE_READ):
读事务禁止写事务,写事务拒绝其他一切事务,因此不会出现不可重复读,但是有可能出现幻象读。
4、序列化(SERIALIZABLE):
所有事务串行执行,能解决一切并发问题,一般不使用。
在开发过程中,我们不应该为了保证数据完整性和一致性的而去随意提高数据库的隔离级别,推荐隔离级别在提交读(READ_COMMITTED)就可以了,剩下的可以依靠程序来控制,例如采用悲观锁、乐观锁、行级锁等。
登录 | 立即注册