MySQL整理与总结


MySQL基础架构

MySQL逻辑架构图.png

MySQL大致可以分成Server层和存储引擎层。

连接器

连接器负责跟客户端建立链接、获取权限、维持和管理链接。

一个用户成功创建连接后,即使对该用户修改权限,也不会影响当前存在连接的权限。修改完成之后,只有创建新的连接采用使用新的权限。

可以使用show processlist;查看连接状态。

客户端通过参数wait_timeout来控制没有动静的连接,默认一般是8小时。

使用长链接可能导致MySQL占用内存上涨很快。因为MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源只有在连接断开时才释放。长期积累可能导致内存占用过大,被系统强行OOM,从现象看就是MySQL异常重启。

对此解决方案:

  • 定期断开长连接
  • 如果是MySQL5.7之后的版本,可以在执行一个较大内存的操作时,通过执行MySQL_reset_connection来重新初始化连接资源。此时连接会恢复成刚创建完时的状态

查询缓存

MySQL拿到一个查询请求后,在老版本会先去查询缓存。之前执行的语句及其结果可能会以kv对的形式存在内存中。key是查询语句,value是查询结果。

但是不推荐使用缓存。因为通常情况下MySQL的缓存利大于弊。

查询缓存的失效非常频繁。只要对一个表的更新,这个表上的所有查询缓存就会被清空。对于更新压力较大的数据库来说,查询缓存的命中率很低。除非你的业务是一个静态表,很长时间才更新一次。

通过设置参数query_cache_typeDEMAND,这样默认的SQL语句都不使用查询缓存。需要是显式指定。例如:

select SQL_cache * from T where ID=...;

MySQL8.0之后没有查询缓存这个功能了。

分析器

分析器进行sql的词法分析,知道你要做什么。

解析器

解析器处理语法和解析查询,生成对应的解析树。

预处理器

预处理器进一步检查解析树的合法。比如:数据表和数据列是否存在,别名是否有歧义。如果通过则生成新的解析树,再交给优化器。详见《高性能MySQL》6.4.3查询优化处理。

优化器

优化器是在表里面有多个索引时,决定使用哪个索引;或者一个语句有多个表join的时候,决定各个表的连接顺序。知道要怎么做。

执行器

开始执行时会首先检查相应表你的权限。在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。

如果有权限,打开表继续执行,执行器会根据表的引擎定义,使用这个引擎提供的接口。如果命中索引的话可以在执行器阶段就直接通过索引查找,没有命中索引就遍历查找每行记录。

在数据库的慢查询日志中可以看到一个rows_examined字段,表示这个语句执行过程中扫描了多少行。

在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数在rows_examined并不是完全相同

日志

redo log

redo log是属于InnoDB特有的日志,属于存储引擎层,记录了这个页“做了什么改动”。

MySQL利用WAL(Write-Ahead Logging)技术,先写日志再落磁盘。具体来说就是当一条记录需要更新时,InnoDB引擎就会先把记录写在redo log中,并更新内存,此时更新算完成。等到InnoDB在恰当的时间会将这个更新操作写入磁盘。

InnoDB的redo log大小是固定的。

redo_log示意图.png

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

wirte poscheckpoint之间的空间就是还可以存放的容量。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

innodb_flush_log_at_trx_commit设置为1表示每次事务的redo log直接持久化到磁盘。

binlog

binlog属于Sever层,binlog不具备crash-safe的能力。

binlog有两种模式,statement格式会记录sql语句,row格式会记录行的内容,记两条更新前后都有。

sync_binlog设置为1表示每次事务的binlog直接持久化到磁盘。

二者对比

  1. redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
  2. redo log是物理日志,记录的是在某个数据页上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑。
  3. redo log是循环写,空间固定会用完;binlog是写到一定大小后切换到下一个,并不会覆盖之前的日志。

整体运行逻辑

update语句执行流程.png

有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行简单的 update 语句update table T set c=c+1 where ID=2时的内部流程。

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

在上述过程中,redo存在“两阶段提交”,即prepare和commit。

两阶段提交的目的是为了让两份日志之间的逻辑一致

如果要实现一定时间内的数据库数据恢复,可以通过binlog中存储的所有逻辑操作实现以及定期的整库备份。

