MySQL性能优化之Innodb事务系统,值得收藏

数据库 MySQL
在Innodb中,每次开启一个事务时,都会为该session分配一个事务对象。今天主要分享下Innodb事务系统的一些优化相关,以下基于mysql 5.7。

今天主要分享下Innodb事务系统的一些优化相关,以下基于mysql 5.7。

一、Innodb中的事务、视图、多版本

1. 事务

在Innodb中,每次开启一个事务时,都会为该session分配一个事务对象。而为了对全局所有的事务进行控制和协调,有一个全局对象trx_sys,对trx_sys相关成员的操作需要trx_sys->mutex锁。

mysql数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

MySQL性能优化之Innodb事务系统,值得收藏

2. 视图

Innodb使用一种称做ReadView(视图)的对象来判断事务的可见性(也就是ACID中的隔离性)。根据可见性原则,某个新开启的事务不应该看到其他未提交的事务。 Innodb在执行一个SELECT或者显式开启START TRANSACTION WITH CONSISTENT SNAPSHOT (后者只应用于REPEATABLE-READ隔离级别) 会创建一个视图对象。对于RR隔离级别,视图的生命周期到事务提交结束,对于RC隔离级别,则每条查询开始时重分配事务。

MySQL性能优化之Innodb事务系统,值得收藏

通常一个视图中包含创建视图的事务ID,以及在创建视图时活跃的事务ID数组。例如,当开启一个视图时,当前事务的事务ID为5, 事务链表上活跃事务ID为{2,5,6,9,12},那么就会把{2,6,9,12}存储到当前的视图中(5是当前事务的ID,不记录到视图中),{2,6,9,12}对应的事务所做的修改对当前事务而言都是不可见的,小于2的事务ID对当前事务都是可见的,大于12的事务ID对当前事务是不可见的。

那么如何判断可见性呢?

InnoDB表数据的组织方式为主键聚簇索引。由于采用索引组织表结构,记录的ROWID是可变的(索引页分裂的时候,Structure Modification Operation,SMO),因此二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。无论是聚簇索引,还是二级索引,其每条记录都包含了一个DELETED BIT位,用于标识该记录是否是删除记录。除此之外,聚簇索引记录还有两个系统列:DATA_TRX_ID,DATA_ROLL_PTR。DATA _TRX_ID表示产生当前记录项的事务ID;DATA _ROLL_PTR指向当前记录项的undo信息。

聚簇索引行结构(与多版本一致读有关的部分,DELETED BIT省略):

MySQL性能优化之Innodb事务系统,值得收藏

二级索引行结构:

MySQL性能优化之Innodb事务系统,值得收藏

从聚簇索引行结构,与二级索引行结构可以看出,聚簇索引中包含版本信息(事务号+回滚指针),二级索引不包含版本信息。

对于聚集索引,每次修改记录时,都会在记录中保存当前的事务ID,同时旧版本记录存储在UNDO中;对于二级索引,则在二级索引页中存储了更新当前页的最大事务ID,如果该事务ID大于readview->up_limit_id(对于上例,up_limit_id值为2),那么就需要回聚集索引判断记录可见性;如果小于2, 那么总是可见的,可以直接读取。

3. 多版本(MVCC)

为了便于理解MVCC的实现原理,这里简单介绍一下undo log的工作过程

在不考虑redo log 的情况下利用undo log工作的简化过程为:

MySQL性能优化之Innodb事务系统,值得收藏

说明:

  • 为了保证数据的持久性数据要在事务提交之前持久化
  • undo log的持久化必须在在数据持久化之前,这样才能保证系统崩溃时,可以用undo log来回滚事务

MVCC只在READ COMMITED 和 REPEATABLE READ 两个隔离级别下工作。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加锁。

MySQL性能优化之Innodb事务系统,值得收藏

(1) SELECT

InnoDB 会根据两个条件来检查每行记录:

  • InnoDB只查找版本(DB_TRX_ID)早于当前事务版本的数据行(行的系统版本号<=事务的系统版本号,这样可以确保数据行要么是在开始之前已经存在了,要么是事务自身插入或修改过的)
  • 行的删除版本号(DB_ROLL_PTR)要么未定义(未更新过),要么大于当前事务版本号(在当前事务开始之后更新的)。这样可以确保事务读取到的行,在事务开始之前未被删除。

(2) INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号

(3) DELETE

InnoDB为删除的每一行保存当前的系统版本号作为行删除标识

(4) UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

Innodb的多版本数据使用UNDO来维护的,例如聚集索引记录(1) =>(2)=>(3),从1更新成2,再更新成3,就会产生两条undo记录。

二、Innodb事务系统优化

在MySQL 5.7版本里,针对性的对事务系统做了比较深入的优化,主要解决了下面几个问题。

1. 视图对象的创建需要trx_sys->mutex锁保护

