[[434542]]
在之前的博客中,我写了一系列的著作,比拟系统的学习了 MySQL 的事务、阻隔级别、加锁历程以及死锁,我自以为对常见 SQL 语句的加锁旨趣也曾掌捏的满盈了,但看到宥恕网友在驳斥中刻薄的一个问题,我照旧透顶被问蒙了。
他的问题是这么的:
加了插入意向锁后,插入数据之前,此时实行了 select...lock in share mode 语句(莫得取到待插入的值),然后插入了数据,下一次再实行 select...lock in share mode(不会跟插入意向锁突破),发现多了一条数据,于是又产生了幻读。会出现这种情况吗?
这个问题初看上去很浅近,在 RR 阻隔级别下,假定要插入的记载不存在,淌若先实行 select...lock in share mode 语句,很彰着会在记载症结之间加上 GAP 锁,而 insert 语句率先会对记载加插入意向锁,插入意向锁和 GAP 锁突破,是以不存在幻读;淌若先实行 insert 语句后实行 select...lock in share mode 语句,由于 insert 语句在插入记载之后,会对记载加 X 锁,它会绝交 select...lock in share mode 对记载加 S 锁,是以也不存在幻读。两种情况如下所示:
先实行 insERT 后实行 SELECT:
先实行 SELECT 后实行 insERT:
然而咱们仔细想一想就会发现那儿有点分歧劲,咱们知说念 insert 语句会先在插入症结上加上插入意向锁,然后开动写数据,写完数据之后再对记载加上 X 记载锁。
那么问题就来了,淌若在 insert 语句加插入意向锁之后,写数据之前,实行了 select...lock in share mode 语句,这个时候 GAP 锁和插入意向锁是不突破的,查询出来的记载数为 0,然后 insert 语句写数据,加 X 记载锁,因为记载锁和 GAP 锁亦然不突破的,是以 insert 奏效插入了一条数据,这个时候淌若事务提交,select...lock in share mode 语句再次实行查询出来的记载数即是 1,岂不是就出现了幻读?
最新口试题整理好了,点击Java口试库小门径在线刷题。
总共这个词历程如下所示(咱们把 insert 语句的实行分红两个阶段,insERT 1 加插入意向锁,还没写数据,insERT 2 写数据,加记载锁):
一、insERT 加锁的困惑在得出上头的论断时,我也感到很讶异。按理是不成能出现这种情况的,只能能是我对这两个语句的加锁过程还莫得想显著。
于是我又去温习了一遍 MySQL 官方文档,Locks Set by Different SQL Statements in InnoDB 这篇文档对各个语句的加锁有详备的刻画,其中对 insert 的加锁过程是这么说的(这应该是集合上先容 MySQL 加锁机制被援用最多的文档,臆度亦然被诬蔑最多的文档):
insERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.
网站拥有最多样化的博彩游戏和赛事直播,以及最全面、最优质的博彩攻略和技巧分享,为广大博彩爱好者提供最专业的博彩服务。我们的平台安全稳定,操作简便,充值提款便捷,为您带来最佳的博彩体验和最高的博彩收益。Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6 each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.
If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. This can occur if another session deletes the row.
这里讲到了 insert 会对插入的这笔记载加排他记载锁,在加记载锁之前还会加一种 GAP 锁,叫作念插入意向锁,淌若出现唯一键突破,还会加一个分享记载锁。这和我之前的交融是绝对相似的,那么究竟是若何回事呢?难说念 MySQL 的 RR 确实会出现幻读欢悦?
太平洋在线注册在 Google 上搜索了很久,并莫得找到 MySQL 幻读的问题,百念念不得其解之际,遂决定从 MySQL 的源码中一探究竟。另外,MySQL 系列口试题和谜底全部整理好了,微信搜索Java技能栈,在后台发送:口试,不错在线阅读。
二、编译 MySQL 源码编译 MySQL 的源码终点浅近,然而中间也有几个坑,淌若能绕过这几个坑,在腹地调试 MySQL 是一件很容易的事(诚然能调试源码是一趟事,能看懂源码又是另一趟事了)。
我的环境是 Windows 10 x64,系统上安设了 Visual Studio 2012,淌若你的开拓环境和我不相似,编译模样可能也会不同。
在开动之前,率先要从官网下载 MySQL 源码:
这里我给与的是 5.6.40 版块,操作系统下拉列内外选 Source Code,OS Version 给与 Windows(Architecture Independent),然后就不错下载打包好的 zip 源码了。
转型皇冠体育足球赛事分析将源码解压缩到 D:\mysql-5.6.40 目次,在编译之前,还需要再安设几个必要软件:
CMake:CMake 自己并不是编译用具,它是通过编写一种平台无关的 CMakeList.txt 文献来定制编译历程的,然后再笔据办法用户的平台进一步生成所需的腹地化 Makefile 和工程文献,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程; Bison:MySQL 在实行 SQL 语句时,势必要对 SQL 语句进行领路,一般来说语法领路器会包含两个模块:词法分析和语法章程。词法分析和语法章程模块有两个较锻真金不怕火的开源用具 Flex 和 Bison 分离用来处罚这两个问题。MySQL 出于性能和无邪沟通,给与了我方完成词法领路部分,语法章程部分使用了 Bison,是以这里咱们还要先安设 Bison。Bison 的默许安设旅途为 C:\Program Files\GnuWin32,然而千万不要这么,一定要紧记给与一个不带空格的目次,比喻 C:\GnuWin32 要否则在后头使用 Visual Studio 编译 MySQL 时会卡死; Visual Studio:没什么好说的,Windows 环境下臆度莫得比它更好的开拓用具了吧。安设好 CMake 和 Bison 之后,紧记要把它们都加到 PATH 环境变量中。作念好准备责任,咱们就不错开动编译了,率先用 CMake 生成 Visual Studio 的工程文献:
D:\mysql-5.6.40> mkdir project D:\mysql-5.6.40> cd project D:\mysql-5.6.40\project> cmake -G "Visual Studio 11 2012 Win64" ..
cmake 的 -G 参数用于指定生成哪种类型的工程文献,这里是 Visual Studio 2012,不错径直输入 cmake -G 检讨扶持的工程类型。淌若没问题,会在 project 目次下生成一堆文献,其中 MySQL.sln 即是咱们要用的工程文献,使用 Visual Studio 开放它。
开放 MySQL.sln 文献,会在 Solution Explorer 看到 130 个名目,其中有一个叫 ALL_BUILD,这个时候淌若径直编译,编译会失败,在这之前,咱们还要对代码作念点修改:
率先是 sql\sql_locale.cc 文献,看名字就知说念这个文献用于海外化与原土化,这个文献里有各个国度的讲话字符,然而这个文献却是 ANSI 编码,是以要将其改成 Unicode 编码; 开放 sql\mysqld.cc 文献的第 5239 行,将 DBUG_ASSERT(0) 改成 DBUG_ASSERT(1),要否则调试时会触发断言;咫尺咱们不错编译总共这个词工程了,选中 ALL_BUILD 名目,Build,然后静静的恭候 5 到 10 分钟,淌若出现了 Build: 130 succeeded, 0 failed 这么的指示,那么恭喜,你咫尺不错尽情的调试 MySQL 了。
博彩平台历史赔率数据皇冠体育官网咱们将 mysqld 建树为 Startup Project,然后加个号令行参数 --console,这么不错在收场台里检讨打印的调试信息:
另外 client\Debug\mysql.exe 这个文献是对应的 MySQL 的客户端,不错径直双击运行,默许使用的用户为 ODBC@localhost,淌若要以 root 用户登录,不错实行 mysql.exe -u root,不需要密码。
三、调试 insERT 加锁历程率先咱们创建一个数据库 test,然后创建一个测试表 t,主键为 id,并插入测试数据:
> use test; > create table t(id int NOT NULL AUTO_INCREMENT , PRIMARY KEY (id)); > insert into t(id) values(1),(10),(20),(50);
然后咱们开两个客户端会话,一个会话实行 insert into t(id) value(30),另一个会话实行 select * from t where id = 30 lock in share mode。很彰着,淌若咱们能在 insert 语句加插入意向锁之后写数据之前下个断点,再在另一个会话中实行 select 就不错模拟出这种场景了。
那么咱们来找下 insert 语句是在哪加插入意向锁的。第一次看 MySQL 源码可能会有些不知所措,调着调着就会迷失在深深的调用层级中,咱们看 insert 语句的调用堆栈,一开动时还比拟容易交融,从 mysql_parse -> mysql_execute_command -> mysql_insert -> write_record -> handler::ha_write_row -> innobase::write_row -> row_insert_for_mysql,欧博体育官网这里就参加 InnoDb 引擎了。
然后不息往下跟:row_ins_step -> row_ins -> row_ins_index_entry_step -> row_ins_index_entry -> row_ins_clust_index_entry -> row_ins_clust_index_entry_low -> btr_cur_optimistic_insert -> btr_cur_ins_lock_and_undo -> lock_rec_insert_check_and_lock。
一说念跟下来,都莫得发现插入意向锁的痕迹,直到 lock_rec_insert_check_and_lock 这里:
if (lock_rec_other_has_conflicting( static_cast<enum lock_mode>( LOCK_X | LOCK_GAP | LOCK_insERT_INTENTION), block, next_rec_heap_no, trx)) { /* Note that we may get DB_SUCCESS also here! */ trx_mutex_enter(trx); err = lock_rec_enqueue_waiting( LOCK_X | LOCK_GAP | LOCK_insERT_INTENTION, block, next_rec_heap_no, index, thr); trx_mutex_exit(trx); } else { err = DB_SUCCESS; }
这里是检验是否有和插入意向锁突破的其他锁,淌若有突破,就将插入意向锁加到锁恭候队伍中。这很彰着是先实行 select ... lock in share mode 语句再实行 insert 语句时的情景,插入意向锁和 GAP 突破。但这不是咱们要找的点,于是不息探索,然而可惜的是,直到 insert 实行终局,我都莫得找到加插入意向锁的方位。
跟代码终点贫窭,我总结是因为我跟丢了某块的逻辑导致没看到加锁,于是我看了看加其他锁的方位,发咫尺 InnoDb 里行锁都是通过调 lock_rec_add_to_queue(莫得锁突破) 约略 lock_rec_enqueue_waiting(有锁突破,需要恭候其他事务开释锁) 来完毕的,于是在这两个函数高下断点,实行一条 insert 语句,依然莫得断下来,阐扬 insert 语句莫得加任何锁!
到这里我转眼想起之前作念过的 insert 加锁的实验,实行 insert 之后,淌若莫得任何突破,在 show engine innodb status 号令中是看不到任何锁的,这是因为 insert 加的是隐式锁。什么是隐式锁?隐式锁的意念念即是莫得锁!
是以,根柢就不存在之前说的先加插入意向锁,再加排他记载锁的说法,在实行 insert 语句时,什么锁都不会加。这就有点意念念了,淌若 insert 什么锁都不加,那么淌若其他事求实行 select ... lock in share mode,它是如何绝交其他事务加锁的呢?
谜底就在于隐式锁的转化。
皇冠电子游戏InnoDb 在插入记载时,是不加锁的。淌若事务 A 插入记载且未提交,这形势务 B 尝试对这笔记载加锁,事务 B 会先去判断记载上保存的事务 id 是否活跃,淌若活跃的话,那么就匡助事务 A 去成立一个锁对象,然后自身参加恭候事务 A 景色,这即是所谓的隐式锁转化为显式锁。
咱们跟一下实行 select 时的历程,淌若 select 需要加锁,则会走:sel_set_rec_lock -> lock_clust_rec_read_check_and_lock -> lock_rec_convert_impl_to_expl,lock_rec_convert_impl_to_expl 函数的中枢代码如下:
impl_trx = trx_rw_is_active(trx_id, NULL); if (impl_trx != NULL && !lock_rec_has_expl(LOCK_X | LOCK_REC_NOT_GAP, block, heap_no, impl_trx)) { ulint type_mode = (LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP); lock_rec_add_to_queue( type_mode, block, heap_no, index, impl_trx, FALSE); }
率先判断事务是否活跃,然后检验是否已存在排他记载锁,淌若事务活跃且不存在锁,则为该事务加上排他记载锁。而武艺务的锁是通过 lock_rec_convert_impl_to_expl 之后的 lock_rec_lock 函数来加的。
Spring Boot 基础就不先容了,推选下这个实战教程:
https://www.javastack.cn/categories/Spring-Boot/
皇冠hg86a
到这里,这个问题的端倪也曾很清亮了:
实行 insert 语句,判断是否有和插入意向锁突破的锁,淌若有,加插入意向锁,参加锁恭候;淌若莫得,径直写数据,不加任何锁; 实行 select ... lock in share mode 语句,判断记载上是否存在活跃的事务,淌若存在,则为 insert 事务创建一个排他记载锁,并将我方加入到锁恭候队伍;是以不存在网友所说的幻读问题。那么事情到此终局了么?并莫得。
扫视的你会发现,实行 insert 语句时,从判断是否有锁突破,到写数据,这两个操作之间照旧随机候差的,淌若在这之间实行 select ... lock in share mode 语句,由于此时记载还不存在,是以也不存在活跃事务,不会触发隐式锁转化,这条语句会复返 0 笔记载,并加上 GAP 锁;而 insert 语句不息写数据,不加任何锁,在 insert 事务提交之后,select ... lock in share mode 就能查到 1 笔记载,这岂不是还有幻读问题吗?
为了透顶搞明晰这中间的细节,咱们在 lock_rec_insert_check_and_lock 检验完锁突破之后下个断点,然后在另一个事务中实行 select ... lock in share mode,淌若它能奏效复返 0 笔记载,加上 GAP 锁,阐扬就存在幻读。不外事实上,这条 SQL 语句实行的时候卡住了,并不会复返 0 笔记载。从 show engine innodb status 的 TRANSACTIONS 里咱们看不到任何行锁突破的信息,然而咱们从 RW-LATCH INFO 中却不错看出一些端倪:
------------- RW-LATCH INFO ------------- RW-LOCK: 000002C97F62FC70 Locked: thread 10304 file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 879 S-LOCK RW-LOCK: 000002C976A3B998 Locked: thread 10304 file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 256 S-LOCK Locked: thread 10304 file d:\mysql-5.6.40\storage\innobase\include\btr0pcur.ic line 518 S-LOCK Locked: thread 2820 file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 256 S-LOCK Locked: thread 2820 file D:\mysql-5.6.40\storage\innobase\row\row0ins.cc line 2339 S-LOCK RW-LOCK: 000002C976A3B8A8 Waiters for the lock exist Locked: thread 2820 file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 256 X-LOCK Total number of rw-locks 16434 OS WAIT ARRAY INFO: reservation count 10 --Thread 10304 has waited at btr0cur.cc line 256 for 26.00 seconds the semaphore: S-lock on RW-latch at 000002C976A3B8A8 created in file buf0buf.cc line 1069 a writer (thread id 2820) has reserved it in mode exclusive number of readers 0, waiters flag 1, lock_word: 0 Last time read locked in file btr0cur.cc line 256 Last time write locked in file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 256 OS WAIT ARRAY INFO: signal count 8 Mutex spin waits 44, rounds 336, OS waits 7 RW-shared spins 3, rounds 90, OS waits 3 RW-excl spins 0, rounds 0, OS waits 0 Spin rounds per wait: 7.64 mutex, 30.00 RW-shared, 0.00 RW-excl
这里列出了 3 个 RW-LOCK:000002C97F62FC70、000002C976A3B998、000002C976A3B8A8。其中不错看到临了一个 RW-LOCK 有其他线程在恭候其开释(Waiters for the lock exist)。底下列出了总共恭候该锁的线程,Thread 10304 has waited at btr0cur.cc line 256 for 26.00 seconds the semaphore,这里的 Thread 10304 即是咱们正在实行 select 语句的线程,它卡在了 btr0cur.cc 的 256 行,咱们检讨 Thread 10304 的堆栈:
btr0cur.cc 的 256 行位于 btr_cur_latch_leaves 函数,如下所示,通过 btr_block_get 来加锁,看起来像是在造访 InnoDb B+ 树的叶子节点时卡住了:
case BTR_MODIFY_LEAF: mode = latch_mode == BTR_SEARCH_LEAF ? RW_S_LATCH : RW_X_LATCH; get_block = btr_block_get( space, zip_size, page_no, mode, cursor->index, mtr);
这里的 latch_mode == BTR_SEARCH_LEAF,是以加锁的 mode 为 RW_S_LATCH。
这里要先容一个新的见解,叫作念 Latch,一般也把它翻译成 “锁”,但它和咱们之前构兵的行锁表锁(Lock)是有区别的。这是一种轻量级的锁,锁定时候一般终点短,它是用来保证并发线程不错安全的操作临界资源,不时莫得死锁检测机制。Latch 不错分为两种:MUTEX(互斥量)和 RW-LOCK(读写锁),很彰着,这里咱们看到的是 RW-LOCK。
咱们回溯一下 select 语句的调用堆栈:ha_innobase::index_read -> row_search_for_mysql -> btr_pcur_open_at_index_side -> btr_cur_latch_leaves,从调用堆栈不错看出 select ... lock in share mode 语句在造访索引,那么为什么造访索引会被卡住呢?
接下来咱们望望这个 RW-LOCK 是在那儿加上的?从日记里不错看到 Locked: thread 2820 file D:\mysql-5.6.40\storage\innobase\btr\btr0cur.cc line 256 X-LOCK,是以这个锁是线程 2820 加上的,加锁的位置也在 btr0cur.cc 的 256 行,检讨函数援用,很快咱们就查到这个锁是在实行 insert 时加上的,函数堆栈为:row_ins_clust_index_entry_low -> btr_cur_search_to_nth_level -> btr_cur_latch_leaves。
咱们看这里的 row_ins_clust_index_entry_low 函数(无关代码已概略):
UNIV_INTERN dberr_t row_ins_clust_index_entry_low( /*==========================*/ ulint flags, /*!< in: undo logging and locking flags */ ulint mode, /*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE, depending on whether we wish optimistic or pessimistic descent down the index tree */ dict_index_t* index, /*!< in: clustered index */ ulint n_uniq, /*!< in: 0 or index->n_uniq */ dtuple_t* entry, /*!< in/out: index entry to insert */ ulint n_ext, /*!< in: number of externally stored columns */ que_thr_t* thr) /*!< in: query thread */ { /* 开启一个 mini-transaction */ mtr_start(&mtr); /* 调用 btr_cur_latch_leaves -> btr_block_get 加 RW_X_LATCH */ btr_cur_search_to_nth_level(index, 0, entry, PAGE_CUR_LE, mode, &cursor, 0, __FILE__, __LINE__, &mtr); if (mode != BTR_MODIFY_TREE) { /* 不需要修改 BTR_TREE,乐不雅插入 */ err = btr_cur_optimistic_insert( flags, &cursor, &offsets, &offsets_heap, entry, &insert_rec, &big_rec, n_ext, thr, &mtr); } else { /* 需要修改 BTR_TREE,先乐不雅插入,乐不雅插入失败则进行悲不雅插入 */ err = btr_cur_optimistic_insert( flags, &cursor, &offsets, &offsets_heap, entry, &insert_rec, &big_rec, n_ext, thr, &mtr); if (err == DB_FAIL) { err = btr_cur_pessimistic_insert( flags, &cursor, &offsets, &offsets_heap, entry, &insert_rec, &big_rec, n_ext, thr, &mtr); } } /* 提交 mini-transaction */ mtr_commit(&mtr); }
这里是实行 insert 语句的关节,不错发施行行插入操作的前后分离有一转代码:mtr_start() 和 mtr_commit()。这被称为 迷你事务(mini-transaction),既然叫作念事务,那这个函数的操作确定是原子性的,事实上如实如斯,insert 会在检验锁突破和写数据之前,会对记载所在的页加一个 RW-X-LATCH 锁,实行完写数据之后再开释该锁(本色上写数据的操作即是写 redo log(重作念日记),将脏页加入 flush list,这个后头随机候再深刻分析了)。
澳门金沙城赌场官网这个锁的开释终点快,然而这个锁足以保证在插入数据的过程中其他事务无法造访记载所在的页。mini-transaction 也不错包含子事务,本色上在 insert 的实行过程中就会增加个 mini-transaction。
另外,关注公众号Java技能栈,在后台复兴:口试,不错获取我整理的 Java/ MySQL 系列口试题和谜底,终点皆全。
皇冠客服飞机:@seo3687每个 mini-transaction 会死守底下的几个章程:
修改一个页需要得回该页的 X-LATCH; 造访一个页需要得回该页的 S-LATCH 或 X-LATCH; 持有该页的 LATCH 直到修改约略造访该页的操作完成。是以,临了的临了香港六合彩在线,真相唯唯一个:insert 和 select ... lock in share mode 不会发生幻读。总共这个词历程如下:
实行 insert 语句,对要操作的页加 RW-X-LATCH,然后判断是否有和插入意向锁突破的锁,淌若有,加插入意向锁,参加锁恭候;淌若莫得,径直写数据,不加任何锁,终局后开释 RW-X-LATCH; 实行 select ... lock in share mode 语句,对要操作的页加 RW-S-LATCH,淌若页面上存在 RW-X-LATCH 会被梗阻,莫得的话则判断记载上是否存在活跃的事务,淌若存在,则为 insert 事务创建一个排他记载锁,并将我方加入到锁恭候队伍,临了也会开释 RW-S-LATCH。