当需要恢复数据到指定的某一秒时,那么可以:

  1. 首先找到最近的一次全量备份。将这个表恢复到临时表。
  2. 从备份的时间点开始,将备份的binlog依次取出,重放到误操作之前的那个时刻。
  3. 再对比临时表与线上表之前的差异,同步到线上表。

基于数据恢复的过程,可以论证为什么需要进行两阶段提交。这里不妨使用反证法,即两种情况:1. 先写redo log再写binlog 2. 先写binlog再写redo log。

针对情况1:假设redo log完成,binlog没有执行,MySQL进程异常重启。redo log保证了数据在系统重启后还能恢复,但是此时binlog中并没有相应的原始逻辑记录,这会导致当前数据能依据redo log恢复,但是之后通过binlog恢复则不存在这次操作的记录,也就没有相应的数据。

针对情况2:假设binlog完成,redo log没有执行,MySQL进程异常重启。由于redo log没有写,恢复后这个事务无效。数据为原本的数据。但是binlog中记录了修改数据的原始逻辑,之后用binlog恢复时就会多出一个事务来。

因此,不使用两阶段提交,数据库的状态就可能和用它日志恢复恢复出来的库的状态不一致。

除了数据恢复的场景,在扩容的时候,需要一些备库来增加系统的读能力的时候。现在主流的做法是全量备份加上应用binlog实现。

redo log和binlog都能表示事务的提交状态,而两阶段保持逻辑上的一致。

change buffer

change buffer是一种可持久化的数据,其在内存中有拷贝,也会被写入磁盘。

change buffer使用的是buffer pool中的内存。可以通过innodb_change_max_size来动态设置。其数值表示的是百分比。

changge buffer的使用场景为写多读少的情况,因为读的时候会出发merge的操作,增加了维护的代价。

与redo log的区别在于:redo log主要节省的是随机写磁盘的IO消耗(转为顺序写),而change buffer节省的是随机读磁盘的IO消耗。

事务

事务的支持在引擎层实现

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)。

隔离性与隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,需要在二者之间寻找一个平衡点。

SQL 标准的事务隔离级别包括:

  • 读未提交(read uncommitted):是指,一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(read committed):是指,一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(repeatable read):是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化(serializable ):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

事务隔离级别描述.png

接着这幅图描述下四种隔离级别的区别:

  • 读未提交:V1,V2,V3都是2
  • 读提交:V1是1,V2和V3都是2
  • 可重复读:V1和V2都是1,V3是2
  • 串行化:在事务B执行update操作时,会被锁住,直到事务A提交后,事务B才能继续执行。因此V1,V2都是1,V3才是2。

实际上,数据库会创建一个视图,访问时以视图的逻辑为准,因此针对上述的四种隔离级别:

  • 读未提交:直接返回记录上的最新值,没有视图
  • 读提交:视图在每个SQL开始执行的时候创建
  • 可重复读:视图在事务启动时创建,整个事务存在期间都用这个视图
  • 串行化:直接用加锁的方式实现

Oracle默认隔离级别是读提交。因此将Oracle迁移至MySQL时为了保证隔离级别的一致,也要改成读提交。

MySQL的默认隔离级别是可重复读。

show variables like 'transaction_isolation’ 查看当前隔离级别。

事务隔离实现

下面以“可重复读”为例子解释事务隔离的实现。

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

MVCC.jpg

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

回滚日志会在系统判断当没有事务需要这些回滚日志时,回滚日志就会被删除。而系统判断的方法就是检查系统中没有比这个回滚日志更早的read-view的时候

长事务[1]意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。导致数据量其实不大,但是产生的回滚段无法清理,只能重建整个库。

事务启动方式

Mysql的事务启动方式有以下几种:

  • 显式启动事务语句,begin或者start transaction。配套的提交语句是commit,回滚语句是rollback。
  • set autocommit=0这个命令会将这个线程的自动提交关闭。意味着如果你只执行一个select语句,这个事务就启动了,而不会自动提交。这个事务会持续存在直到你主动执行commit或rollback语句,或断开连接。

有些客户端连接框架默认连接成功后先执行set autocommit=0的操作,这就导致之后的查询都在事务中,如果是长连接,就会导致意外的长事务。

建议使用set autocommit=1来启动事务。

针对在乎频繁使用事务的业务,推荐使用commit work and chain语法。在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

