数据库并发如何让数据操作串行化
本章主要讲解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 协议》,转载必须注明作者和本文链接
:+1: