作者:赵黎明 爱可生MySQLDBA团队成员,Oracle10gOCM,MySQL5。7OCP,擅长数据库性能问题诊断、事务与锁问题的分析等,负责处理客户MySQL及我司自研DMP平台日常运维中的问题,对开源数据库相关技术非常感兴趣。 本文来源:原创投稿爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 什么是半一致性读? 先看下官方的描述: 是一种用在Update语句中的读操作(一致性读)的优化,是在RC事务隔离级别下与一致性读的结合。 当Update语句的where条件中匹配到的记录已经上锁,会再次去InnoDB引擎层读取对应的行记录,判断是否真的需要上锁(第一次需要由InnoDB先返回一个最新的已提交版本)。 只在RC事务隔离级别下或者是设置了innodblocksunsafeforbinlog1的情况下才会发生。 innodblocksunsafeforbinlog参数在8。0版本中已被去除(可见,这是一个可能会导致数据不一致的参数,官方也不建议使用了)。 测试案例 InnoDB引擎的强大之处就在于它能完美地支持事务,而事务的一致性则是由事务隔离级别和并发事务锁来保证的。接下来,我们先通过2个测试案例来观察半一致性读会对事务产生哪些影响。 案例1 RC隔离级别,3个Session执行事务语句创建测试表rootlocalhost:mysqld。sock〔zlm〕5。7。30logcreatetablezlm。t(idint,salint)engineinnodbdefaultcharactersetutf8mb4;QueryOK,0rowsaffected(0。06sec) rootlocalhost:mysqld。sock〔zlm〕5。7。30logshowcreatetablezlm。tG。rowTable:tCreateTable:CREATETABLEt(idint(11)DEFAULT,salint(11)DEFAULT)ENGINEInnoDBDEFAULTCHARSETutf8mb4rowinset(0。01sec) 写入测试数据(创建插入数据的存储过程)rootlocalhost:mysqld。sock〔zlm〕5。7。30logdropprocedureifexistszlm。proct;QueryOK,0rowsaffected(0。00sec) rootlocalhost:mysqld。sock〔zlm〕5。7。30logdelimiterrootlocalhost:mysqld。sock〔zlm〕5。7。30logcreateprocedurezlm。proctbegindeclareiintdefault;declarejintdefault100;whilei11doinsertintot(id,sal)values(i,j);setii;setjj100;endwhile;endQueryOK,0rowsaffected(0。01sec) rootlocalhost:mysqld。sock〔zlm〕5。7。30logdelimiter;rootlocalhost:mysqld。sock〔zlm〕5。7。30logselectfromt;idsal10022003004400550066007008800990010100010rowsinset(0。00sec) 开启RC隔离级别rootlocalhost:mysqld。sock〔zlm〕5。7。30logsetglobal。txisolationreadcommitted;QueryOK,0rowsaffected,1warning(0。00sec) 注意,从8。0。3版本开始,去掉了txisolation参数,参数名只支持transactionisolation开启两个新的Session(设置global参数后,仅对新连接生效)确认Session的隔离级别和线程IDrootlocalhost:mysqld。sock〔(none)〕5。7。30logshowvariablesliketxisolation;VariablenameValuetxisolationREADCOMMITTEDrowinset(0。01sec) rootlocalhost:mysqld。sock〔(none)〕5。7。30logselectconnectionid;connectionidrowinset(0。00sec) Session执行当前读的Select语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;selectfromtwhereidandid6forupdate;QueryOK,0rowsaffected(0。00sec) idsal4005500rowsinset(0。00sec) 查看加锁详情(需设置参数innodbstatusoutputlockson,否则看不到IX锁)线程的事务1314,获取到了个表级插入意向锁IX,个记录锁,对应id,id5的这两条记录TRANSACTION1313,ACTIVEseclockstruct(s),heapsize1136,rowlock(s)MySQLthreadid,OSthreadhandle140086065960704,queryid277localhostrootTABLELOCKtablezlm。ttrxid1313lockmodeIXRECORDLOCKSspaceid23pagenonbits80indexGENCLUSTINDEXoftablezlm。ttrxid1313lockmodeXlocksrecbutnotgapRecordlock,heapno5PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000204;asc;;:len6;hex000000000515;asc;;:len;hexb0000001240110;asc;;:len;hex80000004;asc;;:len;hex80000190;asc;; Recordlock,heapno6PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000205;asc;;:len6;hex000000000516;asc;;:len;hexb1000001250110;asc;;:len;hex80000005;asc;;:len;hex800001f4;asc;; 确认Session的隔离级别和线程IDrootlocalhost:mysqld。sock〔(none)〕5。7。30logshowvariablesliketxisolation;VariablenameValuetxisolationREADCOMMITTEDrowinset(0。00sec) rootlocalhost:mysqld。sock〔(none)〕5。7。30logselectconnectionid;connectionid9rowinset(0。00sec) Session执行当前读的Select语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;selectfromtwhereidforupdate;QueryOK,0rowsaffected(0。00sec) ERROR1205(HY000):Unknownerror12051205表示锁等待超时,这里吐槽下最新GA的5。7。30,遇到错误仅会抛出一个代码,没有错误描述,不方便排查 查看加锁详情innodb锁等待超时前,可以看到线程9的1314事务正在请求并等待个记录锁,id的这条记录TRANSACTION1314,ACTIVEsecfetchingrowsmysqltablesinuse,lockedLOCKWAITlockstruct(s),heapsize1136,rowlock(s)MySQLthreadid9,OSthreadhandle140086065690368,queryid282localhostrootSendingdataselectfromtwhereidforupdateTRXHASBEENWAITINGSECFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid23pagenonbits80indexGENCLUSTINDEXoftablezlm。ttrxid1314lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno5PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000204;asc;;:len6;hex000000000515;asc;;:len;hexb0000001240110;asc;;:len;hex80000004;asc;;:len;hex80000190;asc;; TABLELOCKtablezlm。ttrxid1314lockmodeIXRECORDLOCKSspaceid23pagenonbits80indexGENCLUSTINDEXoftablezlm。ttrxid1314lockmodeXlocksrecbutnotgapRECORDLOCKSspaceid23pagenonbits80indexGENCLUSTINDEXoftablezlm。ttrxid1314lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno5PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000204;asc;;:len6;hex000000000515;asc;;:len;hexb0000001240110;asc;;:len;hex80000004;asc;;:len;hex80000190;asc;; innodb锁等待超时后再观察一次,线程9的事务1314的事务仍然没有结束,对t表持有IX锁,并且仍然在等待id的行锁释放TRANSACTION1314,ACTIVE453seclockstruct(s),heapsize1136,rowlock(s)MySQLthreadid9,OSthreadhandle140086065690368,queryid282localhostrootTABLELOCKtablezlm。ttrxid1314lockmodeIXRECORDLOCKSspaceid23pagenonbits80indexGENCLUSTINDEXoftablezlm。ttrxid1314lockmodeXlocksrecbutnotgap 确认Session的隔离级别和线程IDVariablenameValuetxisolationREADCOMMITTEDrowinset(0。01sec) rootlocalhost:mysqld。sock〔(none)〕5。7。30logselectconnectionid;connectionid10rowinset(0。00sec) Session执行Update语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;updatetsetsalsalwhereid;QueryOK,0rowsaffected(0。00sec) QueryOK,rowaffected(0。00sec) 在Session1事务仍然未结束的情况下,Session3的事务未被阻塞,可以正常执行。查看个语句的执行计划rootlocalhost:mysqld。sock〔zlm〕5。7。30logexplainselectfromtwhereidandid6forupdate;idselecttypetablepartitionstypepossiblekeyskeykeylenrefrowsfilteredExtraSIMPLEtALL1011。11Usingwhererowinset,warning(0。00sec) rootlocalhost:mysqld。sock〔zlm〕5。7。30logexplainselectfromtwhereidforupdate;idselecttypetablepartitionstypepossiblekeyskeykeylenrefrowsfilteredExtraSIMPLEtALL1010。00Usingwhererowinset,warning(0。00sec) rootlocalhost:mysqld。sock〔zlm〕5。7。30logexplainupdatetsetsalsalwhereid;idselecttypetablepartitionstypepossiblekeyskeykeylenrefrowsfilteredExtraUPDATEtALL10100。00Usingwhererowinset(0。00sec) 意料之中,由于t表没有索引,执行计划必然是走全表扫描,也就是每条被读取到的记录,都会上行锁。那为何Session1只锁了id4,id5的这两条,并没有锁全表呢?而同样是请求id7的记录,为何Session2无法获取锁资源,Session3却能成功执行?也许大家从上面的锁分析可以很快得到结论,由于Session1只占用了id4、id5的行锁,那么Session3去请求id7的自然不会有冲突(似乎挺有道理)那么Session2对id7的请求,为何会被锁定呢? 带着这些疑问,我们继续看第2个案例。 案例2 RC隔离级别,这次Session1执行的Select语句不带where条件Session1执行当前读的Select语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;selectfromtforupdate;QueryOK,0rowsaffected(0。00sec) idsal11002200330044005500660077008800990010100010rowsinset(0。00sec) 查看加锁详情线程8的1317事务获得了1个IX表锁和10个X记录锁,即:把表中的10条记录都锁定了t表上没有索引,MySQL默认会创建GENCLUSTINDEX的聚簇索引,而语句没有加where条件,只能走全表扫描,每条被读取的记录,都要在聚簇索引上加上记录锁(全表记录锁,相当于一个表锁了)TRANSACTION1317,ACTIVE5seclockstruct(s),heapsize1136,10rowlock(s)MySQLthreadid8,OSthreadhandle140086065960704,queryid312localhostrootTABLELOCKtablezlm。ttrxid1317lockmodeIXRECORDLOCKSspaceid23pageno3nbits80indexGENCLUSTINDEXoftablezlm。ttrxid1317lockmodeXlocksrecbutnotgapRecordlock,heapno2PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000201;asc;;:len6;hex00000000050e;asc;;:len7;hexab0000011f0110;asc;;:len4;hex80000001;asc;;:len4;hex80000064;ascd;; Recordlock,heapno3PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000202;asc;;:len6;hex00000000050f;asc;;:len7;hexac000001200110;asc;;:len4;hex80000002;asc;;:len4;hex800000c8;asc;; Recordlock,heapno4PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000203;asc;;:len6;hex000000000514;asc;;:len7;hexaf000001230110;asc;;:len4;hex80000003;asc;;:len4;hex8000012c;asc,;; Recordlock,heapno5PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000204;asc;;:len6;hex000000000515;asc;;:len7;hexb0000001240110;asc;;:len4;hex80000004;asc;;:len4;hex80000190;asc;; Recordlock,heapno6PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000205;asc;;:len6;hex000000000516;asc;;:len7;hexb1000001250110;asc;;:len4;hex80000005;asc;;:len4;hex800001f4;asc;; Recordlock,heapno7PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000206;asc;;:len6;hex000000000517;asc;;:len7;hexb2000001260110;asc;;:len4;hex80000006;asc;;:len4;hex80000258;ascX;; Recordlock,heapno8PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000207;asc;;:len6;hex000000000518;asc;;:len7;hexb3000001270110;asc;;:len4;hex80000007;asc;;:len4;hex800002bc;asc;; Recordlock,heapno9PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000208;asc;;:len6;hex000000000519;asc;;:len7;hexb4000001280110;asc(;;:len4;hex80000008;asc;;:len4;hex80000320;asc;; Recordlock,heapno10PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000209;asc;;:len6;hex00000000051a;asc;;:len7;hexb5000001290110;asc);;:len4;hex80000009;asc;;:len4;hex80000384;asc;; Recordlock,heapno11PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex00000000020a;asc;;:len6;hex00000000051b;asc;;:len7;hexb60000012a0110;asc;;:len4;hex8000000a;asc;;:len4;hex800003e8;asc;; Session2执行当前读的Select语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;selectfromtwhereid7forupdate;QueryOK,0rowsaffected(0。00sec) ERROR1205(HY000):Unknownerror1205 与之前案例1相同,也是锁等待超时退出。查看加锁详情这次线程9的事务1318从第1条记录就开始加锁了TRANSACTION1318,ACTIVE4secstartingindexreadmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize1136,1rowlock(s)MySQLthreadid9,OSthreadhandle140086065690368,queryid315localhostrootSendingdataselectfromtwhereid7forupdateTRXHASBEENWAITING4SECFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid23pageno3nbits80indexGENCLUSTINDEXoftablezlm。ttrxid1318lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno2PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000201;asc;;:len6;hex00000000050e;asc;;:len7;hexab0000011f0110;asc;;:len4;hex80000001;asc;;:len4;hex80000064;ascd;; TABLELOCKtablezlm。ttrxid1318lockmodeIXRECORDLOCKSspaceid23pageno3nbits80indexGENCLUSTINDEXoftablezlm。ttrxid1318lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno2PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000201;asc;;:len6;hex00000000050e;asc;;:len7;hexab0000011f0110;asc;;:len4;hex80000001;asc;;:len4;hex80000064;ascd;; Session3执行Update语句rootlocalhost:mysqld。sock〔zlm〕5。7。30logbegin;updatetsetsalsal1whereid7;QueryOK,0rowsaffected(0。00sec) ERROR1205(HY000):Unknownerror1205 与案例1不同的是,这次Update语句也遭遇锁等待超时退出了。查看加锁详情这次Session3请求id7的记录锁,该锁被Session1持有未释放,导致了Session3锁等待超时TRANSACTION1319,ACTIVE14secfetchingrowsmysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heapsize1136,8rowlock(s)MySQLthreadid10,OSthreadhandle140086066231040,queryid322localhostrootupdatingupdatetsetsalsal1whereid7TRXHASBEENWAITING14SECFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid23pageno3nbits80indexGENCLUSTINDEXoftablezlm。ttrxid1319lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno8PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000207;asc;;:len6;hex000000000518;asc;;:len7;hexb3000001270110;asc;;:len4;hex80000007;asc;;:len4;hex800002bc;asc;; TABLELOCKtablezlm。ttrxid1319lockmodeIXRECORDLOCKSspaceid23pageno3nbits80indexGENCLUSTINDEXoftablezlm。ttrxid1319lockmodeXlocksrecbutnotgapwaitingRecordlock,heapno8PHYSICALRECORD:nfields5;compactformat;infobits00:len6;hex000000000207;asc;;:len6;hex000000000518;asc;;:len7;hexb3000001270110;asc;;:len4;hex80000007;asc;;:len4;hex800002bc;asc;; 案例分析 由于t表上不存在索引,3个会话执行的语句都是全表扫描,在RC事务隔离级别下,这些语句都是需要发起当前读的操作(读取t表上最新的已提交事务版本),需要对读取到的全部记录加上记录锁(即行锁、也可称为InnoDB锁,大多数情况下,RC隔离级别没有Gap锁,因此基本不太会出现NextKey锁,对高并发场景比较友好)。 案例1 Session1:开始需要对每条记录加锁,由于不需要维护可重复读,也不需要锁Gap,当返回MySQLServer层通过where条件过滤后,最终只对id4、id5的记录加了锁。 Session2:从id1开始读取记录并加锁,当读取到id4的记录时,由于Session1先对id4的记录上了锁,就无法再对其进行加锁操作,我们看到它一直在等待id4的X锁,直到锁等待超时报错,为何是id4,而不是id5?因为是按聚簇索引一条条读取记录的,所以锁也需要一条条加,当上一条记录的锁资源没获取到,就不会对下一条记录加锁。 Session3:同样地,最开始也需要对读取到的记录一条条加锁,由于id7的记录与id4、id5上的行锁并不冲突,此处可以利用半一致性读对Update的优化特性,提前将id7上的行锁释放掉了,因此Update不会被阻塞,事务得以正常执行。 案例2 Session1:Select语句没有用where条件,通过全表扫描访问到的所有记录都无法通过MySQLServer层过滤,因此将t表的全部记录都上了X锁。 Session2:由于Session1已经将全部记录都上了X锁,Session2当前读的Select操作由于无法获取任何记录的X锁,就被阻塞了。 Session3:同样地,Session1持有的全记录X锁,使Session3的where条件落到了匹配的区间内,表示Session1对id7的行确实需要更新,必须上锁,因此Session3的Update被阻塞。 总结 在RC事务隔离级别下,Update语句可以利用到半一致性读的特性,会多进行一次判断,当where条件匹配到的记录与当前持有锁的事务中的记录不冲突时,就会提前释放InnoDB锁,虽然这样做违背了二阶段加锁协议,但却可以减少锁冲突,提高事务并发能力,是一种很好的优化行为。 参考链接 https:dev。mysql。comdocrefman5。7eninnodbconsistentread。html https:dev。mysql。comdocrefman5。7eninnodblocksset。html https:my。oschina。netJKOPERAblog1929335 社区近期动态