如何避免长事务

从应用开发来看:

  1. 确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL的general_log[2]开起来,然后随便跑一个业务逻辑,通过general_log的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成1。
  2. 确认是否有不必要的只读事务。
  3. 业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

从数据库监控来看:

  1. 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
  2. Percona的pt-kill,关闭数据库连接
  3. 在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题;
  4. 如果使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces[3]设置成2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

索引

索引常用模型

哈希表

哈希表做范围查询因为其不是有序的,因此大多是利用遍历实现,所以**哈希表适用于等值查询的场景。**比如 Memcached 及其他一些 NoSQL 引擎。

有序数组

**有序数组在等值查询以及范围查找中性能都很优秀,但是插入操作太过麻烦,成本较高。**因此有序数组只适用于静态存储引擎。

搜索树

二叉树的搜索效率高,但是实际数据库并不使用二叉树,其原因在于索引不止存在内存中,还要写到硬盘上。

为了减少每次查询的搜索层数,最直接的方法就是让二叉树变成多叉树。这里的叉树取决于数据块的大小。层高越高时,需要查询数据时依赖的磁盘数将明显增多。为了避免查询时磁盘IO的延迟,采用多叉树。

因此多叉树因为其读写上的性能优点以及适配磁盘的访问模式,广泛应用于数据库引擎中。

InnoDB的索引模型

在MySQL中,索引是在存储引擎层实现的,所以并没有统一的索引标准,即不同存储引擎的索引工作方式是不一样的。即使多个存储引擎支持同一类型的索引,其底层实现可能也是不一致的。下文以InnoDB存储引擎为例分析索引引擎。

在InnoDB中表都是根据主键顺序以索引的形式存放,这种存储方式的表称之为索引组织表。InnoDB使用了B+树索引模型,数据都在B+树上。

每个索引在InnoDB中对应一个B+树。

根据叶子节点的内容,索引类型可以区分为主键索引和非主键索引。其中主键索引的叶子节点存储的是整行数据。在InnoDB里,主键索引也叫聚簇索引非主键索引的叶子结点的内容为主键的值,在InnoDB中也称之为二级索引

基于主键索引和非主键索引的区别在于:

  • 使用主键作为where条件的话,只需要搜索主键索引树
  • 使用非主键索引作为where条件的话,需要在非主键索引树上找到相应主键,再去主键索引树上查找相应的记录,这个过程称之为回表

因此推荐使用主键进行查询。

B+树在存储不足时会页分裂,在删除数据时会页合并。

InnoDB的索引维护

在明确了InnoDB中的索引结构后,就引入下一个问题即索引的维护。

B+树维护了数据的有序性,可能出现插入一个数据前,需要在相应位置后的所有数据后移。更甚一步,当前数据页满了,需要新申请新的数据页,这个过程称之为页分裂,可能导致整体空间利用率下降。

通常推荐自增主键避免这种问题,因为对于自增主键对应的数据来说,只有追加写的操作,不存在挪动记录的需要,也不会触发叶子节点的分裂。

主键长度越小,普通索引的叶子节点越小,普通索引所占用的空间也越小。

使用业务字段作为主键的最好满足以下两个条件:

  1. 只有一个索引
  2. 该索引必须是唯一索引

不然最好创建一个自增主键。

MySQL中的索引

MySQL中存在五种索引:

  • 主键索引
    • ALTER TABLE table_name ADD PRIMARY KEY ( column )
  • 唯一索引
    • ALTER TABLE table_name ADD UNIQUE (column)
  • 普通索引
    • ALTER TABLE table_name ADD INDEX index_name ( column )
    • 普通索引上只有索引列和主键的信息,因此每一次获取完整数据都需要回表。
  • 全文索引
    • ALTER TABLE table_name ADD FULLTEXT ( column )
  • 组合索引
    • ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3 )

覆盖索引

如果索引覆盖了查询需求,而不需要不在索引中的数据或其余字段,就称之为覆盖索引。

优点:可以减少树的搜索次数,提高查询性能。

最左前缀原则

针对B+树这种索引结构,索引项是按照索引定义里面出现的字段顺序排序的。因此,只要满足最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符,就可以利用索引加速查询性能。

