事务:不好意思,你被隔离了!

事务的隔离级别

数据库一般有四种特性:原子性、一致性、隔离性和持久性。而数据库的隔离级别就是针对其中的隔离性而言。

隔离级别也有四种:未提交读、提交读、可重复读、串行化。也不是所有数据库都支持事务的,甚至同一数据库不同存储引擎事务都不是一样的,例如MySQL数据库,里面InnoDB 引擎支持事务,而MyISAM 引擎不支持事务。

而在我们应用层面,例如spring框架基于数据库事务的隔离级别也提供了自己的隔离级别,它有五种,其中四种和数据库一一对应,还有一种是DEFAULT,它表示使用数据库默认的隔离级别。对于不同数据库,默认的隔离级别可能是不一样的,例如MySQL数据库默认隔离级别是可重复读,而Oracle数据库是提交读

下面来具体看看四种隔离级别是什么样的,代码是基于spring事务注解来控制事务的隔离级别。

未提交读(READ_UNCOMMITTED)

此级别属于最低隔离级别,可以读取其他事务未提交的数据,相当于没有隔离,一般不用此种隔离级别。

例如下面这个例子:

@Test
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void select() throws Exception {
    List> maps = jdbcTemplate.queryForList("select * from book where id = 1 ");
    System.out.println(maps);
}

@Test
@Transactional
public void update() throws Exception {
    jdbcTemplate.update("update book set name = '三体2' where id = 1");
    Thread.sleep(30000);
    throw new Exception("异常回滚");
}

首先执行update方法,里面虽然执行了修改操作,但是等待30s后抛出了异常,事务回滚,而在回滚之前执行select方法,发现读取到了 name='三体2'的结果。说明:READ_UNCOMMITTED隔离级别可以读取到其他事务还没提交的数据,造成脏读

读已提交(READ_COMMITTED)

此级别只能读取已经提交的数据,但是读取一行数据时,其他事务可以修改此行数据,再次读取时和上次不一样(不可重复读)。这是Oracle等数据库默认隔离级别。

示例:

@Test
@Transactional(isolation = Isolation.READ_COMMITTED)
public void select() throws Exception {
    List> maps = jdbcTemplate.queryForList("select * from book where id = 1 ");
    System.out.println("第一次:"+maps);
    Thread.sleep(30000);
    maps = jdbcTemplate.queryForList("select * from book where id = 1");
    System.out.println("第二次:"+maps);
}

@Test
@Transactional
public void update() throws Exception {
    jdbcTemplate.update("update book set name = '三体3' where id = 1");
}

首先执行select方法,输出第一次结果,此时name='三体2',然后进入睡眠;接着执行update方法,修改这条数据以后,select方法里第二次输出,此时name='三体3',造成了同一个事务中,两次读取结果不一样。说明:READ_COMMITTED隔离级别虽然只能读取已提交的数据,但是读取时,其他事务还可以修改此数据(不可重复读)

可重复读(REPEATABLE_READ)

此级别是为了解决读提交产生的不可重复读,在一个事务中,各个时段读取的数据都是一致的,不论其他事务在这个时段有没有修改数据,读取结果都是一样的。但是这是针对已有的数据而言,如果其他事务在这个时段插入了符合条件的数据,那这个事务也会读取到插入的这条数据。这就造成了幻读的问题。

示例:

@Test
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void select() throws Exception {
    List> maps = jdbcTemplate.queryForList("select * from book where author = 'zpg'");
    System.out.println(maps);
    Thread.sleep(30000);
    maps = jdbcTemplate.queryForList("select * from book where author = 'zpg'");
    System.out.println(maps);
}

@Test
@Transactional
public void insert() {
    int update = jdbcTemplate.update("insert into `book` values (3,'三体2','zpg')");
    System.out.println(update);
}

测试结果发现select方法中两次查询结果一样,并没有出现幻读,这是为什么呢?因为我这里使用的是MySQL数据库,而MySQL的可重复读隔离级别已经解决了幻读的问题。至于怎么解决的,我们放在下面隔离级别的实现一起讨论。

串行化(SERIALIZABLE)

此级别是隔离效果最好的,解决了脏读、不可重复读和幻读的问题,但是效率也是最差的,它相当于将所有事务执行变成了单线程,因此这种隔离级别也基本用不上。

隔离级别的实现

这里我们只讨论MySQL数据库是怎么实现事务隔离级别的。当然,这里讨论的是InnoDB 引擎下的事务,因为MyISAM 引擎是不支持事务的。

MySQL是通过锁和MVCC来实现事务隔离级别的。先来解释一下MVCC,它的全程是多版本并发控制,它的作用是为了查询一些正在被其他事务修改的行,并且可以看到修改之前的值,它可以不用等待其他事务释放锁,这就大大提高了效率。

那MVCC是怎么实现的呢?这里简单解释一下:数据库会给每行添加三个隐藏字段,其中重要的两个是创建时版本号和删除时版本号。当插入数据时,将当前事务的版本号保存至创建时版本号字段;当更新数据时,新增一行数据,并将当前事务版本号保存至新行的创建时版本号,同时将原数据的删除时版本号设置为当前事务版本号;当删除数据时,将当前事务版本号保存至删除时版本号;当查询数据时,读取创建时版本号小于或等于当前版本号,并且删除版本号为空或大于当前事务版本号的记录。

下面看看各个级别,MySQL数据库是怎么实现的。

首先是读未提交级别,MySQL会在所有的读不加锁,读到的都是最新的值,而所有的写都加行级排他锁,写完就释放锁。

然后是读已提交级别,在此级别下,事务中的每一次读取都会拿到最新快照,这个最新快照是通过MVCC来获取。

接着是可重复读级别,此级别和读已提交最大的区别就是它在事务中只有第一次读取会产生快照,其他读取都是获取的第一次快照,这样就避免了多次读取可能会出现数据不一致的情况。但是可能出现幻读的可能,上面提到可重复读只能通过行锁来控制已存在的数据,如果新插入的数据就无能为力了。在MySQL中,为了解决幻读,引入了另外一个锁,叫间隙锁,就是锁住当前行旁边的数据。通过行锁和间隙锁的控制,这样新插入的数据需要等待事务执行完才能继续执行。

最后就是串行化,这种直接一个表锁就搞定了。


扫一扫,关注我

你可能感兴趣的:(事务)