trx_sys->mutex是事务系统最核心的全局锁对象,持有该锁进行的操作都不应该耗时过长。对于read view对象,完全可以将其缓存下来重复使用。这样就避免了持有锁分配视图内存。

因此在MySQL 5.7版本中,实例启动时就分配1024个视图对象;同时维护两个链表,一个是已使用的视图链表,一个是空闲的视图链表;当需要分配新的视图时,总是从空闲视图链表中分配,如果没有,再新分配一个。

2. 视图对象中保存全局事务ID时,需要扫描事务链表

为了判断事务视图的可见性,在打开一个视图时需要拷贝当时活跃的事务ID。

在5.7中,事务系统维持了一个全局事务ID数组,每个活跃读写事务的ID都被加入到其中,在事务提交时从其中删除,这样打开视图时只需要使用memcpy 拷贝该数组即可,无需遍历链表。在读写链表较长(高并发下)的场景,该优化可以显著的提升性能。

3. 用户需要显式开启只读事务,才会放入只读事务链表

mysql5.7将只读事务链表从其中彻底移除了,取而代之的是,所有事务都以只读模式打开。

例如如下事务序列:

  1. BEGIN; 
  2. SELECT; //事务开始,不分配事务ID,不分配回滚段; 
  3. UPDATE; //分配事务ID并插入全局事务数组和事务对象集合中,分配回滚段; 
  4. COMMIT; 

而对于BEGIN;SELECT;SELECT;COMMIT这样的序列,整个事务周期既不分配事务ID,也不分配回滚段。

4. 隐式锁转换为显式锁的开销

Innodb对于类似INSERT操作,采用的是隐式锁的方式,隐式锁不是锁,只是一种称呼而已,只有在需要的时候,才会转换为显式锁。例如如下:

  1. Session 1: BEING; INSERT INTO t1(pk, val) VALUES (1,2); //不创建锁对象 
  2. Session 2: UPDATE t1 SET valval=val+1 WHERE pk=1; //创建两个锁对象,一个是为session1创建一个记录锁对象,另外一个是给自己创建一个等待类型的记录锁对象,然后session2加入锁等待队列。 

在Session 2中为Session1创建锁对象的过程即是所谓的隐式锁向显式锁转换。 当session2扫描到session 1插入的记录时,发现session 1的事务依然活跃,就会进入转换逻辑。

在5.6版本中,其转换过程如下:

  • 持有lock_sys->mutex
  • 2持有trx_sys->mutex;根据事务ID,扫描读写事务链表,找到对应的事务对象;释放trx_sys->mutex;
  • 创建显式锁对象
  • 释放lock_sys->mutex

可以看到,在该操作的过程中,全程持有lock_sys->mutex,持有锁的原因是防止事务提交掉。当读写事务链表非常长时(例如高并发写入时),这种开销将是不可接受的。

在5.7版本中,上述逻辑则优化成:

(1)  持有trx_sys->mutex

  • 根据事务ID找到对应的事务对象(直接查找trx_sys->rw_trx_set,其保存了trx_id和事务对象的映射关系,因此无需扫描读写事务链表)
  • 增加事务对象引用计数(++trx->n_ref)
  • 释放trx_sys->mutex

(2) 持有lock_sys->mutex;

  • 创建显式锁对象;
  • 释放lock_sys->mutex;

(3) 递减事务对象引用计数

在事务commit,释放记录锁前,会先判断引用记录数是否为0,如果不为0,表示正有其他事务为其转换显式锁,这时候需要等待,直到计数为0,才能进入释放事务记录锁阶段。

总的来说,该优化减少了隐式锁转换时持有LOCK_sys->mutex的时间,从而提升性能。

责任编辑:赵宁宁 来源: 今日头条
相关推荐

2023-11-15 16:35:31

SQL数据库

2020-03-27 15:40:10

MySQL索引数据库

2019-02-26 09:14:02

SSD状态监控

2011-06-14 14:17:23

性能优化系统层次

2019-02-26 15:17:15

工具性能数据

2019-12-02 08:58:09

SQL脚本语言MySQL

2019-08-05 09:19:45

PG事务隔离级别数据库

2019-07-29 17:15:35

MySQL操作系统数据库

2019-09-26 08:59:39

DockerGoogle软件

2019-10-22 18:00:00

MySQL基础入门数据库

2022-02-08 18:53:12

SpringBoot性能优化

2011-03-11 15:53:02

LAMP优化

2015-11-10 16:55:00

性能IO子系统Linux

2021-07-29 14:20:34

网络优化移动互联网数据存储

2018-06-01 16:24:29

数据库MySQL Innod阻塞事务

2024-01-18 09:43:11

MySQL数据库

2021-11-29 11:13:45

服务器网络性能

2022-02-16 14:10:51

服务器性能优化Linux

2020-05-27 11:55:47

Oracle SQL性能优化数据库

2019-10-28 09:11:53

MySQL性能优化
点赞
收藏

51CTO技术栈公众号