因此在建立联合索引的时候,如何安排索引内的字段顺序也很重要。第一原则是,如果通过调整顺序,可以少维护一个索引,则这个顺序需要被优先考虑。

如果不得不维护多个索引,需要考虑字段大小

索引下推

MySQL在5.6之前,如果需要多个字段比较,会在对应的索引树上查找,然后回表查询这个记录的其余字段是否满足。在5.6中引入了索引下推优化,可以在索引遍历过程中,对索引包含的字段先做判断,直接过滤不满足条件的记录,减少回表次数。

索引维护

重建普通索引是合理的:索引可能因为删除或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,这样提高页面的利用率。

但是重建主键索引是不合理的。因为无论删除主键或者更新主键,都会将整个表进行重建。因此可以直接 alter table T engine=InnoDB

mysql的删除并不是直接删除,而是标记为可以复用。因此删除数据后索引大小不变。

唯一索引与普通索引的选择

读取过程

理论上唯一索引在查找的时候只查找一个,普通索引需要查找多个。但是由于InnoDB的数据按照数据页为单位读写,每页大小默认16KB。绝大多数情况下读取其余条也都是在内存中,可以忽略不计。

更新过程

因为唯一索引的存在,所需要先判断操作是否违反唯一性约束因此需要读入内存,也就无法使用change bufer。

如果更新的数据在内存中,唯一索引和普通索引的更新区别微乎其微。

如果更新的数据不在内存中,唯一索引的数据需要将数据页读入内存中,进行操作。而普通索引直接写changer buffer即可。普通索引比唯一索引减少了从磁盘读取数据到内存的随机IO访问时间

结论

没事别用唯一索引。如果写完就读 ,关闭change buffer;否则开着。

全局锁

对整个数据库实例加锁。 Flush tables with read lock将这个库处于只读状态。

典型使用场景:做全库逻辑备份。

缺点:

  • 锁主库,会导致备份期间不能执行除了查询之外的逻辑,业务停摆
  • 锁从库,不能执行主库同步的binlog,导致主从延迟

官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。

针对不支持可重复读隔离级别的存储引擎,只能使用FTWRL。

single-transaction方法只适用于所有的表使用事务引擎的库

相较于set global readonly=true的优势:

  • 在一些系统中readonly值会被用来做判断主库或是从库
  • 执行FTWRL时客户端异常断开,MySQL会释放全局锁。但是readonly则无法解除。

表级锁

表锁

语法:lock tables ...read/write。可以使用unlock tables主动释放锁,页可以在客户端断开时自动释放。lock tables语法会在限制其余线程读写时,同时限定本线程接下来的操作对象。

元数据锁

无需显式使用,在访问一个表的时候会被自动加上,保证读写的正确性。

当对一个表做增删改查操作时加MDL读锁,当对一个表操作ddl命令时加写锁。

读锁之间不互斥

写锁互斥

事务中的MDL锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。

给小表增加字段的方式:

  • 在Mysql的information_schema库中的innodb_trx表中查询当前执行中的事务,kill或等事务执行完成后执行DDL
  • 针对热点表,在alter table语句中设置等待时间,期望能在等待时间中获取到MDL写锁。
    • ALTER TABLE tbl_name NOWAIT add column ...
    • ALTER TABLE tbl_name WAIT N add column ...

行锁

两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。

**如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。**目的是减少事务之间的锁等待时间。

发生死锁时现在有两种解决方案:

  • 直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
  • 发起死锁检测,发现死锁后主动回滚某一个事务。innodb_deadlock_detect on表示开启死锁检测

解决热点数据行更新导致的性能问题的方式:

  • 如果能确保整个业务一定不会死锁,临时将死锁检测关闭。但是关闭死锁检测可能导致大量的超时对业务有损
  • 控制并发数量:在服务端或者中间件中增加并发控制,减少需要检测死锁的线程数量
  • 数据分段:通过将一种数据拆分成多种数据,减少冲突概率和需要死锁检测的时间,但是需要额外的业务逻辑

innodb行级锁是通过锁索引记录实现的,如果更新的列没建索引是会锁住整个表的

事务启动的时机

  • begin/start transaction一致性视图是在执行第一个快照读语句时创建
  • start transaction with consistent snapshot即时创建一个维持整个事务的一致性快照。在RC下,等效于普通的start transaction

MVCC

