数据库事务隔离级别:
| Isolation\Problem |dirty reads|non-repeatable reads|phantom reads |
|--------------------------------------------------------------------|
| READ_UNCOMMITTED | yes | yes | yes |
| READ_COMMITTED | no | yes | yes |
| REPEATABLE_READ | no | no | yes |
| SERIALIZABLE | no | no | no |
本文将以Spring + Hibernate + postgres 为例,重现各个级别遇到的问题。
主要代码:
Entity:
@Entity
public class City implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String country;
@OneToMany(mappedBy="city")
private Set hotles;
...
Service:
@Component("cityService")
@Transactional(isolation=Isolation.READ_UNCOMMITTED)
class SampleServiceImpl implements SampleService {
@Autowired
private SessionFactory sessionFactory;
@Override
public void request_1() {
Session session = sessionFactory.getCurrentSession();
City city = (City) session.get(City.class, 1L);
System.out.println(city);
session.clear();
city = (City) session.get(City.class, 1L);
System.out.println(city);
}
@Override
public void request_2() {
Session session = sessionFactory.getCurrentSession();
City city = (City) session.get(City.class, 1L);
city.setName("ShangHai");
city.setCountry("China");
session.flush();
System.out.println(city);
}
@Override
public void request_3() {
Session session = sessionFactory.getCurrentSession();
int totalCity = session.createCriteria(City.class).list().size();
System.out.println("First time: " + totalCity);
int totalCitySecondTime = session.createCriteria(City.class).list().size();
System.out.println("Second time: " + totalCitySecondTime);
}
@Override
public void request_4() {
Session session = sessionFactory.getCurrentSession();
City city = new City("BeiJing", "China");
session.save(city);
}
}
READ_UNCOMMITTED :
READ_UNCOMMITTED
是最低的事务级别,会出现"脏读","不可重复读","幻读"。
如上代码所示,使用@Transactional(isolation=Isolation.READ_UNCOMMITTED)
已将事务的隔离级别设置成了READ_UNCOMMITTED
。接下来,我们将在这个事务级别重现“脏读”。
重现步骤:
- 以Debug Mode启动程序,并在
request_1()
中session.clear()
处打上断点。 - 触发
request_1()
,让当前线程停留在session.clear()
处。 - 在
request_2()
中System.out.println(city)
处打上断点,然后触发request_2()
,让其更新City数据,但并不提交事务(停在断点处,不让方法返回) - 让
request_1()
线程从session.clear()
处继续执行。
期望结果:
request_1()
两次打印出的City数据应该不一样。(在此级别下出现了"脏读",request_1()
读取到了request_2()
未提交事务的修改。)
实际结果:
2017-02-26 11:36:23.274 DEBUG 6948 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=Brisbane, country=Australia]
2017-02-26 11:36:28.079 DEBUG 6948 --- [nio-8080-exec-2] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
2017-02-26 11:36:28.108 DEBUG 6948 --- [nio-8080-exec-2] org.hibernate.SQL : update City set country=?, name=? where id=?
Hibernate: update City set country=?, name=? where id=?
2017-02-26 11:36:45.977 DEBUG 6948 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=Brisbane, country=Australia]
[nio-8080-exec-1]
是request_1()
打印的结果,[nio-8080-exec-2]
是request_2()
打印的结果。
request_1()
两次都打印出了City [name=Brisbane, country=Australia]
,与期望不同,并没出现"脏读"。
找到了一篇文章对该现象做出了解释:
虽然官方宣称PostgreSQL支持所有四种ANSI事务隔离级别,但事实上PostgreSQL中只有三种事务隔离级别。每当查询请求“未提交读”时,PostgreSQL就默默地将其升级为“提交读”。因此PostgreSQL不允许脏读。
结论:
使用Postgres时,使用@Transactional(isolation=Isolation.READ_UNCOMMITTED)
将不会生效,事务隔离级别将自动提升为@Transactional(isolation=Isolation.READ_COMMITTED)
,所以"脏读"也是不会出现的.
READ_COMMITTED:
READ_COMMITTED
解决了“脏读“问题,在一个事务中是不会读取到其它事务未提交的修改的。也就是上一个例子中request_1()
两次打印结果都相同,并不会读取到request_2()
未提交的修改。
但READ_COMMITTED
会出现“不可重复读”问题。接下来,我们将在这个事务级别重现“不可重复读”
重现步骤:
- 改为
@Transactional(isolation=Isolation.READ_COMMITTED)
- 以Debug Mode重新启动程序(让spring重新导入数据),并在
request_1()
中session.clear()
处打上断点。 - 触发
request_1()
,让当前线程停留在session.clear()
处。 - 触发
request_2()
,让其更新City数据,并让其返回提交事务。 - 让
request_1()
线程从session.clear()
处继续执行。
期望结果:
request_1()
两次打印出的City数据应该不一样。(在此级别下出现了"不可重复读",request_1()
两次访问数据库拿到的数据不一致)
实际结果:
2017-02-26 12:07:59.964 DEBUG 6912 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=Brisbane, country=Australia]
2017-02-26 12:08:06.207 DEBUG 6912 --- [nio-8080-exec-2] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
2017-02-26 12:08:06.241 DEBUG 6912 --- [nio-8080-exec-2] org.hibernate.SQL : update City set country=?, name=? where id=?
Hibernate: update City set country=?, name=? where id=?
City [name=ShangHai, country=China]
2017-02-26 12:08:16.576 DEBUG 6912 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=ShangHai, country=China]
[nio-8080-exec-1]
是request_1()
打印的结果,[nio-8080-exec-2]
是request_2()
打印的结果。
request_1()
两次打印结果的确不同,表明在request_1()
中出现了“不可重复读”
结论:
@Transactional(isolation=Isolation.READ_COMMITTED)
可能会出现同个事务中两次拿同条数据,而两次拿到的数据不一致问题,这即是“不可重复读”。为了避免“不可重复读”,我们可以将事务隔离级别提高到REPEATABLE_READ
(改为@Transactional(isolation=Isolation.REPEATABLE_READ)
),然后再按照上面的步骤进行试验,得到结果:
2017-02-26 12:14:33.065 DEBUG 5372 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=Brisbane, country=Australia]
2017-02-26 12:14:37.424 DEBUG 5372 --- [nio-8080-exec-2] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
2017-02-26 12:14:37.456 DEBUG 5372 --- [nio-8080-exec-2] org.hibernate.SQL : update City set country=?, name=? where id=?
Hibernate: update City set country=?, name=? where id=?
City [name=ShangHai, country=China]
2017-02-26 12:14:43.559 DEBUG 5372 --- [nio-8080-exec-1] org.hibernate.SQL : select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
Hibernate: select city0_.id as id1_0_0_, city0_.country as country2_0_0_, city0_.name as name3_0_0_ from City city0_ where city0_.id=?
City [name=Brisbane, country=Australia]
结果中可以看到request_1()
两次都打印出了City [name=Brisbane, country=Australia]
,两次读取数据一致了,避免了“不可重复读”。
REPEATABLE_READ:
REPEATABLE_READ
解决了“不可重复读“问题。
但REPEATABLE_READ
会出现“幻读”问题。接下来,我们将在这个事务级别重现““幻读””
重现步骤:
期望结果:
实际结果:
Code:
Sample Code on Github
参考:
How Does Spring @Transactional Really Work?
Spring transaction isolation level tutorial
理解事务的4种隔离级别
Spring @Transactional(isolation=Isolation.REPEATABLE_READ) not work