程序员

Spring 事务隔离性

2017-02-26  本文已影响0人  昵称全尼马被注册了

数据库事务隔离级别:

| 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 为例,重现各个级别遇到的问题。


</br>

主要代码:

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<Hotel> 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);
        
    }

}

</br>
</br>

READ_UNCOMMITTED :

READ_UNCOMMITTED 是最低的事务级别,会出现"脏读","不可重复读","幻读"。
如上代码所示,使用@Transactional(isolation=Isolation.READ_UNCOMMITTED)已将事务的隔离级别设置成了READ_UNCOMMITTED。接下来,我们将在这个事务级别重现“脏读”。

重现步骤:
  1. 以Debug Mode启动程序,并在request_1()session.clear()处打上断点。
  2. 触发request_1(),让当前线程停留在session.clear()处。
  3. request_2()System.out.println(city)处打上断点,然后触发request_2(),让其更新City数据,但并不提交事务(停在断点处,不让方法返回)
  4. 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),所以"脏读"也是不会出现的.


</br>
</br>

READ_COMMITTED:

READ_COMMITTED解决了“脏读“问题,在一个事务中是不会读取到其它事务未提交的修改的。也就是上一个例子中request_1()两次打印结果都相同,并不会读取到request_2()未提交的修改。
READ_COMMITTED会出现“不可重复读”问题。接下来,我们将在这个事务级别重现“不可重复读”

重现步骤:
  1. 改为@Transactional(isolation=Isolation.READ_COMMITTED)
  2. 以Debug Mode重新启动程序(让spring重新导入数据),并在request_1()session.clear()处打上断点。
  3. 触发request_1(),让当前线程停留在session.clear()处。
  4. 触发request_2(),让其更新City数据,并让其返回提交事务。
  5. 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],两次读取数据一致了,避免了“不可重复读”。


</br>
</br>

REPEATABLE_READ:

REPEATABLE_READ解决了“不可重复读“问题。
REPEATABLE_READ会出现“幻读”问题。接下来,我们将在这个事务级别重现““幻读””

重现步骤:
期望结果:
实际结果:

</br>
</br>


Code:

Sample Code on Github

</br>
</br>


参考:
How Does Spring @Transactional Really Work?
Spring transaction isolation level tutorial
理解事务的4种隔离级别
Spring @Transactional(isolation=Isolation.REPEATABLE_READ) not work

上一篇 下一篇

猜你喜欢

热点阅读