InnoDB的锁机制浅析(三)—幻读
文章总共分为五个部分:
- InnoDB的锁机制浅析(一)—基本概念/兼容矩阵
- InnoDB的锁机制浅析(二)—探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/插入意向锁)
- InnoDB的锁机制浅析(三)—幻读
- InnoDB的锁机制浅析(四)—不同SQL的加锁状况
- InnoDB的锁机制浅析(五)—死锁场景(Insert死锁)
大而全版(五合一):InnoDB的锁机制浅析(All in One)
前言
这一章节,我们通过幻读,逐步展开对InnoDB锁的探究。
1 幻读概念
解释了不同概念的锁的作用域,我们来看一下幻读到底是什么。幻读在RR条件下是不会出现的。因为RR是Repeatable Read,它是一种事务的隔离级别,直译过来也就是“在同一个事务中,同样的查询语句的读取是可重复”,也就是说他不会读到”幻影行”(其他事务已经提交的变更),它读到的只能是重复的(无论在第一次查询之后其他事务做了什么操作,第二次查询结果与第一次相同)。
上面的例子都是使用for update
,这种读取操作叫做当前读,对于普通的select
语句均为快照读。
当前读,又叫加锁读,或者 阻塞读。这种读取操作不再是读取快照,而是读取最新版本并且加锁。
快照读不会添加任何锁。
官方文档对于幻读的定义是这样的:
原文:The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
手动无脑翻译:所谓的幻影行问题是指,在同一个事务中,同样的查询语句执行多次,得到了不同的结果,这就是幻读。例如,如果同一个SELECT
语句执行了两次,第二次执行的时候比第一次执行时多出一行,则该行就是所谓的幻影行。
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.
,这句话看起来应该是不可重复读的定义,同样的查询得到了不同的结果(两次结果不是重复的),但是后面的举例给出了幻读真正的定义,第二次比第一次多出了一行。也就是说,幻读的出现有这样一个前提,第二次查询前其他事务提交了一个INSERT
插入语句。而不可重复读出现的前提是第二次查询前其他事务提交了UPDATE
或者DELETE
操作。
mysql的快照读,使得在RR的隔离级别上在next-Key的作用区间内,制造了一个快照副本,这个副本是隔离的,无论副本对应的区间里的数据被其他事务如何修改,在当前事务中,取到的数据永远是副本中的数据。
RR级别下之所以可以读到之前版本的数据,是由于数据库的MVCC(Multi-Version Concurrency Control,多版本并发控制)。参见InnoDB Multi-Versioning
有些文章中提到“RR也不能完全避免幻读”,实际上官方文档实际要表达的意义是“在同一个事务内,多次连续查询的结果是一样的,不会因其他事务的修改而导致不同的查询结果”,这里先给出实验结论:
1.当前事务如果未发生更新操作(增删改),快照版本会保持不变,多次查询读取的副本是同一个。
2.当前事务如果发生更新(增删改),再次查询时,会刷新快照版本。
示例的基础是一个只有两列的数据库表。
mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL,
PRIMARY KEY(id),
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
mysql> INSERT INTO test(id,code) values(1,1),(10,10);
2 RC级别下的幻读
RC情况下会出现幻读。
首先设置隔离级别为RC,SET SESSION tx_isolation='READ-COMMITTED';
事务一 | 事务二 |
---|---|
mysql> SET SESSION tx_isolation='READ-COMMITTED'; mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 9 | 9 | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
RC(Read Commit)隔离级别可以避免脏读,事务内无法获取其他事务未提交的变更,但是由于能够读到已经提交的事务,因此会出现幻读和不重复读。
也就是说,RC的快照读是读取最新版本数据,而RR的快照读是读取被next-key锁作用区域的副本
3 RR级别下能否避免幻读?
我们先来模拟一下RR隔离级别下没有出现幻读的情况:
开启第一个事务并执行一次快照查询。
事务一 | 事务二 |
---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
这两个事务的执行,有两个问题:
1.为什么之前的例子中,在第二个事务的INSERT
被阻塞了,而这次却执行成功了。
这是因为原来的语句中带有for update
,这种读取是当前读,会加锁。而本次第一个事务中的SELECT
仅仅是快照读,没有加任何锁。所以不会阻塞其他的插入。
2.数据库中的数据已经改变,为什么会读不到?
这个就是之前提到的next-key lock锁定的副本。RC及以下级别才会读到已经提交的事务。更多的业务逻辑是希望在某段时间内或者某个特定的逻辑区间中,前后查询到的数据是一致的,当前事务是和其他事务隔离的。这也是数据库在设计实现时遵循的ACID原则。
再给出RR条件下出现幻读的情形,这种情形不需要两个事务,一个事务就已经可以说明,
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.00 sec)
mysql> update test set code=9 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 | 9 |
+----+------+
1 row in set (0.00 sec)
至于RR隔离级别下到底会不会出现幻读,就需要看幻读的定义中的查询到底是连续的查询还是不连续的查询。如果认为RR级别下可能会出现幻读,那该级别下也会出现不重复读。
RR隔离级别下,虽然不会出现幻读,但是会因此产生其他的问题。
前提:当前数据表中只存在(1,1),(5,5),(10,10)三组数据。
如果数据库隔离级别不是默认,可以执行SET SESSION tx_isolation='REPEATABLE-READ';
(该语句不是全局设置)更新为RR。
然后执行下列操作:
事务一 | 事务二 | 备注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
开启事务一,并查询code>8 的记录,只有一条(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
开启第二个事务,插入(11,11)并提交 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事务一再查询一次,由于RR级别并没有读到更新 | |
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); ERROR 1062 (23000): Duplicate entry '11' for key 'PRIMARY' |
事务一明明没有查到,却插入不了 |
4 更新丢失(Lost Update)
4.1 更新丢失
除了上述这类问题外,RR还会有丢失更新的问题。
如下表给出的操作:
事务一 | 事务二 | 备注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
开启事务一,并查询code>8 的记录,只有一条(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update test set id=12,code=12 where id=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) |
开启第二个事务,将(10,10)改为(12,12)并提交,注意这里matched是1,changed也是1 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事务一再次查询code>8 的记录,仍然只有一条(10,10) |
|
mysql> update test set id=9,code=9 where id=10; Query OK, 0 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 |
这里查询到0条,更新了0条 |
这个例子里,事务一的更新是无效的,尽管在这个事务里程序认为还存在(10,10)记录。
事务一中更新之前的SELECT
操作是快照读,所以读到了快照里的(10,10),而UPDATE
中的WHERE
子句是当前读,取得是最新版本的数据,所以matched: 0 Changed: 0
。
如果上述例子中的操作是对同一条记录做修改,就会引起更新丢失。例如,事务一和二同时开启,事务一先执行update test set code=100 where id=10;
,事务二再执行update test set code=200 where id=10;
,事务一的更新就会被覆盖。
这就是经典的丢失更新问题,英文叫
Lost Update
,又叫提交覆盖,因为是最后执行更新的事务提交导致的覆盖。还有一种更新丢失叫做回滚覆盖,即一个事务的回滚把另一个事务提交的数据给回滚覆盖了,但是目前市面上所有的数据库都不支持这种stupid的操作,因此不再详述。
4.2 乐观锁与悲观锁
这种情况下,引入我们常见的两种方式来解决该问题
- 乐观锁:在
UPDATE
的WHERE
子句中加入版本号信息来确定修改是否生效 - 悲观锁:在
UPDATE
执行前,SELECT
后面加上FOR UPDATE
来给记录加锁,保证记录在UPDATE
前不被修改。SELECT ... FOR UPDATE
是加上了X锁,也可以通过SELECT ... LOCK IN SHARE MODE
加上S锁,来防止其他事务对该行的修改。
无论是乐观锁还是悲观锁,使用的思想都是一致的,那就是当前读。乐观锁利用当前读
判断是否是最新版本,悲观锁利用当前读
锁定行。
但是使用乐观锁时仍然需要非常谨慎,因为RR是可重复读的,一定不能在UPDATE之前先把版本号使用快照读获取出来。
InnoDB的锁机制浅析(三)—幻读的更多相关文章
- InnoDB的锁机制浅析(五)—死锁场景(Insert死锁)
可能的死锁场景 文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/插入意 ...
- InnoDB的锁机制浅析(四)—不同SQL的加锁状况
不同SQL的加锁状况 文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/ ...
- InnoDB的锁机制浅析(二)—探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/插入意向锁)
Record锁/Gap锁/Next-key锁/插入意向锁 文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Recor ...
- InnoDB的锁机制浅析(一)—基本概念/兼容矩阵
InnoDB锁的基本概念 文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Record锁/Gap锁/Next-key ...
- InnoDB的锁机制浅析(All in One)
目录 InnoDB的锁机制浅析 1. 前言 2. 锁基本概念 2.1 共享锁和排它锁 2.2 意向锁-Intention Locks 2.3 锁的兼容性 3. InnoDB中的锁 3.1 准备工作 3 ...
- InnoDB之锁机制
前两天听了姜老大关于InnoDB中锁的相关培训,刚好也在看这方面的知识,就顺便利用时间把这部分知识做个整理,方便自己理解.主要分为下面几个部分 1. InnoDB同步机制 InnoDB存储引擎有两种同 ...
- 巧用MySQL InnoDB引擎锁机制解决死锁问题(转)
该文会通过一个实际例子中的死锁问题的解决过程,进一步解释innodb的行锁机制 最近,在项目开发过程中,碰到了数据库死锁问题,在解决问题的过程中,笔者对MySQL InnoDB引擎锁机制的理解逐步加深 ...
- MySQL锁机制浅析
MySQL使用了3种锁机制 行级锁,开销大,加锁慢,会出现死锁,发生锁冲突的概率最高,并发度也最高 表级锁,开销小,加锁快,不会出现死锁,发生锁冲突的概率最低,并发度最低 页级锁,开销和加锁时间界于表 ...
- PostgreSQL 锁机制浅析
锁机制在 PostgreSQL 里非常重要 (对于其他现代的 RDBMS 也是如此).对于数据库应用程序开发者(特别是那些涉及到高并发代码的程序员),需要对锁非常熟悉.对于某些问题,锁需要被重点关注与 ...
随机推荐
- python---django中models配置修改数据库引擎
Django支持多种数据库,sqlite,mysql,oracle等,其默认数据库是sqlite 在settings文件中可以发现: DATABASES = { 'default': { 'ENGIN ...
- SQL记录-PLSQL条件控制
PL/SQL条件控制 决策结构需要程序员指定一个或多个条件要计算,或由程序进行测试,如果条件被确定为真那么一条或多条语句被执行,如果要被执行的其它语句条件被确定为假,则选其它执行块. 以下是从在大 ...
- C#获取文件超大图标256*256(转)
从Bing搜索得到,保存于此 using System; using System.Collections.Generic; using System.Linq; using System.Text; ...
- fastJson顺序遍历JSON字段(转)
fastJson在把json格式的字符串转换成JSONObject的时候,使用的是HashMap,所以排序规则是根据HASH值排序的,如果想要按照字符串顺序遍历JSON属性,需要在转换的时候指定使用L ...
- hdu 5181 numbers
http://acm.hdu.edu.cn/showproblem.php?pid=5181 题意: 有一个栈,其中有n个数1~n按顺序依次进入栈顶,在某个时刻弹出. 其中m个限制,形如数字A必须在数 ...
- HDU 5528 反演
$f(m)=\sum\limits_{i=1}^{m-1}\sum\limits_{j=1}^{m-1}[(ij,m) \ne m]$,$g(n)=\sum\limits_{m|n}f(m)$,$1 ...
- Nginx 服务器性能Bug和性能优化方案(真实经历)
一.遇到的问题 1.问题:本应该是3个ffmpeg ,但是怎么会有5个ffmpeg出现? 2.Lua脚本问题,一直写入日志,导致有大量的日志,这里的错误日志是直接写进nginx的error.log 日 ...
- JavaScript绝句的小研究
前几日在网上看到一篇文章:JavaScript绝句,看了以后觉得里面的代码颇为有趣,不过文章里面只是简单的说了这样写的目的和结果,却没有令读者起到既知其然,又知其所以然的效果.这里简单写一篇小文章剖析 ...
- 从零开始编写自己的JavaScript框架(一)
1. 模块的定义和加载 1.1 模块的定义 一个框架想要能支撑较大的应用,首先要考虑怎么做模块化.有了内核和模块加载系统,外围的模块就可以一个一个增加.不同的JavaScript框架,实现模块化方式各 ...
- 日期控件-my97DatePicker用法
网上资料,用法,只能选最近30天等等:http://jingyan.baidu.com/article/e6c8503c7244bae54f1a18c7.html