本文对锁、事务和并发控制做了一个总结。看了网上很多文章,描述的很不准确。如对观点有异议,欢迎有理有据的拍砖!mysql服务器逻辑架构每个连接都会在mysql服务器上产生一个线程(内部线程池管理线程),比如一条select语句,mysql会先检查select的结果集是否缓存在querycache中,如果没有,则继续执行解析、优化、执行的过程;否则,将从缓存中获取结果集。mysql并发控制——共享锁、独占锁共享锁共享锁又叫读锁,读锁允许多个连接同时并发读取同一个资源,互不干扰;独占锁独占锁也叫写锁,一个写锁会阻塞其他的写锁或读锁,保证同一时间只有一个连接可以写数据,同时防止其他用户读写这个数据。锁定策略锁定的开销是比较昂贵的。加锁策略实际上是在保证线程安全和最大化性能之间的一种平衡策略。mysql锁策略:talbelock(表锁)表锁是mysql最基本的锁策略,也是开销最小的锁。它将锁定整个表;具体情况是:如果一个用户正在执行写操作,会得到一个独占的“写锁”,可能会锁住整个表,阻塞其他用户的读写操作;如果一个用户正在执行读操作,它会先获取一个共享锁“readlock”,这使得其他读锁可以并发读取该表,互不干扰。只要不输入写锁,就可以使用读锁并发读取同一个资源。通常出现在不使用索引的DDL语句\DML语句中,比如这个DML更新表setcolumnA="A"wherecolumnB="B"。如果columnB字段没有索引(或者不是复合索引前缀),所有的记录都会被锁住,也就是锁表。如果语句的执行可以对columnB字段执行索引,那么满足where的行就会被锁定(rowlock)。MySQL锁策略:行锁(rowlock)行锁可以最大程度的支持并发处理,当然也带来了最大的开销。顾名思义,行锁的粒度其实就是每一行数据。TransactionTransaction是一组原子SQL,或者说是一个独立的工作单元。事务意味着要么mysql引擎将执行所有这组sql语句,要么不执行它们(例如,如果其中一条语句失败)。例如Tim要转100元给Bill:1.查看Tim的账户余额是否大于100元;2、蒂姆账户减100元;3、账单账户增加100元;包执行,要么全部成功,要么都不执行,其中任何一个失败都会导致这三个操作都“不执行”——回滚。CREATEDATABASEIFNOTEXISEmployees;USEmployees;CREATETABLE`employees`.`account`(`id`BIGINT(11)NOTNULLAUTO_INCREMENT,`p_name`VARCHAR(4),`p_money`DECIMAL(10,2)NOTNULLDEFAULT0,PRIMARYKEY(`id`));插入`employees`.`account`(`id`,`p_name`,`p_money`)VALUES('1','tim','200');INSERTINTO`employees`.`account`(`id`,`p_name`,`p_money`)VALUES('2','bill','200');STARTTRANSACTION;SELECTp_moneyFROMaccountWHEREp_name="tim";--step1UPDATEaccountSETp_money=p_money-100WHEREp_name="tim";--step2UPDATEaccountSETp_money=p_money+100WHEREp_name="bill";--step3COMMIT;一个好的事务系统必须满足ACID特性:事务ACIDA:atomiciyatomicity一个事务必须保证其中的操作要么全部执行要么全部回滚,不可能只执行其中的一部分这种情况出现。C:ConsistencyConsistency数据必须保证从一种一致状态转换到另一种一致状态。比如上一笔交易执行第二步时系统崩溃,数据不会显示bill的账户亏了100元,而tim的账户没有变化。要么保持原样(全部回滚),要么bill少100,tim多100。只有这两种状态一致:Isolation隔离通常保证在事务未完成时其他Session看不到一旦这个事务D:持久性的执行结果被提交,数据就会被保存。即使提交后系统崩溃,数据也不会丢失。隔离级别查看系统隔离级别:选择@@global.tx_isolation;查看当前会话隔离级别select@@tx_isolation;设置当前会话隔离级别SETsessionTRANSACTIONISOLATIONLEVELserializable;设置全局系统隔离级别SETGLOBALTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;READUNCOMMITTED(未提交读,脏读)事务中的修改对其他会话可见,即使它们没有提交。可以读取未提交的数据——脏读。脏读会导致很多问题,这种隔离级别一般是不适用的。实例:------------------------读未提交实例------------------------------设置全局系统隔离级别SETGLOBALTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;--SessionASTARTTRANSACTION;SELECT*FROMUSER;UPDATEUSERSETNAME="READUNCOMMITTED";--commit;--SessionBSELECT*FROMUSER;//SessionBConsole能不能看到SessionA未提交的事务处理,在另一个Session中也看到了。这就是所谓的脏读idname2READUNCOMMITTED34READUNCOMMITTEDREADCOMMITTED(提交读或不可重复读,幻读)。一般数据库默认使用这个隔离级别(mysql没有),这个隔离级别保证如果一个事务没有完全成功(commit被执行),事务中的操作对其他会话是不可见的。--------------------------读取提交的实例----------------------------------设置全局系统隔离级别SETGLOBALTRANSACTIONISOLATIONLEVELREADCOMMITTED;--SessionASTARTTRANSACTION;SELECT*FROMUSER;UPDATEUSERSETNAME="READCOMMITTED";--COMMIT;--SessionBSELECT*FROMUSER;//ConsoleOUTPUT:idname2READUNCOMMITTED34READUNCOMMITTED----------------------------------------------------SessionA执行commit时,SessionB得到如下结果:idname2READCOMMITTED34READCOMMITTED还验证了事务完成commit操作之前readcommitted级别修改的数据对其他Session不可见,只有在完成commit操作后才会对其他Session可见提交被执行。我们可以看到SessionB的两次查询得到了不同的数据。readcommitted隔离级别解决了脏读问题,但是对于其他Session会产生两次不一致的读取结果(因为另一个Session执行了一个事务,一致性发生了变化)。REPEATABLEREAD(可重复读)在一个事务中多次执行统一读SQL,返回相同的结果。这个隔离级别解决了脏读和幻读的问题。这里指的是InnoDB的rr级别。在InnoDB中,next-key锁用于锁定“当前读”,锁定可能产生幻读的行和插入位置,防止新数据插入产生幻读。下面详细分析。详细请参考mysql手册https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.htmlSERIALIZABLE(可序列化)最强隔离级别,通过在事务中给每一个读行锁,写锁和写锁,保证不会出现幻读,但是会造成很多超时和锁争用。多版本并发控制——MVCCMVCC(multiple-version-concurrency-control)是行级锁的一种变体,避免了正常读取情况下的加锁操作,因此开销更低。尽管实现各不相同,但它们通常实现非阻塞读取并且只锁定必要的行以进行写入。一致性读(即读快照)select*fromtable....;当前读(即读取实际持久化数据)特殊读操作,插入/更新/删除操作,属于当前读,处理的都是当前需要加锁的数据。从表中选择*在哪里?锁定共享模式;从表中选择*在哪里?更新;插入;mysql称为一致性读)。所谓一致性读或读取快照,就是读取当前事务开始前的数据快照,事务开始后的更新将不会被读取。详细信息在以下选择中描述。对于带FORUPDATE(独占锁)或LOCKINSHAREMODE(共享锁)的lockedreadSELECT、update、delete语句,需要考虑是否是唯一索引的等价查询。写lock-recordLock、gapLock、nextkeylock对于使用唯一索引等价的查询:比如wherecolumnA=”…”,如果使用columnA上的索引,那么会在满足where的记录上加一个行锁(对于update是排他锁,shared中的lock是共享锁,其他写操作加排他锁)。这里是行级锁,记录锁。对于范围查询(使用非唯一索引):例如(做范围查询):其中columnA在10到30之间,其他session中10之后的数据无法插入(nextkeylock),从而解决幻读问题。这里下一个键锁将包括所有涉及的行。nextkeylock=recordLock+gapLock不仅锁定了相关数据,还锁定了边界,从而彻底避免幻读。对于没有索引锁的表,通常出现在不使用索引的DDL语句\DML语句中,比如这个DMLupdatetablesetcolumnA="A"wherecolumnB="B"。如果columnB字段没有索引(或者不是复合索引前缀),将锁定所有记录,即锁定表。如果语句的执行可以对columnB字段执行索引,那么满足where的行就会被锁定(rowlock)。INNODB的MVCC通常是在每行数据后面保存两个隐藏列(实际上是三列,第三列用于事务回滚,这里省略),一个保存该行的创建版本号,一个保存更新的版本号行的(最后更新数据的版本号)。此版本号是每个事务的版本号,递增。这样可以保证innodb不需要对读操作加锁来保证数据被正确读取。在MVCCselect无锁运维版本号下,在mysql默认的RepeatableRead隔离级别下,我们看一下MVCC的操作:select(snapshotread,所谓readsnapshot就是读取snapshot之前的数据currenttransaction.):a.InnoDB只select查找版本号早于当前版本号的数据行,从而保证读取的数据要么在事务开始前committed(早于当前版本号),或者创建操作是在事务本身数据(等于当前版本号)中进行的。b.搜索行的更新版本号未定义或者大于当前版本号(为了保证事务可以读取旧数据),保证了当前事务启动后事务读取的是未更新的数据。注意:这里的select不能有forupdate,lockinshare语句。简而言之,只返回满足以下条件的行数据,达到快照读取的效果:(行创建版本号<=当前版本号&&(行更新版本号==null或行更新版本号>当前版本号))InsertInnoDB对于本次事务中新插入的行,保存当前事务版本号的行作为该行的行创建版本号。删除InnoDB将每个删除行的当前事务版本号保存为该行的删除标记。update会有两条数据,保存当前版本号作为更新数据的新版本号,保存当前版本号作为旧数据行的更新版本号。当前版本号—写入—>新数据行创建版本号&&当前版本号—写入—>旧数据更新版本号();Dirtyreadvsphantomreadvsnon-repeatableread脏读:事务未提交的中间状态的更新数据被其他会话读取。当一个事务正在访问数据和修改数据,而这个修改还没有提交到数据库(commit还没有执行),这时候另一个session也访问这个数据,因为这个数据还没有提交,那么在另外一个session读取的数据是脏数据,基于脏数据的操作也有可能是错误的。不可重复读:简单的说,一个事务中读到的数据可能会发生变化。ReadCommitted也称为不可重复读。在同一个事务中,多次读取相同的数据返回不同的结果。换句话说,后续读取可以从另一个会话事务中读取已提交的更新。相反,“可重复读”在同一个事务中多次读取数据时,可以保证读取到的数据是相同的,即后续读取无法读取到另一个会话事务提交的更新数据。幻读:在sessionT1的事务中执行了一个query,然后sessionT2插入了一行新的记录,刚好满足T1使用的query的条件。然后T1使用相同的查询再次检索该表,但这次看到的是事务T2刚刚插入的新行。这个新行被称为“幻象”,因为它在T1看来就像是突然出现的一样。innoDB的RR级别不能完全避免幻读,下面会详细分析。--------------------------------前期准备--------------------------------------先决条件:--创建表mysql>创建表`t_bitfly`(`id`bigint(20)NOTNULLDEFAULT'0',`value`varchar(32)DEFAULTNULL,PRIMARYKEY(`id`))--确保当前隔离级别为默认RR级别mysql>select@@global.tx_isolation,@@tx_isolation;+-----------------------+----------------+|@@global.tx_isolation|@@tx_isolation|+---------------------+----------------+|REPEATABLE-READ|REPEATABLE-阅读|+-----------------------+----------------+1rowinset(0.00sec)---------------------开始---------------------------------------sessionA|sessionB||mysql>STARTTRANSACTION;|mysql>STARTTRANSACTION;QueryOK,0rowsaffected(0.00秒)|QueryOK,0rowsaffected(0.00sec)||mysql>SELECT*FROMtest.t_bitfly;|mysql>SELECT*FROMtest.t_bitfly;Emptyset(0.00sec)|Emptyset(0.00sec)||mysql>INSERTINTOt_bitflyVALUES(1,'test');|QueryOK,1rowaffected(0.00sec)||mysql>SELECT*FROMtest.t_bitfly;|清空yset(0.00sec)|||mysql>;commit;|QueryOK,0rowsaffected(0.01sec)mysql>SELECT*FROMtest.t_bitfly;|Emptyset(0.00sec)|--可见虽然两次执行结果返回的数据是一致的,|--但是不代表没有幻读然后看:||mysql>INSERTINTOt_bitflyVALUES(1,'test');|ERROR1062(23000):|Duplicateentry'1'forkey'PRIMARY'||--明明是的表空,为什么主键重复?——幻读出现!!!|如何保证rr级别永远不会产生幻读?在用于实现此目的的select...where语句中添加forupdate(独占锁)或lockinsharemode(共享锁)语句。其实就是锁住可能造成幻读的数据,防止数据被写入。其实是因为数据的写操作(插入、更新)需要先获得写锁,因为可能产生幻读的部分已经获得了某种锁,所以获得写锁的前提是在另一个session是当前会话释放locking语句创建的所有锁。mysql死锁问题死锁是一个循环等待链,我等你的资源,你却等我的资源,大家都互相等待,没有人释放自己拥有的资源,造成无线等待。比如://sessionAstartTransAction;updateAccountSetp_money=p_money-100wherep_name=“tim”;updateAccountsetp_money=p_money+100wherep_name=“bill”;bill“;///////////////////////////////-";COMMIT;当线程A执行到第一条语句UPDATEaccountSETp_money=p_money-100WHEREp_name=”tim”;锁定p_name=”tim”的行数据;并尝试获取p_name=”bill”的数据;,此时线程B也执行了第一条语句:UPDATEaccountSETp_money=p_money+100WHEREp_name=”bill”;锁定p_name=”bill”的数据,同时尝试获取p_name=”tim”的数据;此时两个线程进入死锁,谁也拿不到自己想要的资源,进入无线等待直到超时!innodb_lock_wait_timeout等待锁超时回滚事务:直观的做法是当两个事务互相等待时,当一个等待时间超过设定的阈值时,回滚其中一个事务,另一个事务可以继续执行。这种方法简单有效。在innodb中,参数innodb_lock_wait_timeout用于设置超时时间。Wait-forgraph算法主动进行死锁检测:innodb也提供wait-forgraph算法主动进行死锁检测。每当锁请求不能立即满足需求而进入等待时,就会触发等待图算法。如何尽可能避免死锁1)按固定顺序访问表和行。例如,对于更新数据的两个事务,事务A更新数据的顺序是1、2;事务B更新数据的顺序是2、1,这样比较容易造成死锁。2)大事化小。大事务更容易出现死锁。如果业务允许,大交易应该拆分成小交易。3)在同一个事务中,尽量一次性锁定所有需要的资源,减少死锁的概率。4)降低隔离级别。如果业务允许,降低隔离级别也是更好的选择。比如将隔离级别从RR调整为RC,可以避免很多由间隙锁引起的死锁。5)为表添加合理的索引。可以看出,如果不使用索引,就会对表的每一行加锁,死锁的概率会大大增加。显式锁和隐式锁隐式锁:我们上面提到的锁都是隐式锁,不需要额外的语句锁。显示锁:SELECT...LOCKINSHAREMODE(加共享锁);SELECT...FORUPDATE(加上独占锁);上面已经提到了细节。可以通过下面的sql查看等待锁的情况select*frominformation_schema.innodb_trxwheretrx_state="lockwait";或showengineinnodbstatus;mysql中的transactionshowvariableslike"autocommit";setautocommit=0;//0表示关闭AutoCommitsetautocommit=1;//1表示AutoCommit开启自动提交(AutoCommit,mysql默认)mysql默认采用AutoCommit模式,即每条sql都是一个交易,不需要显示来执行交易。如果关闭autoCommit,则默认每条sql都会开启一个事务,只有在显式执行“commit”后才会提交该事务。
