并发场景最近做了一些分布式事务的项目,对事务的隔离性有了更深的理解。后面会写一篇文章讲分布式事务。今天我们来回顾一下单机事务的隔离是如何实现的?“隔离的本质是控制并发”,如果SQL语句是串行执行的。那么数据库的四大特性之间就没有隔离的概念,也就不会出现脏读、不可重复读、幻读等问题。”并发操作只有以下四种数据库写与写,读与读,读与写,写与读”写写事务A更新一条记录时,事务B是否可以同时更新同一条记录?答案肯定不是,否则会造成“脏写”问题,那么如何避免脏写呢?答案是“加锁”read-readMySQL的读操作默认是不加锁的,所以可以并行读写和写读。MySQL发展了隔离的概念。”你根据自己的业务场景选择隔离级别。隔离级别脏读不可重复读幻读未提交读(uncommittedread)√√√读已提交(committedread)×√√可重复读(repeatableread)××√serializable(可序列化)×××》所以你看,MySQL通过锁和隔离级别来控制MySQL的并发性MySQL中的行级锁InnoDB存储引擎中的行级锁有两种:当一个事务需要读取一条记录时,需要获取S锁”独占锁”(ExclusiveLock,简称X锁)先改变记录。当事务需要修改一条记录时,需要先获得该记录的X锁。获得一条记录的S锁后,事务T2也需要访问这条记录,如果事务T2想再次获取这条记录的S锁,就可以成功,这种情况称为锁兼容,如果事务T2想再次获取这条记录的X锁,那么这个操作就会被阻塞直到事务T1s提交S锁。事务T1获取的一条记录的X锁释放后,无论事务T2接下来要获取该记录的S锁还是X锁,都会被阻塞,直到事务1提交。这种情况称为锁不兼容。"多个事务可以同时读取记录,即共享锁不互斥,但共享锁会阻塞排他锁。排他锁互斥》S锁与X锁的兼容关系如下兼容X锁S锁X锁互斥互斥S锁互斥兼容性》update,delete,insert会自动给涉及到的数据加排他锁,select语句默认不会加任何锁。”那么读操作在什么情况下会被加锁呢?select..lockinsharemode,给读记录加S锁select...forupdate,forread加X锁读取事务中的记录,读取记录加S锁事务隔离级别在SERIALIZABLE下,读取记录加S锁"InnoDB有以下三种锁"RecordLock:ForasinglerecordGapLock:间隙锁,锁住记录前面的空隙,不允许插入记录Next-keyLock:同时锁住数据和数据前面的空隙,即既不锁数据也不锁前面的空隙数据我允许插入记录写个demo演示CREATETABLE`girl`(`id`int(11)NOTNULL,`name`varchar(255),`age`int(11),PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;insertintogirlvalues(1,'西施',20),(5,'王昭君',23),(8,'貂蝉',25),(10,'杨玉环',26),(12,'陈媛媛',20);RecordLock「锁定单条记录」比如给id值为8的数据加一个RecordLock,原理图如下。RecordLock也有S锁和X锁,兼容性同前所述。SQL执行加什么样的锁受很多条件限制,比如事务的隔离级别,执行时使用的索引(如聚集索引,非聚集索引等),这里就不分析了很详细,但举几个简单的例子。--READUNCOMMITTED/READCOMMITTED/REPEATABLEREAD使用主键进行等价查询--为id=8的记录添加S型RecordLockselect*fromgirlwhereid=8lockinsharemode;RecordplusX-typeRecordLockselect*fromgirlwhereid=8forupdate;GapLock《锁住记录前面的空隙,不允许插入记录》《MySQL通过MVCC解决幻读问题,在可重复读隔离级别下加锁》Current阅读:添加锁快照阅读:MVCC,但如何锁定它?因为在第一次读操作的时候这些幻象记录是不存在的,所以我们没有办法加RecordLock。这时候可以通过加GapLock来解决,也就是加Lock。例如,如果一个事务对id=8的记录加了间隙锁,则意味着其他事务不允许在id=8的记录前面的间隙插入新的记录,即具有区间(5,8)中的id值是不允许的。允许立即插入。直到有间隙锁的事务提交,id值在区间(5,8)的记录才能被提交。我们看下面的SQL加锁过程——REPEATABLEREAD使用主键进行等价查询——但是主键的值不存在——在id=8的聚簇索引记录中添加GapLockSELECT*FROMgirlWHEREid=7LOCKINSHAREMODE;由于不存在id=7的记录,为了防止幻读(避免在同一个事务下执行同一条语句得到的结果集id=7的记录),所以在当前事务提交之前,我们需要以防止其他事务插入id=7记录。这时候只要在id=8的记录上加一个GapLock,即不允许其他事务插入id值在区间(5,8)内的新记录”我问你个问题,GapLock只能锁住记录前面的空隙,那怎么锁住最后一条记录后的空隙呢?”其实mysql的数据是存放在页面中的。每页有2条伪记录Infimum记录,表示该页最小的记录。Upremum记录,表示页面上最大的记录。为了防止其他事务插入id值在(12,+∞)区间的记录,我们可以给id=12记录所在页面的Supreme记录加一个间隙锁。此时可以阻止其他事务插入id值在(12,+∞)范围内的新记录。Next-keyLock“对数据和前面的Gap都加锁,即数据和数据前面的gap都不允许插入记录”。所以可以理解为Next-keyLock=RecordLock+GapLock--REPEATABLEREAD使用主键进行范围查询-在id=8的聚簇索引记录中添加STypeRecordLock--添加S-typeNext-keyLock(包括Supreme伪记录)到所有id>8的聚簇索引记录SELECT*FROMgirlWHEREid>=8LOCKINSHAREMODE;解决幻读问题,需要禁止其他事务插入id>=8的记录,所以对id=8的聚簇索引记录加S-typeRecordLock,加S-typeNext-keyLock(包括Supreme伪记录)到所有id>8的聚簇索引记录。X锁”在对一张表执行select、insert、update、delete语句时,innodb存储引擎不会给这张表加表级的S锁或X锁。当对表执行ALTERTABLE、DROPTABLE等DDL语句时,会在表上加一个X锁,这样其他事务在表上执行SELECTINSERTUPDATEDELETE等语句时就会被阻塞。系统变量autocommit=0,当innodb_table_locks=1时,手动获取InnoDB存储引擎提供的表t的S锁或X锁,可以这样写给表t加上表级的S锁locktablestread加表级xlocklocktablestwrite》如果一个事务给一个表加了S锁,其他事务可以继续获取该表的S锁,其他事务可以继续获取表中某些记录的S锁,其他事务不能继续获取获取表的X锁,其他事务无法继续获取表中某些记录的X锁“如果一个事务给表加了X锁,则”其他事务无法继续获取表的S锁其他事务无法继续获取表中某些记录的S锁事务不能继续获取表的X锁,其他事务不能继续获取表中某些记录的X锁。修改在线表,因为会阻塞大量事务。”修改在线表的方法不会重复隔离级别ReadUncommitted:每次读取最新的记录,不做特殊处理。序列化:事务是串行执行的,没有并发,所以我们重点关注“ReadCommitted”和“Repeatableread”隔离实现!”这两个隔离级别是通过MVCC(多版本并发控制)实现的,本质是MySQL通过undolog存储多个版本的历史数据,读取某个历史版本的数据,这样就可以实现读写并行,无需锁,提高数据库性能。”“那么undolog是如何存储修改前的记录的呢?”"对于使用InnoDB存储引擎的表,聚簇索引记录都包含以下两个必要的隐藏列"trx_id":每次事务改变某条聚簇索引记录时,该事务的事务id会赋值给隐藏列"roll_pointer"oftrx_id:每次改变一条聚簇索引记录时,当索引记录发生改变时,会将旧版本写入undolog。这个隐藏列相当于一个指针,通过它可以获取记录修改前的信息如果一条记录的名字由貂蝉依次改为王昭君、西施,就会有如下记录。多条记录组成一个版本链”用于判断版本链中的哪个版本对版本可见当前事务,MySQL设计了ReadView的概念”。4个重要内容如下“m_ids”:生成ReadView时,当前系统中的活跃事务id列表“min_trx_id”:生成ReadView时,当前系统中最小的活跃事务id,即m_ids中的最小值"max_trx_id":当生成一个ReadView时,系统应该将事务id值赋给下一个事务"creator_trx_id":生成该ReadView的事务的事务id当表中的记录发生变化时,insert,delete,update这些statements被执行时,会为该交易分配一个唯一的transactionid,否则一个交易的transactionid值默认为0。max_trx_id不是m_ids中的最大值,transactionid是递增分配的。比如有3个事务id分别为1、2、3的事务,然后提交事务id为3的事务。当一个新的事务生成ReadView时,m_ids的值包括1和2,min_trx_id的值为1,max_trx_id的值为4。执行过程如下:如果接入版本的trx_id=creator_id,则表示当前事务正在访问自己修改的记录,所以这个版本可以被当前事务访问。如果被访问版本的trx_id为被访问版本trx_id>=max_trx_id,说明生成该版本的事务是在当前事务生成ReadView后打开的,该版本不能被当前事务访问。访问版本的trx_id是否在m_ids列表中4.1是的,这个版本在ReadView创建的时候还处于活动状态,无法访问该版本。沿着版本链寻找下一个版本的数据,继续执行上面的步骤判断可见性。如果最后一个版本不可见,则意味着该记录对当前事务完全不可见。4.2不行,在创建ReadView的时候,生成这个版本的事务已经被创建提交,版本可以访问“好了,我们知道了获取版本可见性的规则,那么如何实现读提交和可重复读呢?”其实很简单,就是产生ReadView的时机不一样。举个例子首先创建下表CREATETABLE`girl`(`id`int(11)NOTNULL,`name`varchar(255),`age`int(11),PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;ReadCommitted"ReadCommitted(ReadCommitted),每次读取数据前都会生成一个ReadView。"下面是3笔交易执行的过程,一行代表一个时间点。系统中“首先分析select在这个时间点5的执行过程”。两个id为100和200的事务正在执行一条select语句生成一个ReadView,mids=[100,200],min_trx_id=100,max_trx_id=201,creator_trx_id=0(select这个事务不执行变化操作,事务iddefaults是0)最新版本名称列为西施,该版本的trx_id值为100,在mids列表中,不满足可见性要求。根据roll_pointer,跳到下一个版本。下个版本的name栏是王昭君,这个版本的trx_id值为100,而且还在mids列表中,所以不符合要求。继续跳到下一个版本。下一个版本的名称列为貂蝉。该版本的trx_id值为10,小于min_trx_id,所以最后返回的name值为貂蝉“我们来分析一下时间点8的select的执行过程”系统中有事务id为200的事务正在执行(事务id为100的事务已提交)执行select语句时会生成一个ReadView,mids=[200],min_trx_id=200,max_trx_id=201,creator_trx_id=0最新版本名称为杨玉环,trx_id这个版本的值为200,在mids列表中,不满足可见性要求,根据roll_pointer跳转到下一个版本,下一个版本的name列为西施,本版本的trx_id值为100,小于min_trx_id,所以最后返回的name值为西施。当事务id为200的事务提交时,查询的name列为杨玉环RepeatableRead"RepeatableRead(可重复读取),第一次读取数据时生成一个ReadView"图片可以重复读取因为ReadView只是在第一次读取数据的时候才会产生,所以每次读取的都是同一个版本,即name值始终是貂蝉。具体过程上面已经演示了两次,这里不再重复演示。我相信你会自己分析的。本文转载自微信公众号“Java知堂”,可通过以下二维码关注。转载本文请联系Java石塘公众号。
