数据库并发如何让数据操作串行化
本章主要讲解 MySQL 是如何在并发请求保证数据的正确性。
引入了一些概念,指引学习方向,文末有我们学习参考文章
概念 (针对于 InnoDB)#
学习一个新鲜事物的时,我们总会碰到一些新的概念或设计,如果你细心领悟或许可以举一反三,通过之前学过的东西与之关联。但是如果你并无感悟,倒也无妨。因为大多数你只需要整理后自己能记住就可以。毕竟我们不是开创者而是使用者。接下来我将要介绍几个概念或是设计是针对于今天所探讨的主题。
MVCC (多版本并发控制)#
对于未接触过这个概念的人肯定是一脸雾水,那我只简介一下它的作用。如果想学习可以自行查找资料。
它会在每个数据行后面追加隐藏两个字段,分别代表创建(创建的事务版本号),删除(删除的事务版本号)。
事务版本号:每启动一个事务都会产生版本号
id | name | create_version | delete_version |
---|---|---|---|
1 | dog | 1 | 2 |
1 | pig | 2 | null |
2 | cat | 2 | 3 |
3 | cats | 3 | null |
上表为 MVCC 的一个简图,我来描述一下它的经历。
- 事务 1 :插入一条 id=1,name=dog 的数据。
- 事务 2:更新了 id=1,name=pig。 并且插入一条数据 id=2,name=cat
- 事务 3:插入了一条 id=3,name=cats,并且删除了 id=2 的数据
有了这个设计我们就能弄出不同版本的查询结果(隔离级别中的快照读就与之相识)
- 获取最新版本(版本 3)的数据,查询条件为:create_version>=3 and delete_version=null。
- 获取指定版本(版本 2)的数据,查询条件为:create_version<=2 and (delete_version=null or delete_version>2)。
锁 (防止数据并发操作)#
锁一个特别简单的名词,但是初学者未必能理的清楚概念。像我这样文字理解能不好的人就卡了半天。
最后我的理解是:阻碍你获取数据的就是锁。锁是你与数据交流间的一道屏障。
行锁#
共享锁(S 锁)又称为读锁(Share lock, 简记为 S 锁),若事务 T 对数据对象 A 加上 S 锁,则其它事务只能再对 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。#
排它锁(X 锁)又称为写锁((eXclusive lock, 简记为 X 锁)),若事务 T 对数据对象 A 加上 X 锁,则只允许 T 读取和修改 A,其它任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。#
这里我进行了一个比喻,假设数据是一个透明的箱子。如果箱子里东西能看到就是可查询,能触碰到就是可修改。
共享锁:添加条件(能看到箱子里东西的事务都能添加,否者需要等待),相当于给箱子加了一个锁,这个锁只有自己能打开。
排它锁:添加条件(能触碰到箱子里东西的事务都能添加,否者需要等待),相当于打开箱子给里面东西套上一个盒子,再对盒子加锁,这个锁只有自己能打开
- 事务 1 为数据添加共享锁,事务 2 能再添加共享锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
- 事务 1 为数据添加共享锁,事务 2 能添加排它锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
- 事务 1 为数据添加排它锁,事务 2 能添加共享锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
- 事务 1 为数据添加排它锁,事务 2 能添加排它锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
间隙锁 (gap 锁)#
会在查询数据的两侧插入间隙锁,导致无法插入。 这与 InnoDB 索引顺序排序有关。#
四种事务隔离#
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。
锁和多版本数据(MVCC)是 InnoDB 实现一致性读和隔离级别的手段。
因此,在不同的隔离级别下,InnoDB 处理 SQL 时采用的一致性读策略和需要的锁是不同的。
Read Uncommitted (RU)#
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed (RC)#
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。
Repeatable Read (RR)#
这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的 “幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)#
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
隔离级别与 MVCC 的关系#
事务中 sql 语句有加锁与不加锁两种,我们把不加锁的定义为快照读,加锁的定义为当前读,以下是分类
- 快照读:就是 select
- select * from table ….;
- 当前读:特殊的读操作,插入 / 更新 / 删除操作,属于当前读,处理的都是当前的数据,需要加锁。
- select * from table where ? lock in share mode; S 锁,Gap 锁
- select * from table where ? for update; X 锁,Gap 锁
- insert; X 锁,Gap 锁
- update ; X 锁,Gap 锁
- delete; X 锁,Gap 锁
MVCC 定义了隔离级别不同的快照读版本,只对 RC 与 RR 进行分析。
- RC 隔离级别:每次快照读都是获取最新版本。
- RR 隔离级别:每次快照读都是获取指定版本。
隔离级别与锁的关系#
不同的隔离级别当前读加锁情况不同,只对 RC 与 RR 进行分析。
- 共同点:在添加 X 锁与 S 锁方面是一样的。
- 不同点:RR 隔离级别有时还会添加 Gap 锁。
加锁分析#
数据库加锁解锁时间点#
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
事务 | 加锁 / 解锁处理 |
---|---|
begin | |
insert into test ….. | 加 insert 对应的锁 |
update test set… | 加 update 对应的锁 |
delete from test …. | 加 delete 对应的锁 |
delete from test …. | 事务提交时,同时释放 insert、update、delete 对应的锁 |
但在实际使用过程当中,MySQL 做了一些改进,在 MySQL Server 过滤条件,发现不满足后,会调用 unlock_row 方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是 MySQL,为了效率也是会违反规范的。(参见《高性能 MySQL》中文第三版 p181)
数据库如何加锁#
数据库加锁是比较复杂,有比较多的情况。需要你了解 InnoDB 的索引机制。这里简单通过 sql 执行计划(explain)分析,步骤如下:
- type=range
- key=PRIMARY:主键的范围扫描,会在主键 B + 树添加行锁。RR 隔离级别下主键 B + 树会添加间隙锁。
- key = 唯一索引:唯一索引的范围扫描,会在主键 B + 树添加行锁,唯一索引 B + 树上添加行锁。RR 隔离级别下唯一索引 B + 树会添加间隙锁。
- key = 普通索引:普通索引的范围扫描,会在主键 B + 树添加行锁,普通索引 B + 树上添加行锁。RR 隔离级别下普通索引 B + 树会添加间隙锁。
- type=ref
- key = 普通索引:普通索引扫描,返回匹配某个单独值的所有行,会在主键 B + 树添加行锁,普通索引 B + 树上添加行锁。RR 隔离级别下普通索引 B + 树会添加间隙锁。
- type=eq_ref
- key = 唯一索引:唯一索引扫描,返回匹配某个单独值行,会在主键 B + 树添加行锁,唯一索引 B + 树上添加行锁。
- type=const
- key = 主键索引:主键索引扫描,返回匹配某个单独值行,会在主键 B + 树添加行锁。
事务隔离中关于不可重复读,可重复读,幻读的理解#
以下是作者的理解,符合所学知识,大家可以参考。
- 是否可重复读是针对于快照读,幻读是针对于当前读。
- RC 的不可重复读,是因为在 RC 隔离中快照读是获取最新快照版本,所有每次相同 sql 快照读的数据都可能不一样。
- RR 可重复读,是因为在 RR 隔离中快照读是获取指定快照版本,所有每次相同 sql 快照读数据的是一样的。
-
RC 隔离中存在幻读可能性。举个例子有一个获取某表全部信息 sql 当前读(select from table for update)(sql 的语义是锁住全表并获取最新信息),但是这时如果另一个事务执行(insert into table) 语句仍然可以插入,因为 RC 隔离中没有间隙锁。这会导致前面 select from table for update 的数据不是最新有效数据。这就是幻读。
幻读 —— 一个当前读的查询 sql 但是保证不了数据的正确性,这个查询就可能存在幻读。
- RR 隔离中有间隙锁,所以不会幻读。资料上会出现幻读的其实已经被 InnoDB 引擎优化了。
MySQL 架构图 (题外)#
参考文档#
真正理解 Mysql 的四种隔离级别
MySQL 锁总结
MYSQL MVCC 实现原理
理解事务 - MySQL 事务处理机制
Innodb 中的事务隔离级别和锁的关系
MySQL 加锁处理分析
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: