mysql 隔离级别与间隙锁等

数据库隔离级

SQL标准中DB隔离级别有:

read uncommitted:可以读到其它transaction 未提交数据
read committed:可以读到其它transaction 已提交数据
repeatable read:一个transaction中相同的查询,每次获取的结果是一样的
serialize:所有操作串行

这几种隔离级别为的是解决并发中的如下问题:

脏读
即一个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 会有幻读呢? 首先要进一步清晰不可重复读和幻读的区别

幻读和不可重复读

幻读和不可重复读不太好区分。不可重复读针对update/delete等操作和已有的数据, 而幻读针对的是insert类型操作。一次transaction中,之前读取过的数据被其它transaction 更改提交了,并且在本transaction中能够看到。这是不可重复读。一次transaction中,之前没有的数据,再次操作时却有了,是幻读。虽然幻读也是一种不可重复读,但还是要把它们区分开讨论。这是因为,这两种问题的解决方案很不一样。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。

所以幻读和不可重复读最大的区别是如何用锁来解决他们产生的问题。

MVCC

用锁解决不可重复读的问题非常简单。只需要对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 如何解决当前读的幻读问题呢? 通过间隙锁

间隙锁 和 next-key 锁

用例子说明一下,假设我们有数据表如下:

其中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锁

为什么 DML/SELECT FOR UPDATE, SELECT FOR SHARE 是当前读

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的锁仍然毫无意义..

posted on 2019-06-03 18:20  kramer  阅读(3611)  评论(0编辑  收藏  举报

导航