SQL标准中DB隔离级别有:mysql
read uncommitted:能够读到其它transaction 未提交数据 read committed:能够读到其它transaction 已提交数据 repeatable read:一个transaction中相同的查询,每次获取的结果是同样的 serialize:全部操做串行
这几种隔离级别为的是解决并发中的以下问题:sql
脏读 即一个transaction 能够读到另外一个的未提交数据。 不可重复读 即一个transaction 中,两次相同的查询会读到不同的结果。 幻读 一个transaction中,以前不存在的记录,忽然存在了。
read uncommitted 和 serialize 基本没有数据库在用。因此咱们只关注read committed 和 repeatable read, ORACLE 默认是 rc 级别,mysql默认是rr级别。咱们本文的实验都是mysql rr级别下作的。 这两种隔离级别对并发冲突的解决程度以下:数据库
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
RC | 不存在 | 存在 | 存在 |
RR | 不存在 | 不存在 | 存在 |
**要注意的是,sql标准中定义的RR 是存在幻读的,但实际上mysql 的RR级别不存在幻读 **并发
RC级别能够看到其它transaction的提交,因此它是不可重复读的,同时也会存在幻读 RR级别不可看到其它transaction的更改,因此它是可重复读的。但SQL标准定义的RR存在幻读。(虽然mysql的实现中作到了RR无幻读)。为何SQL 标准定义的RR 会有幻读呢? 首先要进一步清晰不可重复读和幻读的区别ui
幻读和不可重复读不太好区分。不可重复读针对update/delete等操做和已有的数据, 而幻读针对的是insert类型操做。一次transaction中,以前读取过的数据被其它transaction 更改提交了,而且在本transaction中可以看到。这是不可重复读。一次transaction中,以前没有的数据,再次操做时却有了,是幻读。虽然幻读也是一种不可重复读,但仍是要把它们区分开讨论。这是由于,这两种问题的解决方案很不同。spa
若是使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务没法修改这些数据,就能够实现可重复读了。但这种方法却没法锁住insert的数据,因此当事务A先前读取了数据,或者修改了所有数据,事务B仍是能够insert数据提交,这时事务A就会发现莫名其妙多了一条以前没有的数据,这就是幻读,不能经过行锁来避免。code
因此幻读和不可重复读最大的区别是如何用锁来解决他们产生的问题。orm
用锁解决不可重复读的问题很是简单。只须要对transaction中读取到的数据加行锁。保证读取过程当中该行不被修改便可。但这样会形成部分数据的操做变成串行。主流的数据库通常不经过锁的方式来实现可重复读,它们采起的方式叫 MVCC (multi-version concurrent control),咱们以mysql 的MVCC 来讲明。索引
mysql 在每一行后面加了几个字段,来标识每一行的状态, 如 row_id 记录行的聚簇索引 key , 建立/更新事务ID, 删除事务ID,回滚ID , 如:事务
ROW_ID, F1, F2, ... ROW_ID, 建立事务ID, 删除事务ID,回滚ID
INSERT 操做会把建立事务ID 置为本身的transaction ID , 删除事务ID 和回滚ID 为空
UPDATE 操做会:
1. 建立该行的回滚记录 2. 更新建立事务ID 为本身的ID 3. 更新回滚ID 指向回滚记录
DELETE 操做会
1. 建立该行回滚记录 2. 更新删除事务ID为本身的transaction ID 3. 更新回滚ID
SELECT 操做会
查找删除事务ID 为空(没删除)或大于本身事务ID 的记录(本身transaction 开始以后删除的) and 建立事务ID 小于等于本身的transaction id (确保不是本身transaction后建立的)
经过这种方式,能够发现mysql 其实实现了可重复读。而且没必要加锁,由于各事务能够维护和操做不一样版本的数据。
也行你认为该方式不只解决了可重复读,还解决了幻读。 确实,假设有transaction A和B ,而且A早于B 开始,那么B中insert的记录A 是读不到的,从这个角度说是解决了幻读。但实际上并不彻底对,mysql中的读其实有两种,当前读和快照读
RR模式MYSQL 中的SELECT 操做只读取某一时间点的数据,即transaction开始时刻的数据,尽管后续有transaction 对数据作出了更改,当前transaction 也看不到。这有点相似于 MYSQL 在transaction开始的时刻打了一个快照。因此这种读叫快照读。它可重复但不是实时的。
MYSQL 中还有另一种读叫当前读(CURRENT READ). 这种读只读取表中当前最新的已提交的数据,能够理解为是一种READ COMMITTED。当前读通常发生在 UPDATE/DELETE/INSERT 以及 SELECT ... FOR UPDATE 和 LOCK ... IN SHARE MODE中。能够经过实验验证一下
transaction A
MariaDB [test]> start transaction; Query OK, 0 rows affected (0.00 sec) MariaDB [test]> select * from t1; +----+--------+ | id | number | +----+--------+ | 1 | 11 | | 2 | 22 | +----+--------+ 2 rows in set (0.00 sec)
transaction B 插入一条数据并commit
MariaDB [test]> select * from t1; +----+--------+ | id | number | +----+--------+ | 1 | 11 | | 2 | 22 | | 3 | 33 | +----+--------+
transaction A 先是 select 证实select 是可重复读,快照读
MariaDB [test]> select * from t1; +----+--------+ | id | number | +----+--------+ | 1 | 11 | | 2 | 22 | +----+--------+ 2 rows in set (0.01 sec)
而后 transaction A 再delete
MariaDB [test]> delete from t1 where id=3; Query OK, 1 row affected (0.00 sec)
能够发现,有趣的是虽然SELECT 不到但delete操做显示成功的删除了该数据。这说明DELETE 能够看到其它TRANSACTION 提交的数据,是RC。 transaction A 提交后再查询,也能够发现 数据确实被删除了
这说明DML操做确实是RC, 为何这些操做是当前读咱们后面再看,但至少如今能够知道,MVCC 的方式虽然能解决快照读的不可重复与幻读问题,但不能解决当前读的。由于当前读是Read committed。那么 Mysql 如何解决当前读的幻读问题呢? 经过间隙锁
用例子说明一下,假设咱们有数据表以下:
其中number上有索引 start transaction;
select * from t4; +--------+ | number | +--------+ | 5 | | 10 | | 15 | +--------+ select * from t4 where number=10 for update; +--------+ | number | +--------+ | 10 | +--------+
上述语句用当前读,读取number=10 , 这种状况下要避免幻读,即接下来:
- insert 操做不会插入到 number=10 t1
- update操做不能更新 number=10 这行
- delete操做不能删除number = 10这行
mysql 所用的方式很简单,经过row锁锁住 number=10的行,阻止update/delete。 经过间隙锁锁住10 可能出现的位置
由于 number 有索引,经过索引咱们知道 number = 10 可能出现的位置有两处 5-10 和 10-15 , 因此mysql 会把这两处锁住, 从其它transaction 去 insert 数据到 5-10 和10 - 15 的位置会被卡住。这就是间隙锁。咱们验证一下
MariaDB [test]> start transaction; Query OK, 0 rows affected (0.00 sec) MariaDB [test]> insert into t4 values(18); Query OK, 1 row affected (0.00 sec) MariaDB [test]> insert into t4 values(6); ^CCtrl-C -- query killed. Continuing normally. ERROR 1317 (70100): Query execution was interrupted MariaDB [test]> insert into t4 values(11); ^CCtrl-C -- query killed. Continuing normally. ERROR 1317 (70100): Query execution was interrupted
除了 18 能够成功,其它两条被卡住
但要注意的是:mysql 经过间隙锁来锁住目标记录可能出现的位置,若是检索条件有索引,能够经过索引锁住目标位置,若是索引是unique 则不用锁间隙,由于不会出现间隙,若是没有索引会锁住全表
间隙锁加行锁的方式来防止当前读的幻读,在mysql中叫next key锁
SELECT FOR UPDATE/SHARE 是当前读比较好理解。这两种读的目的就是锁住记录,不让他人更改,因此锁住快照没有意义
那么DML 为何是当前读呢?考虑如下场景
start transaction; select * from t4; +--------+ | number | +--------+ | 5 | | 10 | | 15 | +--------+ select * from t4 where number=10 for update; +--------+ | number | +--------+ | 10 | +--------+
这时有另外transaction 进行DML 操做,若是insert / update / delete 不是当前读, 那么 SELECT FOR UPDATE的锁仍然毫无心义..