关系型数据库的事务机制因其有原子性,一致性等优秀特性深受开发者喜爱,类似的思想已经被应用到很多其他系统上,例如文件系统等。本文主要介绍InnoDB事务子系统,主要包括,事务的启动,事务的提交,事务的回滚,多版本控制,垃圾清理,回滚段以及相应的参数和监控方法。代码主要基于RDS 5.6,部分特性已经开源到AliSQL。事务系统是InnoDB最核心的中控系统,涉及的代码比较多,主要集中在trx目录,read目录以及row目录中的一部分,包括头文件和IC文件,一共有两万两千多行代码。
基础知识事务ACID: 原子性,指的是整个事务要么全部成功,要么全部失败,对InnoDB来说,只要client收到server发送过来的commit成功报文,那么这个事务一定是成功的。如果收到的是rollback的成功报文,那么整个事务的所有操作一定都要被回滚掉,就好像什么都没执行过一样。另外,如果连接中途断开或者server crash事务也要保证会滚掉。InnoDB通过undolog保证rollback的时候能找到之前的数据。一致性,指的是在任何时刻,包括数据库正常提供服务的时候,数据库从异常中恢复过来的时候,数据都是一致的,保证不会读到中间状态的数据。在InnoDB中,主要通过crash recovery和double write buffer的机制保证数据的一致性。隔离性,指的是多个事务可以同时对数据进行修改,但是相互不影响。InnoDB中,依据不同的业务场景,有四种隔离级别可以选择。默认是RR隔离级别,因为相比于RC,InnoDB的RR性能更加好。持久性,值的是事务commit的数据在任何情况下都不能丢。在内部实现中,InnoDB通过redolog保证已经commit的数据一定不会丢失。
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
垃圾清理: 对于用户删除的数据,InnoDB并不是立刻删除,而是标记一下,后台线程批量的真正删除。类似的还有InnoDB的二级索引的更新操作,不是直接对索引进行更新,而是标记一下,然后产生一条新的。这个线程就是后台的Purge线程。此外,过期的undolog也需要回收,这里说的过期,指的是undo不需要被用来构建之前的版本,也不需要用来回滚事务。
回滚段: 可以理解为数据页的修改链,链表最前面的是最老的一次修改,最后面的最新的一次修改,从链表尾部逆向操作可以恢复到数据最老的版本。在InnoDB中,与之相关的还有undo tablespace, undo segment, undo slot, undo log这几个概念。undo log是最小的粒度,所在的数据页称为undo page,然后若干个undo page构成一个undo slot。一个事务最多可以有两个undo slot,一个是insert undo slot, 用来存储这个事务的insert undo,里面主要记录了主键的信息,方便在回滚的时候快速找到这一行。另外一个是update undo slot,用来存储这个事务delete/update产生的undo,里面详细记录了被修改之前每一列的信息,便于在读请求需要的时候构造。1024个undo slot构成了一个undo segment。然后若干个undo segemnt构成了undo tablespace。
历史链表: insert undo可以在事务提交/回滚后直接删除,没有事务会要求查询新插入数据的历史版本,但是update undo则不可以,因为其他读请求可能需要使用update undo构建之前的历史版本。因此,在事务提交的时候,会把update undo加入到一个全局链表(history list)中,链表按照事务提交的顺序排序,保证最先提交的事务的update undo在前面,这样Purge线程就可以从最老的事务开始做清理。这个链表如果太长说明有很多记录没被彻底删除,也有很多undolog没有被清理,这个时候就需要去看一下是否有个长事务没提交导致Purge线程无法工作。在InnoDB具体实现上,history list其实只是undo segment维度的,全局的history list采用最小堆来实现,最小堆的元素是某个undo segment中最小事务no对应的undopage。当这个undolog被Purge清理后,通过history list找到次小的,然后替换掉最小堆元素中的值,来保证下次Purge的顺序的正确性。