数据库隔离级别
本文主要目的是阐明数据库的四种隔离级别以及在mysql下实现隔离的原理。
隔离级别
数据库事务隔离级别从低到高分别是:read uncommitted、read committed、retpeatable read、serializable,依次解决了数据库脏读、不可重复读、幻读问题。
- 脏读:一个事务读到了另一个事务未提交的数据
- 不可重复读:在一个事务中多次读取数据过程中发生了另一个事务对数据进行了更新,导致前后两次查询数据结果不同,主要体现在数据库数据前后数据内容的不一致。
- 幻读:一个事务在执行过程中读取到了另一个事务已提交的插入数据;即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样。主要体现在数据库数据数目不一致的情况(serializable解决了这个问题,需要锁住满足条件的所有记录以及相近的记录)
隔离原理
Mysql的默认隔离级别是可重复读,是读已提交。数据库的隔离级别其实是依赖锁实现的。
读未提交
读未提交其实就是数据库操作不会加锁,其实就是没有隔离。B事务能看到A事务已经修改但没有提交的数据,这时候如果A事务回滚,B事务看到的其实就是脏数据,一般情况下我们不会选用这种级别。
读已提交
这个是pg、oracle等数据库的默认隔离级别。一个事务只能读到另一个事务已经提交的数据。在mysql中可重复读和读已提交都是通过MVCC
进行实现的,区别在于可重读是事务启动的时候就生成read view整个事务结束都一直使用这个read view,而在读已提交中则是每执行一条语句就重新生成最新的read view。
可重复读
mysql通过MVCC解决了不可重复读的问题,本质上是一种快照读。在事务开启的时候生成快照,后续读到的都是当时的快照数据,即使当前数据已经其他事务修改并提交。具体规则如下:
- 当前事务内的更新可以读到
- 其实事务未提交的不能读到
- 其他事务在快照创建后提交的不能读到
- 其他事务在快照创建前提交的可以读到(这种其实另一个事务已经结束了)
如下,可以看到虽然B事务对数据进行了修改并提交,但是事务A第二次读到的只还是10。
我们在postgre
下进行相同的测试就可以发现postgre
默认隔离级别是读已提交。
可以通过命令行设置当前事务隔离级别,即可以得到和mysql相同的预期。
1 | postgres=# show default_transaction_isolation; |
通过行级锁可以解决并发写,修改的时候需要对数据行加锁,且在事务提交时才会释放。
这里需要注意一下,如果更新的条件没有用到索引的话,mysql会对所有行加行锁,然后再判断不满足的行进行释放,这个过程其实也比较影响性能。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。
幻读
mysql有两种读数据方式,一种是快照读(普通select),另一种是当前读(select … for update,update,delete)。mysql可以完全解决快照读下的幻读问题,但是并不能完全解决当前读下的幻读问题。解决方式是使用了行锁+间隙锁,这个锁叫做 Next-Key锁。
快照读
mysql通过mvcc解决了快照读下的幻读问题。当启动事务后执行查询会创建一个read view,后续的查询语句会利用这个read view在undo log版本中找到事务,即便其他事务后续插入或删除了新的数据,原事务也只会读原来的快照,避免了幻读问题。
以下以select ... for update
举例说明,update和delete同理。
当前读
mysql数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。如下图被分成了(负无穷,10)、(10,30)、(30,正无穷)三个区间,这三个区间是可以加间隙锁的。注意,mysql的间隙锁依赖于索引,否则会为整个表加上间隙锁,即所有的数据插入、删除都无法执行。
如下操作,事务A对(10,30)区间增加了间隙锁,对数据行10和30增加的行锁,来保证不会出现幻读。
使用等值条件会将值两端区间都增加间隙锁,如
price = 10
或price <= 10
都会将(负无穷,10)和(10,30)两个区间加锁。
注意mysql并没有真正意义上解决幻读的问题,如下场景可以看出同一事务的两次查询看到的是不同的现象,因此解决幻读的最好方式是及早使用当前读的方式对数据加间隙锁。
串行化
要彻底解决幻读问题,只能采用串行化,简单粗暴地将所有sql命令串行化执行,显然这样会极大地影响数据库性能。