今天主要分享一些Innodb事务系统相关的优化。以下是基于mysql5.7的。一、Innodb中的事务、视图和多版本1、事务Innodb中,每启动一个事务,都会为session分配一个事务对象。为了控制和协调所有全局事务,有一个全局对象trx_sys,对trx_sys相关成员的操作需要trx_sys->互斥锁。mysql数据库遵循两阶段锁协议,将事务分为两个阶段,加锁阶段和解锁阶段(所谓两阶段锁)。加锁阶段:在这个阶段可以进行加锁操作。在读取任何数据之前,申请并获得一个S锁(共享锁,其他事务可以继续加共享锁,但不能加排他锁),在执行写操作之前,申请并获得X锁(排他锁,其他事务)不能再获取任何锁)。如果加锁不成功,则事务进入等待状态,直到加锁成功后才继续进行。解锁阶段:当事务释放锁后,事务进入解锁阶段,此时只能进行解锁操作,不能再进行加锁操作。2、视图Innodb使用一个叫做ReadView(视图)的对象来决定事务的可见性(也就是ACID中的隔离)。根据可见性原则,一个新开启的事务不应该看到其他未提交的事务。Innodb在执行SELECT或显式启用STARTTRANSACTIONWITHCONSISTENTSNAPSHOT时创建视图对象(后者仅适用于REPEATABLE-READ隔离级别)。对于RR隔离级别,视图的生命周期在事务提交结束时结束。对于RC隔离级别,事务在每个查询开始时重新分配。通常一个视图包含创建视图的事务ID,以及创建视图时处于活动状态的事务ID数组。例如打开一个视图,当前事务的事务ID为5,事务列表上活跃的事务ID为{2,5,6,9,12},则{2,6,9,12}会存储在当前视图中(5为当前事务的ID,不记录在视图中),{2,6,9,12}对应的事务所做的修改对当前视图是不可见的transaction,以及交易ID小于2的对当前交易可见,大于12的交易ID对当前交易不可见。那么如何判断能见度呢?InnoDB表数据组织在主键聚集索引中。由于索引组织表结构,记录的ROWID是可变的(StructureModificationOperation,索引分页时的SMO),所以在二级索引中使用(索引键值,主键键值)的组合来唯一确定一条记录。无论是聚簇索引还是二级索引,每条记录都包含一个DELETEDBIT位,用于标识该记录是否为删除记录。另外,聚簇索引记录有两个系统列:DATA_TRX_ID、DATA_ROLL_PTR。DATA_TRX_ID表示生成当前记录项的交易ID;DATA_ROLL_PTR指向当前记录项的撤销信息。聚集索引行结构(多版本一致性读相关部分,DELETEDBIT省略):二级索引行结构:从聚集索引行结构和二级索引行结构可以看出聚集索引包含版本信息(事务号+回滚指针),二级索引不包含版本信息。对于聚簇索引,每修改一条记录,记录中保存当前事务ID,UNDO中保存旧版本记录;对于二级索引,二级索引页中存储了更新当前页面的最大事务ID,如果事务ID大于readview->up_limit_id(上例up_limit_id值为2),则需要返回聚集索引以确定记录的可见性;如果小于2,则始终可见,可以直接读取。3、多版本(MVCC)为了便于理解MVCC的实现原理,这里简单介绍一下undolog的工作过程。不考虑redolog而使用undolog的简化过程是:注意:为了保证数据的持久化要在事务提交之前持久化undolog,undolog的持久化必须在数据持久化之前,这样才能确保当系统崩溃时,undolog可以用来回滚事务。MVCC只工作在READCOMMITED和REPEATABLEREAD这两个隔离级别下。READUNCOMMITTED始终读取最新的数据行,而不是与当前事务版本匹配的数据行。SERIALIZABLE将锁定所有读取的行。(1)SELECTInnoDB会根据两个条件检查每一行记录:InnoDB只查找版本(DB_TRX_ID)早于当前事务版本(行的系统版本号<=事务的系统版本号)的数据行,这确保数据行要么在开始之前就已存在,要么被事务本身插入或修改)该行的删除版本号(DB_ROLL_PTR)要么未定义(未更新),要么大于当前事务版本号(在当前事务开始后更新)。这确保事务读取的行在事务开始之前不会被删除。(2)INSERTInnoDB保存每条新插入行的当前系统版本号作为行版本号(3)DELETEInnoDB保存每条删除行的当前系统版本号作为行删除标识(4)UPDATEInnoDB保存当前系统版本号insertinganewrow以系统版本号作为行版本号,将当前系统版本号保存到原行作为行删除标识。Innodb的多版本数据由UNDO维护。比如聚簇索引记录(1)=>(2)=>(3),如果从1更新到2,再更新到3,就会产生两条undo记录。2.Innodb事务系统优化在MySQL5.7版本中,对事务系统进行了深度优化,主要解决了以下问题。1、view对象的创建需要trx_sys->mutex锁保护。trx_sys->mutex是交易系统的核心全局锁对象,操作这个锁的时间不要太长。对于读到的视图对象,可以缓存起来重复使用。这避免了持有锁来分配视图内存。因此,在MySQL5.7中,实例启动时分配了1024个视图对象;同时维护两个链表,一个是已用视图链表,一个是自由视图链表;当需要分配新的视图时,总是从视图链表中空闲的开始分配,如果没有,则分配一个新的。2.在视图对象中保存全局事务ID时,需要扫描事务列表来确定事务视图的可见性,并在打开视图时复制当时活跃的事务ID。5.7中,事务系统维护了一个全局的事务ID数组,将每个活跃的读写事务的ID加入其中,事务提交时从中删除,这样打开视图时,只需要使用memcpy复制数组。无需遍历链表。在读写链表较长的场景下(高并发下),这种优化可以显着提升性能。3.用户需要明确启用只读交易,才能将其放入只读交易列表。mysql5.7从中彻底去掉了只读事务列表。相反,所有事务都以只读模式打开。例如,以下事务顺序:BEGIN;SELECT;//事务开始,不分配事务ID,不分配回滚段;UPDATE;//分配事务ID,插入全局事务数组和事务对象集合,分配回滚段;犯罪;对于像BEGIN这样的序列;选择;选择;COMMIT,整个事务周期既不分配事务ID也不分配回滚段。4.隐式锁转换为显式锁的开销Innodb对类似的INSERT操作使用隐式锁。隐式锁不是锁,只是一个名字。只有在需要时,它们才会转换为显式锁。风格锁。例如如下:Session1:BEING;INSERTINTOt1(pk,val)VALUES(1,2);//不创建锁对象Session2:UPDATEt1SETvalval=val+1WHEREpk=1;//创建两个锁对象,一个为session1的记录锁对象,另一种是为自己创建一个等待类型的记录锁对象,然后session2加入锁等待队列。在Session2中为Session1创建锁对象的过程就是所谓的隐式锁到显式锁的转换。当session2扫描到session1插入的记录,发现session1的事务仍然活跃时,就会进入转换逻辑。在5.6版本中,转换过程如下:holdlock_sys->mutex2holdtrx_sys->mutex;根据交易ID扫描读写交易列表,找到对应的交易对象;释放trx_sys->mutex;createanexplicitlock对象释放lock_sys->mutex。可以看出,在运行过程中,lock_sys->mutex是全程持有的。持有锁的原因是为了防止事务被提交。当读写事务列表很长时(比如高并发写),这个开销将是无法接受的。在5.7版本中,上述逻辑优化为:(1)holdtrx_sys->mutex根据交易ID找到对应的交易对象(直接搜索trx_sys->rw_trx_set,保存了trx_id和交易对象的映射关系,所以无需扫描读写事务链表)增加事务对象引用计数(++trx->n_ref)释放trx_sys->mutex(2)holdlock_sys->mutex;创建显式锁定对象;releaselock_sys->mutex;(3)递减事务对象引用计数在事务提交中,释放记录锁之前,会先判断引用记录数是否为0,如果不为0,说明其他事务正在为其转换显式锁。这时需要等到计数为0,进入释放事务记录锁阶段。总的来说,这个优化减少了隐式锁转换时持有LOCK_sys->mutex的时间,从而提高了性能。