MVCC模型在MySQL中的具体实现规则是由以下组成:

  • undo日志
  • Read View
  • 四个隐式字段

隐式字段

每行记录除了自动以的字段外,还有数据库隐式定义的字段:

  • DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据库没有主键,InnoDB会自动以DB_ROW_ID创建一个聚簇索引。MySQL8.0版本引入
  • DB_TRX_ID:6byte,最近修改事务ID:记录这条记录最后一次修改该记录的事务ID
  • DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存在rollback segment里)
  • DELETE_BIT:1byte,记录被更新或删除并不代表真的删除,而是删除flag变了

undo日志

查询操作不记录对应的unde log。undo log分为三种:

  • Insert undo log:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。

  • Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。

  • Delete undeo log: 删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。

    • 在事务过程中,删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。
    • 为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

    Undo log日志产生过程可见

    最终undo log会记录成一条版本线性表。链首是最新的历史记录,链尾是最早的历史记录。在事务完成之后就可能会被purge线程删除。

Read View

Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是严格递增的,所以最新的事务,ID值越大)。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。

在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View

RR下,一个事务只能读到启动前提交的数据以及自身修改的数据。

对于一个事务来说:

  1. 版本未提交,不可见
  2. 版本已提交,但是在一致性视图创建后提交,不可见
  3. 版本已提交,在一致性视图创建前提交,可见

当前读

读取的是记录的最新版本,读取时保证其他并发事务不能修改当前记录,会对读取的记录上锁。在update的时候会通过当前读更新数据,此外select预计加锁也是当前读。形如:

select k from t where id=1 lock in share mode;读锁(S锁,共享锁)
select k from t where id=1 for update;写锁(X锁,排他锁)

快照读

不加锁的非阻塞锁。其前提是隔离级别不是串行级别。串行下的快照读会退化成当前读。

MySQL_MVCC_流程.png

Tip

具体而言,general_log 主要有以下作用:

  1. 故障排除:通过查看 general_log 日志,可以追踪和分析数据库的操作过程,帮助识别和解决出现的错误、异常或故障情况。
  2. 性能分析:通过分析 general_log 中的查询语句和连接事件,可以了解数据库的工作负载、查询频率、响应时间等信息,从而进行性能调优和优化。
  3. 安全审计:general_log 记录了所有的查询语句和连接事件,可以用于安全审计和合规要求。通过检查日志,可以跟踪和审查用户的操作,检测潜在的安全问题、数据泄露和非法访问。

需要注意的是,启用 general_log 会生成大量的日志数据,因此在生产环境中长期启用可能会导致日志文件过大,对磁盘空间和性能产生负面影响。因此,通常只在需要进行故障排除、性能分析或安全审计时才启用 general_log,在完成相关任务后及时关闭或定期清理日志文件。

  1. 分散回滚数据:将回滚数据分散存储在多个回滚段中,可以减轻单个回滚段的负担,降低回滚段过大的风险。
  2. 清理方便:当需要清理回滚段时,如果有多个回滚段,可以通过删除不再需要的回滚段文件来实现清理。相比只有一个回滚段的情况,清理起来更加方便,不会影响其他正在活动的回滚操作。

需要注意的是,设置 innodb_undo_tablespaces 的值需要权衡存储空间和性能。每个回滚段都需要占用一定的磁盘空间和内存,因此设置过大的值可能会导致额外的资源消耗。应根据实际情况和系统需求来进行适当的设置。

系统表空间是存放系统信息的,对应的磁盘文件是ibdata1,数据表空间是一个个表数据文件,对应磁盘文件是表名.ibd

参考文献

  1. 掘金,MySQL-长事务详解,2019

  1. 长事务,顾名思义就是执行时间较长,长时间未提交的事务。 ↩︎

  2. MySQL 的 general_log 是一种日志记录功能,用于记录所有的查询语句和连接事件,包括客户端的连接、查询、事务和其他操作。它的作用是提供详细的日志记录,用于故障排除、性能分析和安全审计等方面。 ↩︎

  3. 通过将 innodb_undo_tablespaces 设置为 2 或更大的值,可以创建多个回滚段(undo tablespace),从而将回滚数据分散存储在不同的回滚段中。这样做的好处是: ↩︎


文章作者: 不二
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 不二 !
  目录