共计 5272 个字符,预计需要花费 14 分钟才能阅读完成。
本篇文章给大家分享的是有关 MySQL 中怎么实现跨行事务模型,丸趣 TV 小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着丸趣 TV 小编一起来看看吧。
MySQL 事务原子性保证
事务原子性要求事务中的一系列操作要么全部完成,要么不做任何操作,不能只做一半。原子性对于原子操作很容易实现,就像 HBase 中行级事务的原子性实现就比较简单。但对于多条语句组成的事务来说,如果事务执行过程中发生异常,需要保证原子性就只能回滚,回滚到事务开始前的状态,就像这个事务根本没有发生过一样。如何实现呢?
MySQL 实现回滚操作完全依赖于 undo log,多说一句,undo log 在 MySQL 除了用来实现原子性保证之外,还用来实现 MVCC,下文也会涉及到。使用 undo 实现原子性在操作任何数据之前,首先会将修改前的数据记录到 undo log 中,再进行实际修改。如果出现异常需要回滚,系统可以利用 undo 中的备份将数据恢复到事务开始之前的状态。下图是 MySQL 中表示事务的基本数据结构,其中与 undo 相关的字段为 insert_undo 和 update_undo,分别指向本次事务所产生的 undo log。
事务回滚根据 update_undo(或者 insert_undo)找到对应的 undo log,做逆向操作即可。对于已经标记删除的数据清理删除标记,对于更新数据直接回滚更新; 插入操作稍微复杂一些,不仅需要删除数据,还需要删除相关的聚集索引以及二级索引记录。
undo log 是 MySQL 内核中非常重要的一块内容,涉及知识比较多而且复杂,比如:
1. undo log 必须在数据修改之前持久化,undo log 持久化需不需要记录 redo 以防止宕机异常? 如果需要就又涉及宕机恢复 hellip;
2. 通过 undo log 如何实现 MVCC?
3. 那些 undo log 可以在什么场景下回收清理? 如何清理?
MySQL 事务一致性保证:强一致性事务保证
MySQL 事务隔离级别
Read Uncommitted(RU 技术解读:使用 X 锁实现写写并发)
Read Uncommitted 只实现了写写并发控制,并没有有效的读写并发控制,导致当前事务可能读到其他事务中还未提交的修改数据,这些数据准确性并不靠谱(有可能被回滚掉),因此在此基础上作出的一切假设就都不靠谱的。在现实场景中很少有业务会选择该隔离级别。
写写并发实现机制和 HBase 并无两样,都是使用两阶段锁协议对相应记录加行锁实现。不过 MySQL 中行锁机制比较复杂,根据行记录是否是主键索引、唯一索引、非唯一索引或者无索引等分为多种加锁情况。
1. 如果 id 列是主键索引,MySQL 只会为聚簇索引记录加锁。
2. 如果 id 列是唯一二级索引,MySQL 会为二级索引叶子节点以及聚簇索引记录加锁。
3. 如果 id 列是非唯一索引,MySQL 会为所有满足条件 (id = 15) 的二级索引叶子节点以及对应的聚簇索引记录加锁。
4. 如果 id 列是无索引的,SQL 会走聚簇索引全表扫描,并将扫描结果加载到 SQL Server 层进行过滤,因此 InnoDB 会为扫描过的所有记录先加上锁,如果 SQL Server 层过滤不符合条件,InnoDB 会释放该锁。因此 InnoDB 会为扫描到的所有记录都加锁,很恐怖吧!
接下来无论是 RC、RR,抑或是 Serialization,写写并发控制都使用上述机制,所以不再赘述。接下来会重点分析 RC 和 RR 隔离级别中的读写并发控制机制。
在详细介绍 RC 和 RR 之前,有必要在此先行介绍 MySQL 中 MVCC 机制,因为 RC 和 RR 都使用 MVCC 机制实现事务之间的读写并发。只不过两者在实现细节上有一些区别,具体区别接下来再聊。
MVCC in MySQL
MySQL 中 MVCC 机制相比 HBase 来说要复杂的多,涉及的数据结构也比较复杂。为了解释的比较清晰,以一个栗子为模版进行解释。比如当前有一行记录如下图所示:
前面四列是该行记录的实际列值,需要重点关注的是 DB_TRX_ID 和 DB_ROLL_PTR 两个隐藏列(对用户不可见)。其中 DB_TRX_ID 表示修改该行事务的事务 ID,而 DB_ROLL_PTR 表示指向该行回滚段的指针,该行记录上所有版本数据,在 undo 中都通过链表形式组织,该值实际指向 undo 中该行的历史记录链表。
现在假设有一个事务 trx2 修改了该行数据,该行记录就会变为下图形式,DB_TRX_ID 为最近修改该行事务的事务 ID(trx2),DB_ROLL_PTR 指向 undo 历史纪录链表:
了解了 MySQL 行记录之后,再来看看事务的基本结构,下图是 MySQL 的事务数据结构,上文我们提到过。事务在开启之后会创建一个数据结构存储事务相关信息、锁信息、undo log 以及非常重要的 read_view 信息。
read_view 保存了当前事务开启时整个 MySQL 中所有活跃事务列表,如下图所示,在当前事务开启的时候,系统中活跃的事务有 trx4、trx6、trx7 以及 trx10。另外,up_trx_id 表示当前事务启动时,当前事务链表中最小的事务 ID;low_trx_id 表示当前事务启动时,当前事务链表中最大的事务 ID。
read_view 是实现 MVCC 的一个关键点,它用来判断记录的哪个版本对当前事务可见。如果当前事务要读取某行记录,该行记录的版本号 (事务 ID) 为 trxid,那么:
1. 如果 trxid up_trx_id,说明该行记录所在的事务已经在当前事务创建之前就提交了,所以该行记录对当前事务可见。
2. 如果 trxid low_trx_id,说明该行事务所在的事务是在当前事务创建之后才开启,所以该行记录对当前事务不可见。
3. 如果 up_trx_id trxid low_trx_id, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态。从 up_trx_id 到 low_trx_id 进行遍历,如果 trxid 等于他们之中的某个事务 id 的话,那么不可见,否则可见。
以下面行记录为例,该行记录存在多个版本(trx2、trx5、trx7 以及 trx12),其中 trx12 是最新版本。看看该行记录中哪个版本对当前事务可见。
1. 该行记录的最新版本为 trx12,与当前事务 read_view 进行对比发现,trx12 大于当前活跃事务列表中的最大事务 trx10,表示 trx12 是在当前事务创建之后才开启的,因此不可见。
2. 再查看该行记录的第二个最新版本为 trx7,与当前事务 read_view 对比发现,trx7 介于当前活跃事务列表最小事务 ID 和最大事务 ID 之间,表明该行记录所在事务在当前事务创建的时候处于活动状态,在活跃列表中遍历发现 trx7 确实存在,说明该事务还没有提交,所以对当前事务不可见。
3. 继续查看该记录的第三个最新版本 trx5,也介于当前活跃事务列表最小事务 ID 和最大事务 ID 之间,表明该行记录所在事务在当前事务创建的时候处于活动状态,但遍历发现该版本并不在活跃事务列表中,说明 trx5 对应事务已经提交(注:事务提交时间与事务编号没有任何关联,有可能事务编号大的事务先提交,事务编号小的事务后提交),因此 trx5 版本行记录对当前事务可见,直接返回。
Read Committed(技术解读:写写并发使用 X 锁,读写并发使用 MVCC 避免脏读)
上文介绍了 MySQL 中 MVCC 技术实现机制,但要明白 RC 隔离级别下事务可见性,还需要 get 一个核心点:RC 隔离级别下的事务在每次执行 select 时都会生成一个最新的 read_view 代替原有的 read_view。
如上图所示,左侧为 1 号事务,在不同时间点对 id= 1 的记录分别查询了三次。右侧为 2 号事务,对 id= 1 的记录进行了更新。更新前该记录只有一个版本,更新好变成了两个版本。
1 号事务在 RC 隔离级别下每次执行 select 请求都会生成一个最新的 read_view,前两次查询生成的全局事务活跃列表中包含 trx2,因此根据 MVCC 规定查到的记录为老版本; 最后一次查询的时间点位于 2 号事务提交之后,因此生成的全局活跃事务列表中不包含 trx2,此时在根据 MVCC 规定查到的记录就是最新版本记录。
Repeatable Read(技术解读:写写并发使用 X 锁,读写并发使用 MVCC 避免不可重复读; 当前读使用 Gap 锁避免幻读)
和 RC 模式不同,RR 模式下事务不会再每次执行 select 的时候生成最新的 read_view,而是在事务第一次 select 时就生成 read_view,后续不会再变更,直至当前事务结束。这样可以有效避免不可重复读,使得当前事务在整个事务过程中读到的数据都保持一致。示意图如下所示:
这个就很容易理解,三次查询所使用的全局活跃事务列表都一样,且都是第一次生成的 read_view,那之后查到的记录必然和第一次查到的记录一致。
RR 隔离级别能够避免幻读吗?
如果对幻读还不了解的话,可以参考该系列的第一篇文章。如下图所示,1 号事务对针对 id 1 的过滤条件执行了三次查询,2 号事务执行了一次插入,插入的记录刚好符合 id 1 这个条件。可以看出来,三次查询得到的数据是一致的,这个是由 RR 隔离级别的 MVCC 机制保证的。这么看来,是避免了幻读,但是在最后 1 号事务在 id= 2 处插入一条记录,MySQL 会返回 Duplicate entry 的错误,可见避免了幻读是一种假象。
严格意义避免幻读(技术解读:当前读使用 Gap 锁避免幻读)
之前提到的所有 RR 级别的 select 语句我们称为快照读,快照读能够保证不可重复读,但并不能避免幻读。于是 MySQL 又提出”当前读”的概念,常见的当前读语句有:
1. select for update
2. select lock in share mode
3. update / delete
并且规定,RR 级别下当前读语句会给记录加上一种特殊的锁 -Gap 锁,Gap 锁并不锁定某个具体的记录,而是锁定记录与记录之间的间隔,保证这个间隔中不会插入新的其他记录。下图是一个示意图:
上图中 1 号事务首先执行了一个当前读的 select 语句,这个语句会在 id 0 的所有间隔加上 Gap 锁,接下来 2 号事务在 id = 3 处执行插入时系统就会返回 Lock wait timeout execcded 的异常。当然,其他事务可以在 id = 0 的条件下插入成功,这没问题。
Serializable (技术解读:S 锁(读)+ X 锁(写))
Serialization 隔离级别是最严格的隔离级别,所有读请求都会加上读锁,不分快照读和当前读,所有写会加上写锁。当然,这种隔离级别的性能因为锁开销而相对最差。
MySQL 事务持久性保证
MySQL 事务持久化策略和 HBase 基本相同,但是涉及的组件相对比较多,主要有 doublewrite、redo log 以及 binlog:
1. MySQL 数据持久化(DoubleWrite)
实际上 MySQL 的真实数据写入分为两次写入,一次写入到一个称为 DoubleWrite 的地方,写成功之后再真实写入数据所在磁盘。为什么要写两次? 这是因为 MySQL 数据页大小与磁盘一次原子操作大小不一致,有可能会出现部分写入的情况,比如默认 InnoDB 数据页大小为 16K,而磁盘一次原子写入大小为 512 字节(扇区大小),这样一个数据页写入需要多次 IO,这样一旦中间发生异常就会出现数据丢失。另外需要注意的是 DoubleWrite 性能并不会影响太大,因为写入 DoubleWrite 是顺序写入,对性能影响来说不是很大。
2. redolog 持久化策略(innodb_flush_log_at_trx_commit)
redolog 是 InnoDB 的 WAL,数据先写入 redolog 并落盘,再写入更新到 bufferpool。redolog 的持久化策略和 HBase 中 hlog 的持久化策略一致,默认为 1,表示每次事务提交之后 log 就会持久化到磁盘; 该值为 0 表示每隔 1 秒钟左右由异步线程持久化到磁盘,这种情况下 MySQL 发生宕机有可能会丢失部分数据。该值为 2 表示每次事务提交之后 log 会 flush 到操作系统缓冲区,再由操作系统异步 flush 到磁盘,这种情况下 MySQL 发生宕机不会丢失数据,但机器宕机有可能会丢失部分数据。
3. binlog 持久化策略(sync_binlog)
binlog 作为 Server 层的日志系统,主要以 events 的形式顺序纪录了数据库的各种操作,同时可以纪录每次操作所花费的时间。在 MySQL 官方文档上,主要介绍了 Binlog 的两个最基本核心作用:备份和复制,因此 binlog 的持久化会一定程度影响数据备份和复制的完整性。和 redo 持久化策略相同,可取值有 0,1,N。默认为 0,表示写入操作系统缓冲区,异步 flush 到磁盘。该值为 1 表示同步写入磁盘。为 N 则表示每写 N 次操作系统缓冲就执行一次刷新操作。
以上就是 MySQL 中怎么实现跨行事务模型,丸趣 TV 小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注丸趣 TV 行业资讯频道。