MVCC多版本并发控制和幻读问题的解决
首先我们先介绍一下锁的分类,再进入今天的正题。
一、锁分类:
1.从性能上分:乐观锁、悲观锁。乐观锁(用版本号对比或CAS机制)适用于读比较多的场景,悲观锁适用于写比较多的场景。如果在写比较多的场景使用乐观锁,会导致对比次数过多,影响性能。
2.从对数据的粒度上分:表锁、页锁、行锁。
3.从对数据库的操作上分:读锁(悲观锁)、写锁(悲观锁)、意向锁。
- 读锁(共享锁,S锁(shared)):针对同一条数据加了读锁之后,其他读操作可以同时进行,不受影响。
比如:select * from A where id = 1 lock in share mode. - 写锁(排他锁,X锁(exclusive)):针对同一条数据加了写锁之后,其他的事务不能写和读。insert、update、delete会加写锁,查询可以通过加for update加写锁。
比如:select * from A where id = 1 for update. - 意向锁:又称I锁,表锁。主要是为了提高加表锁的效率,mysql提供的。
- 读锁(共享锁,S锁(shared)):针对同一条数据加了读锁之后,其他读操作可以同时进行,不受影响。
间隙锁和临键锁:
间隙锁的目的主要是为了防止幻读,以满足相关隔离级别的要求。间隙锁主要通过两个方面实现这个目的:一是防止间隙内有新数据被插入,二是防止已存在的数据被更新成间隙内的数据。 1.间隙锁:锁的是两个值之间的间隙,在可重复读隔离级别下生效,mysql默认是RR级别,RR级别下有幻读问题,间隙锁就是为了解决幻读问题出现的。
2.临键锁:行锁和间隙锁的组合。
注意:关于RR级别行锁升级为表锁的原因:
在RR级别下,需要解决不可重复读和幻读问题。在遍历扫描聚集索引记录时,为了防止扫描过的索引被其他事务修改(不可重复读问题) 或 间隙被其他事务插入记录(幻读问题),从而导致数据不一致,索引mysql的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其他的事务锁住表里的其他记录。
二、MVCC多版本并发控制:
首先先介绍几个相关的概念:
1.mvcc定义和核心思想:
定义:mvcc是一种并发控制机制,用来控制并发执行的事务,保证事务之间的隔离性。
核心思想:mvcc是通过保存某个时间点的数据生成快照版本来定义的。mvcc允许同一条记录有不同的快照版本,不同事务在查询时通过添加一些约束条件,就可以得到对于某个时刻快照版本的数据。
mysql在读已提交和可重复读隔离级别下都实现了mvcc机制。
2.快照读和当前读:
1.快照读:基于mvcc机制和undo log实现的,适用于简单的select语句。
2.当前读:基于临键锁(行锁+间隙锁)实现的,适用于update、insert、delete、select...for update、select...lock in share mode及加锁的select语句。
3.undo日志版本链:
undo日志版本链是指一行数据被每个事务依次修改过后,在每个事务修改完后,会保留修改前的数据到undo log日志中,通过trx_id和roll_pointer字段将undo log日志串连成一个历史记录日志版本链。
4.MVCC版本对比规则:

在可重复读隔离级别下,当事务开启时,任何查询的sql在第一次select时都会生成一致性视图read-view,并且这个视图在事务结束前都不会改变。这个视图是由所有未提交的事务组成的事务组,事务组里面的事务查询sql都需要都对应记录版本链的最新记录逐条和read-view做对比,最终得到想要的快照结果。 注:在读已提交的隔离级别下,每次select查询都会生成一个一致性视图。 a.create_trx_id : 当前事务id
b.trxs组:当前所有未提交事务
c.min_trx_id: 最小未提交事务id
d.max_trx_id:最大未提交事务id
# 版本对比规则:
1.当trx_id = create_trx_id可见。
2.当trx_id < min_trx_id,表示这个版本由已提交事务生成的,可见。
3.当trx_id > max_trx_id,表示这个版本是由将来的事务生成的,可见。
4.当min<= trx_id <= max_trx_id,分两种情况:
a.当trx_id在这个trxs组内,说明这个版本是未提交的事务,不可见。
b.当trx_id不再这个trxs组内,说明这个版本是由已提交事务生成的,可见。
三、幻读问题的解决:
首先回顾一下事务的四大特性和事务的隔离级别,以及不同的隔离级别会出现什么样的问题。
事务的四大特性:原子性、一致性、隔离性、持久性。
事务的隔离级别:读未提交、读已提交、不可重复读、串行化。
读未提交:会出现脏读(当前内存读),不可重复读,幻读问题。
读已提交:不会出现脏读问题,但会出现不可重复读,幻读问题。
不可重复读:不会出现脏读、不可重复读问题,但会出现幻读问题(mvcc机制和锁可以彻底解决这个问题)。
串行化:串行化读取数据,但是事务的并发度就没有了。
* 脏读:读取到其他事务未提交的数据
* 不可重复读问题:指的是在同一个事务中,多次查询同一条数据(已经存在的数据),由于其他事务的修改,导致查询的结果不一样。
* 幻读:指的是在一个事务中,查询一个范围内的数据,一般是count,多次返回的数量不一样,查询到其他事务新插入的数据。
结论:先说一下结论,在RR隔离级别下,幻读问题可以通过mvcc机制和间隙锁或临键锁解决(必需让查询语句使用当前读,不能使用快照读)。
下面举了一些示例:CREATE TABLE `gap_test` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`number` int(11) NOT NULL COMMENT '数字',
PRIMARY KEY (`id`) USING BTREE,
KEY `index_number` (`number`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='间隙锁表'

1.成功解决幻读的问题示例:
#间隙锁示例:(间隙锁之间是兼容的,高并发的场景下,不正确的使用可能会导致死锁)
session 1:
begin;
select * from gap_test where number = 8 from update; // 记录不存在,执行成功 session 2:
begin;
select * from gap_test where number = 8 from update; // 记录不存在,执行成功
insert into gap_test value (11,11);//阻塞
insert into gap_test value (7,5);//阻塞
insert into gap_test value (2,5);//执行成功
insert into gap_test value (14,11);//执行成功 这时会对number=8进行检索,检索不到记录,会向左取最近的值number=5作为左区间,向右取最近的值number=11作为右区间,所以session 1锁的间隙范围为:number(5,11)。即在(id=6,number=5)和(id=13,number=11)这个间隙范围内不能插入任何数据。 #临键锁示例:
session 1:
begin;
select * from gap_test where number = 5 from update;//记录存在,执行成功(会加写锁) session 2:
begin;
select * from gap_test where number = 5 from update;//阻塞(写锁互斥)
insert into gap_test value (4,4);//阻塞
insert into gap_test value (4,5);//阻塞
insert into gap_test value (8,8);//阻塞
insert into gap_test value (11,11);//阻塞
insert into gap_test value (2,4);//执行成功
insert into gap_test value (14,11);//执行成功 这时会对number = 5进行检索,检索到记录之后,会对number =5的记录加写锁,然后向左取最近的值number=4作为左区间,向右取最近的值number=11作为右区间,所以session 1锁的间隙范围为:number(4,5),number(5,11)。即在(id=3,number=4)和(id=6,number=5)、(id=6,number=5)和(id=13,number=11)之间的间隙范围内不能插入任何数据。
2.没有解决幻读问题的示例:


- 上面的示例产生了幻读问题。事务A和事务B同时执行,事务A修改了事务B的新提交的记录,再查询时查到上次没有查到的记录,产生了幻读。要彻底解决幻读问题,查询语句需要加锁,由快照读变为当前读。
3.高并发的场景下,不正确的使用可能会导致死锁示例:


- 事务A和事务B同时开启事务执行查询语句,这个间隙锁的范围是(30, +oo),事务B先在间隙锁范围内插入一条语句,事务A也在间隙锁范围内插入一条语句,然后发生了死锁。
- 我们执行一下SHOW ENGINE INNODB STATUS语句查看死锁日志:
=====================================
2024-01-12 16:42:35 4af4 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 34 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 661 srv_active, 0 srv_shutdown, 239375 srv_idle
srv_master_thread log flush and writes: 240027
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 156
OS WAIT ARRAY INFO: signal count 155
Mutex spin waits 228, rounds 1256, OS waits 27
RW-shared spins 125, rounds 3674, OS waits 121
RW-excl spins 4, rounds 264, OS waits 4
Spin rounds per wait: 5.51 mutex, 29.39 RW-shared, 66.00 RW-excl
------------------------
### 发生死锁
LATEST DETECTED DEADLOCK
------------------------
2024-01-12 16:40:30 48e8
*** (1) TRANSACTION:
TRANSACTION 1488272, ACTIVE 67 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root update
insert into gap_test value (40,36)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488272 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION:
TRANSACTION 1488273, ACTIVE 48 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root update
insert into gap_test value (41,37)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (2)
------------
TRANSACTIONS
------------
Trx id counter 1488278
Purge done for trx's n:o < 1488278 undo n:o < 0 state: running but idle
History list length 583
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started
MySQL thread id 41, OS thread handle 0x4ac8, query id 2577 ::1 root cleaning up
---TRANSACTION 1488267, not started
MySQL thread id 40, OS thread handle 0x4af4, query id 2629 ::1 root init
SHOW ENGINE INNODB STATUS
---TRANSACTION 1488273, not started
MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root cleaning up
---TRANSACTION 1488272, ACTIVE 192 sec
4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root cleaning up
--------
FILE I/O
--------
I/O thread 0 state: wait Windows aio (insert buffer thread)
I/O thread 1 state: wait Windows aio (log thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
I/O thread 9 state: wait Windows aio (write thread)
Pending normal aio reads: 0 [0, 0, 0, 0] , aio writes: 0 [0, 0, 0, 0] ,
ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
3071 OS file reads, 882 OS file writes, 559 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 276707, node heap has 8 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number 1239834765
Log flushed up to 1239834765
Pages flushed up to 1239834765
Last checkpoint at 1239834765
0 pending log writes, 0 pending chkp writes
251 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137428992; in additional pool allocated 0
Dictionary memory allocated 4504559
Buffer pool size 8192
Free buffers 5820
Database pages 2364
Old database pages 888
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 2339, created 25, written 525
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 2364, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Main thread id 7344, state: sleeping
Number of rows inserted 34, updated 25, deleted 0, read 3991433
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================
后面一章,我们会着重分析一下mysql在RR隔离级别下的加锁过程,敬请期待!
MVCC多版本并发控制和幻读问题的解决的更多相关文章
- InnoDB学习(五)之MVCC多版本并发控制
MVCC多版本并发控制,是一种数据库管理系统并发控制的方法.MVCC多版本并发控制下,数据库中的数据会有多个版本,分别对应不同的事务,从而达到事务之间并发数据的隔离.MVCC最大的优势是读不加锁,读写 ...
- MVCC多版本并发控制
MVCC多版本并发控制 爱情小傻蛋关注 82019.09.28 23:23:37字数 4,740阅读 91,421 前提概要 什么是MVCC 什么是当前读和快照读? 当前读,快照读和MVCC的关系 M ...
- mysql的MVCC多版本并发控制机制
MVCC多版本并发控制机制 全英文名:Multi-Version Concurrency Control MVCC不会通过加锁互斥来保证隔离性,避免频繁的加锁互斥. 而在串行化隔离级别为了保证较高的隔 ...
- 【Mysql】深入理解 MVCC 多版本并发控制
MVCC MVCC(Multi-Version Concurrency Control),即多版本并发控制.是 innodb 实现事务并发与回滚的重要功能.锁机制可以控制并发操作,但是其系统开销较大, ...
- MySQL MVCC(多版本并发控制)
概述 为了提高并发MySQL加入了多版本并发控制,它把旧版本记录保存在了共享表空间(undolog),当事务提交之后将重做日志写入磁盘(前提innodb_flush_log_at_trx_commit ...
- MVCC多版本并发控制的理解
前置知识 当前读与快照读 当前读 什么是当前读:读取的是最新的数据,不会读到老数据. 何时触发:update.insert.delete.select lock in share mode.selec ...
- [MySQL] MVCC 多版本并发控制实现的事务
1.没有一个统一的实现标准,实现了非阻塞的读操作,写操作也只锁定必要的行2.通过保存数据在某个时间点的快照实现的3.典型的有乐观并发控制和悲观并发控制4.innodb的mvcc是每次事务都有递增的版本 ...
- 聊聊MVCC多版本并发控制
一.介绍 MVCC只在RR和RC 2个隔离级别下才能工作.MySQL的大多数事务存储引擎实现的都不是简单的行级锁机制.基于提升并发性能的考虑,它们一般都同时实现了MVCC. 通俗的来讲,MVCC是行级 ...
- 针对MySQL的MVCC多版本并发控制的一些总结
MVCC MVCC细节太多,我直接备忘一下总结: MVCC就是通过事务的ID与行数据的版本(修改事务的ID)进行比较(通过redo log可以回溯版本)得出哪些版本的行数据可见和不可见而实现的事务隔离 ...
- MVCC 多版本并发控制
关于事务的介绍暂且不谈. InnoDB行级锁,虽然在很大程度上提高了事务的并发性,但是终究还是要耗费很大的.为了更进一步的提高并发性同时降低开销,存储引擎会同时实现MVCC. InnoDB实现MVCC ...
随机推荐
- vue-router钩子执行顺序
Vue的路由在执行跳转时,根据源码可知,调用了router中定义的navigate函数 function push(to: RouteLocationRaw) { return pushWithRed ...
- 万字解析XML配置映射为BeanDefinition的源码
本文分享自华为云社区<Spring高手之路16--解析XML配置映射为BeanDefinition的源码>,作者:砖业洋__. 1. BeanDefinition阶段的分析 Spring框 ...
- UData+StarRocks在京东物流的实践
1 背景 数据服务与数据分析场景是数据团队在数据应用上两个大的方向,行业内大家有可能会遇到下面的问题: 1.1 数据服务 烟囱式开发模式:每来一个需求开发一个数据服务,数据服务无法复用,难以平台化,技 ...
- 【结对作业】第一周 | 学习体会day03
昨天解决线路查询时遇到的type接受为空导致出现空指针异常抛出,后来发现是因为传递的数据类型出现了问题,更改数据类型之后问题就得到了解决今天在实现站点查询线路时遇到了乱码问题,在这之前我们单独编写代码 ...
- jvm的jshell,学生的工具
jshell 在我眼里,只能作为学校教学的一个玩具,事实上官方也做了解释,以下是官方的解释: 在学习编程语言时,即时反馈很重要,并且 它的 API.学校引用远离Java的首要原因 教学语言是其他语言 ...
- 网安靶场环境_DVWA-读取文件报错File not found! Cookie中有两个security键
DVWA-文件包含漏洞-读取文件报错-ERROR: File not found! Cookie中有两个security键 1 问题复现 (1)登录DVMA后,设置DVWA Security为Low. ...
- 2023年奔走的总结---吉特日化MES 制药项目 篇二
书接上文,反正今年也就折腾一下了,索性好好整理一下思绪写写文章,当做工作笔记.今年工作中遇到了各种各样的事情,可能是由于今年项目压力像无头苍蝇一样瞎撞,也打发一下按耐不住的心.本篇将记录一下<吉 ...
- HDU-2159 二维背包
最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务.久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级.现在的问题是,xhd升掉最后一级还需n的经 ...
- CentOS基线检测脚本
本脚本适用于CentOS 7.5-7.9版本,其他版本不详 1.检查系统信息 查看代码 echo " " echo "########################## ...
- 技本功|数据安全之IDC数据容灾设计实现
近年来,数据安全问题日渐受到大家的关注,对于任何一家企业,数据无疑是最重要的资产之一.提到数据容灾,大家可能会想到备库和备份的概念,那么我们先来谈谈备库与备份的区别. 备库与备份的区别 通常来讲,备库 ...