mysql innodb 引擎

2024-04-10 189点热度 0人点赞 0条评论

架构图

InnoDB Architecture 官网地址 : https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html

mysql_架构图

从架构图上可以看到Innodb的架构可以划分为“内存架构(In-Memory Structures)”和“磁盘架构(On-Disk Structures)”。
内存架构比较简单, 由Buffer Pool和Log Buffer组成,磁盘架构稍微复杂些,
由Redo Log日志文件、Undo Log日志文件、系统表空间(System Tablespace)下的文件、独立表空间(General Tablespaces)下的文件;
还有临时表空间(Temporary Tablespaces)下的一些文件和双写缓冲区(Doublewrite Buffer)下的文件等等。

内存架构 Buffer Pool

Buffer Pool设计了free list、flush list、lru list等数据结构在内存中缓存并管理数据页。

free list

Mysql服务器刚启动时,Innodb引擎需要完成Buffer Pool的初始化,首先向操作系统申请需要使用的内存空间,
然后将它划分成很多控制块和缓存页。因为此时还没有真正的数据页被缓存到Buffer Pool中,
为了记录哪些缓存页是可以使用的,Innodb引擎维护了一个free链表:

有了free链表之后,每当需要从磁盘加载一个页到Buffer Pool中时,就直接从free链表中取一个空闲的缓存页就好了,
并且将该缓存页对应的控制块填充上缓存页的元信息(就是我们前边提到的,
页面的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、锁信息和LSN等),
并将该控制块从free链表中移除,表示该缓存页已经被使用了。

flush list

Innodb引擎维护了另外一种链表——flush list(脏页链表),当有缓存页被修改的时候,就添加到里边,将来脏页同步到磁盘以后,
就从flush list中移除,flush list的构造也是和free list差不多的。

lru list

https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html

Innodb引擎也在Buffer Pool中创建了自己的LRU链表,设计的原则就是:尽量提高Buffer Pool的缓存命中率。

  • 如果数据页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,将该缓存页对应的控制块作为节点塞到链表的头部。

  • 如果该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。

Change Buffer

在查找二级索引记录时,我们遇到了大量的随机I / O,这是Innodb引擎的设计者所不能容忍的,
所以设计了Change Buffer这个特殊的数据结构来解决这个问题。
一句话总结:Change Buffer是为了缓解Innodb引擎在读请求期间产生的随机I / O而设计的一种特殊的数据结构。

https://dev.mysql.com/doc/refman/8.0/en/innodb-change-buffer.html

类似与军事上的兵马未到,粮草先行,当二级索引页不在Buffer Pool中时,在写请求到来时,Innodb引擎会缓存这些更改 。
此后页面通过其他读取操作加载到Buffer Pool中时,可能由INSERT, UPDATE或 DELETE操作(DML)导致的缓冲更改将在以后合并到缓存页中。

demo

  1. reader 1 执行写操作(e.g update),但针对的 P1 并不在 buffer pool 中
  2. 于是 client 1 将这个操作缓存到 change buffer 里,即添加一个 entry(ibuf insert)
  3. reader 2 需要读操作,将 P1 读到 buffer pool 中
  4. 将 change buffer 里相关的缓存的操作全部合并(merge)至 P1(ibuf merge)
  5. 将 P1 返回给用户线程

总结

InnoDB 实现了 Change buffer 来优化用户在二级索引上的随机写入问题,
用户可以根据自己的需求结合 Change buffer 的一些条件来判断是否启用 Change buffer,
但需要注意的是 Change buffer 的阈值只有 2kb,假如在一个二级索引的数据 Page 写入的 record 长度超过 2kb,
就会触发 ibuf merge, 从而使后续的 ibuf 缓存条件失效, 但这也符合 IO-bound 的场景需求.

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。

Log Buffer

Innodb引擎的内存架构主要分两部分,一部分是Buffer Pool,我们已经详细介绍过它了,
Buffer Pool主要是为了解决每次数据页更新都同步磁盘的问题;相同的,Innodb引擎的内存架构的另一部分Log Buffer,
也是为了解决redo log日志直接写磁盘带来的性能损耗问题。
详细参见 磁盘架构下面相关的描述

磁盘架构

https://juejin.cn/post/6999625352955822087

数据页
写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。
读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。

用户记录
对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分空闲空间分配给用户记录。

额外信息
额外信息并非真正的用户数据,它是为了辅助存数据用的。

变长字段列表
有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。

null值列表
数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。

记录头信息
记录头信息用于描述一些特殊的属性。

隐藏列
数据库在保存一条用户记录时,会自动创建一些隐藏列。
   如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。
   如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。

真正数据列
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。

最大和最小记录
在保存用户记录的同时,也保存最大和最小记录了。
最大记录保存到 Supremum 记录中。
最小记录保存在 Infimum 记录中。

页目录
通过二分查找,快速的定位需要查找的记录。

文件头部
innodb是通过文件头部对象中的 页号、上一页页号和下一页页号来串联不同数据页的。

文件尾部
它里面记录了页面的校验和(4字节)+ LSN(4字节)

redo log

什么是redo log ?

redo log叫做重做日志,是用来实现事务的持久性以及 故障后恢复。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),
前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。

redo log 有什么作用?

mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Buffer Pool(缓冲池)里头,
把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。

那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行操作。这样会导致丢部分已提交事务的修改信息!

所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。

读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;

写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;

mysql_redo_log 图示

Redo Log 的刷盘时机

  • 在事务提交时,若设置了参数 innodb_flush_log_at_trx_commit 为 1,则会进行刷盘操作。
  • 当 Redo Log Buffer 空间不足时,系统也会强制进行刷盘操作。
  • 后台线程也会每秒进行一次刷盘操作。
  • 做checkpoint时。
  • 服务正常停止。
innodb_flush_log_at_trx_commit 参数可以配置以什么样的机制刷入磁盘
0 表示由后台线程来处理;

1 表示同步刷盘(默认值);

2 表示写到操作系统缓冲区,只要操作系统不挂就没事,操作系统挂了,事务就无法保证

redo log 数据结构

  • MLOG_1BYTE(type字段对应的十进制数字为1):表示在页面的某个偏移量处写入1个字节的redo日志类型。
  • MLOG_2BYTE(type字段对应的十进制数字为2):表示在页面的某个偏移量处写入2个字节的redo日志类型。
  • MLOG_4BYTE(type字段对应的十进制数字为4):表示在页面的某个偏移量处写入4个字节的redo日志类型。
  • MLOG_8BYTE(type字段对应的十进制数字为8):表示在页面的某个偏移量处写入8个字节的redo日志类型。
  • MLOG_WRITE_STRING(type字段对应的十进制数字为30):表示在页面的某个偏移量处写入一串数据。
  • MLOG_MULTI_REC_END(type字段对应的十进制数字为31),该类型的redo log日志结构很简单,只有一个type字段。
  • MLOG_COMP_REC_INSERT(对应的十进制数字为38):表示插入一条使用紧凑行格式的记录时的redo日志类型。
  • 等等

拿更通用的MLOG_WRITE_STRING类型的日志举例,在data部分需要记录三种信息:

  • 要修改的内容在数据页中的偏移量(offset)
  • 修改了多少个字节的数据(len)
  • 修改后的数据内容是什么(content)

mysql_redo_log_struct

Mini-Transaction

关于Mini-Transaction,Mysql是这样进行定义的:Innodb引擎对底层页的一次原子访问的过程叫做Mini-Transaction。


我们来举一个例子,假如我们在创建有索引的数据表中插入一条记录,那么我们需要在Innodb引擎的聚簇索引B+树的数据页中插入这条记录,
更改这条记录的上一条记录的next_record的内容,使其指向这条记录;然后还需要在辅助索引B+树的数据页的中插入这条记录的索引信息;
上边描述的这个过程就称为对底层页的一次原子访问,也就是说这次原子访问可能修改了多个数据页的信息,这个过程是不可分割的。
怎么理解这个不可分割呢?就是说我们不能只在聚簇索引B+树的数据页中插入了一条记录,而相应的辅助索引B+树的数据页却没有做变更,
我们就把这个不可分割的访问过程称为Mini-Transaction(翻译成中文就是“小事务”)。

作用
向有索引的数据表中插入一条记录会产生多条redo log,假如这些redo log只插入了其中一部分时,数据库宕机了。
我们知道redo log是做数据页恢复用的,假如我们只恢复了一部分redo log,那么这张数据表所对应的B+树就处于不正确的状态了。
Innodb引擎当然不会允许这种事情发生,所以对于一个Mini-Transaction产生的redo log日志都会被划分到一个组当中去,在进行系统崩溃重启恢复时,
针对某个组中的redo日志,要么把全部的日志都恢复掉,要么就一条也不恢复。


#### Log Buffer是怎样存储redo log信息

```text
Innodb引擎为了方便业务数据的管理,设计了“数据页”来存放记录;
同样的,为了更加方便的使用redo log,Innodb也设计了一种结构:redo log block, 用来存放我们前边提到的redo log。

真正存储redo log数据的仅仅是log block body的那496个字节;log block header占12个字节,log block tailer占4个字节,log block tailer存储的LOG_BLOCK_CHECKSUM是用来校验数据完整性使用的,我们不用关心,我们主要来看看log block header的几个属性吧。

  • LOG_BLOCK_HDR_NO:每一个block都有一个大于0的唯一标号,本属性就表示该标号值。

  • LOG_BLOCK_HDR_DATA_LEN:表示block中已经使用了多少字节,初始值为12(因为log block body从第12个字节处开始)。随着往block中写入的redo日志越来也多,本属性值也跟着增长。如果log block body已经被全部写满,那么本属性的值被设置为512。

  • LOG_BLOCK_FIRST_REC_GROUP:一条redo日志也可以称之为一条redo日志记录(redo log record),一个mtr会生产多条redo日志记录,这些redo日志记录被称之为一个redo日志记录组(redo log record group)。LOG_BLOCK_FIRST_REC_GROUP就代表该block中第一个mtr生成的redo日志记录组的偏移量(其实也就是这个block里第一个mtr生成的第一条redo日志的偏移量)。

  • LOG_BLOCK_CHECKPOINT_NO:表示所谓的checkpoint的序号,checkpoint是我们后续内容的重点,现在先不用清楚它的意思。

redo log是怎样写入到log buffer中的?

Buffer Pool主要是为了解决每次数据页更新都同步磁盘的问题;
相同的,Innodb引擎的内存架构的另一部分Log Buffer,也是为了解决redo log日志直接写磁盘带来的性能损耗问题。

在Mysql服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,
Log Buffer内存空间的大小由启动参数innodb_log_buffer_size来指定,默认是16MB,
这片内存空间被划分成若干个连续的redo log block,这也是Log Buffer的内存结构,
Innodb引擎还定义了一个称之为buf_free的全局变量,标示redo log日志写到了Log Buffer的哪个位置。

总结

redo log是用来恢复数据的 用于保障,已提交事务的持久化特性

undo log

什么是 undo log ?

undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。
undo log 主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。

需要注意的是,undo log默认存在全局表空间里面,你可以简单的理解成undo log也是记录在一个MySQL的表里面,插入一条undo log和插入一条普通数据是类似。也就是说,写undo log的过程中同样也是要写入redo log的。

undo log 有什么作用?

undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

总结

undo log是用来回滚数据的用于保障 未提交事务的原子性

mysql锁技术以及MVCC基础

1. mysql锁技术

当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。

读写锁
解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,这两种锁被称为:

共享锁(shared lock),又叫做"读锁"
读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。

排他锁(exclusive lock),又叫做"写锁"
写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
总结:
通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行.
事务的隔离性就是根据读写锁来实现的!!!

2. MVCC基础

MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间,
当然存储的并不是实际的时间值,而是系统版本号。

MVCC在mysql中的实现依赖的是undo log与read view

  • undo log :undo log 中记录某行数据的多个版本的数据。
  • read view :用来判断当前版本数据的可见性

mysql_mvcc 图示

MVCC的必要性

MVCC只在RC和RR下. 应对高并发事务, MVCC 比单纯的加行锁更有效, 开销更小。

InnoDB MVCC 实现原理

InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)。

  • DATA_TRX_ID 记录最近更新这条行记录的事务 ID,大小为 6 个字节.
  • DATA_ROLL_PTR 表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。
  • DB_ROW_ID 行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除。

事务的实现

前面讲的重做日志,回滚日志以及锁技术就是实现事务的基础。

  • 事务的原子性是通过 undo log 来实现的
  • 事务的持久性性是通过 redo log 来实现的
  • 事务的隔离性是通过 (读写锁+MVCC)来实现的
  • 而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!
  • 原子性,持久性,隔离性折腾半天的目的也是为了保障数据的一致性!

事务想要做到什么效果?

要做到可靠性以及并发处理。

可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,
想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log和redo log。

并发处理:也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,
为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL 的隔离级别。
1. 原子性的实现
什么是原子性:

一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,
对于一个事务来说不可能只执行其中的部分操作,这就是事务的原子性。

上面这段话取自《高性能MySQL》这本书对原子性的定义,原子性可以概括为就是要实现要么全部失败,要么全部成功。

以上概念相信大家伙儿都了解,那么数据库是怎么实现的呢? 就是通过回滚操作。
所谓回滚操作就是当发生错误异常或者显式的执行rollback语句时需要把数据还原到原先的模样,
所以这时候就需要用到undo log来进行回滚.

总结

  • 每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上。
  • 所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。
2. 持久性的实现
事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。

先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。 
为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;

上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!!!

因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。

于是 redo log就派上用场了。下面看下redo log是什么时候产生的

https://cloud.tencent.com/developer/article/1431307

既然redo log也需要存储,也涉及磁盘IO为啥还用它?

(1)redo log 的存储是顺序存储,而缓存同步是随机操作。

(2)缓存同步是以数据页为单位的,每次传输的数据大小大于redo log。

3. 隔离性实现
隔离性是事务ACID特性里最复杂的一个。在SQL标准里定义了四种隔离级别,每一种级别都规定一个事务中的修改,哪些是事务之间可见的,哪些是不可见的。

级别越低的隔离级别可以执行越高的并发,但同时实现复杂度以及开销也越大。

Mysql 隔离级别有以下四种(级别由低到高):

READ UNCOMMITED (未提交读)
READ COMMITED (提交读)
REPEATABLE READ (可重复读)
SERIALIZABLE (可重复读)
只要彻底理解了隔离级别以及他的实现原理就相当于理解了ACID里的隔离型。前面说过原子性,隔离性,持久性的目的都是为了要做到一致性,
但隔离型跟其他两个有所区别,原子性和持久性是为了要实现数据的可性保障靠,比如要做到宕机后的恢复,以及错误后的回滚。

那么隔离性是要做到什么呢?  隔离性是要管理多个并发读写请求的访问顺序。 这种顺序包括串行或者是并行
说明一点,写请求不仅仅是指insert操作,又包括update操作。
总之,从隔离性的实现可以看出这是一场数据的可靠性与性能之间的权衡。
  • 可靠性性高的,并发性能低(比如 Serializable)
  • 可靠性低的,并发性能高(比如 Read Uncommited)
READ UNCOMMITTED
在READ UNCOMMITTED隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。

因为读不会加任何锁,所以写操作在读的过程中修改数据,所以会造成脏读。好处是可以提升并发处理性能,能做到读写并行。

换句话说,读的操作不能排斥写请求。

  • 优点:读写并行,性能高
  • 缺点:造成脏读
READ COMMITTED
一个事务的修改在他提交之前的所有修改,对其他事务都是不可见的。其他事务能读到已提交的修改变化。在很多场景下这种逻辑是可以接受的。

InnoDB在 READ COMMITTED,使用排它锁,读取数据不加锁而是使用了MVCC机制。或者换句话说他采用了读写分离机制。
但是该级别会产生不可重读以及幻读问题。

什么是不可重读?

在一个事务内多次读取的结果不一样。

为什么会产生不可重复读?

这跟 READ COMMITTED 级别下的MVCC机制有关系,在该隔离级别下每次 select的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。

在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读

REPEATABLE READ(Mysql默认隔离级别)
在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。
mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC。

采用读写锁实现

为什么能可重复度?只要没释放读锁,在次读的时候还是可以读到第一次读的数据。

  • 优点:实现起来简单

  • 缺点:无法做到读写并行

采用MVCC实现:

为什么能可重复度?因为多次读取只生成一个版本,读到的自然是相同数据。

  • 优点:读写并行

  • 缺点:实现的复杂度高

SERIALIZABLE
该隔离级别理解起来最简单,实现也最单。在隔离级别下除了不会造成数据不一致问题,没其他优点。

4. 一致性的实现

数据库总是从一个一致性的状态转移到另一个一致性的状态。
通过回滚,以及恢复,和在并发环境下的隔离做到一致性。

事务传播行为

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,
他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。
什么是事务传播行为?
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

用伪代码说明:
 public void methodA(){
    methodB();
    //doSomething
 }

 @Transaction(Propagation=XXX)
 public void methodB(){
    //doSomething
 }

代码中methodA()方法嵌套调用了methodB()方法,methodB()的事务传播行为由@Transaction(Propagation=XXX)设置决定。
这里需要注意的是methodA()并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。

Spring中七种事务传播行为
  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常见的选择。

  2. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

  3. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

  4. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

  5. PROPAGATION_REQUIRES_NEW:支持当前事务,创建新事务,无论当前存不存在事务,都创建新事务。

  6. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

  7. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

优化技巧

img.png

java代码中事务失效问题

1、@Transactional 应用在非 public 修饰的方法上
事务拦截器在目标方法执行前后进行拦截,内部会调用方法来获取Transactional 注解的事务配置信息,
调用前会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

2、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。

如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。

3、同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),
但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

在同一个类中调用异步方法,等于调用this本类的方法,没有走Spring生成的代理类,也就不会让他异步执行,@Transactional的原理也类似。

4、捕获异常
如果你手动的catch捕获这个异常并进行处理,事务管理器会认为当前事务应该正常commit,就会导致注解失效,
如果非要捕获且不失效,就必须在代码块内throw new Exception抛出异常。

@async + @Transactional 事务失效问题

Spring事务管理的传播机制是使用 ThreadLocal 实现的。因为 ThreadLocal 是线程私有的,所以 Spring 的事务传播机制是不能够跨线程的。

如何实现分布式事务?

jta & XA

XA :XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列,等等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。XA使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。

JTA: Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。

1. 流水任务,最终一致性,前提是接口要支持幂等性

执行业务逻辑前,先插入流水任务,如果中间过程调用外部RPC接口服务或者本地数据库操作失败时,流水任务会被定时调度任务周期性触发、重试,直到成功。
前提条件,所有接口服务都要实现幂等。当执行成功时,流水记录会被删除。
当然为了缩小接口的重试范围,也可以针对局部调用失败引入局部重试流水。

优点:

  • 实现简单,不依赖任何外部框架

缺点:

  • 不支持回滚,只能不断重试直到接口成功。如果中间某一步操作因数据问题无法成功,只能重试若干次后报警人工介入。
  • 无论全局重试、还是片段重试,都要单独处理,复杂度高

2. 事务消息

在淘宝平台中,广泛使用分布式事务场景的方案是基于消息分布式事务,通过MQ事务消息功能特性达到分布式事务的最终一致性。

mysql_transaction_msg

  • MQ发送方执行第一个本地事务前,会向MQ服务端发送一条消息,但这条消息不同于普通MQ消息,而是一条事务消息。事务消息在MQ的服务端处于一个特殊的状态,此时消息已经保存到MQ服务端,但MQ订阅方是无法感知到该条消息,并且不会进行消费。

  • 完成事务消息的发送后,开始执行本地的数据库事务操作,并根据执行结果走提交或回滚

  • 如果本地事务执行后,因为某些原因没有及时给MQ服务端相应的反馈,MQ服务端会向业务处理服务询问消息状态,业务处理服务根据消息ID或者消息内容确认该消息是否有效。

  • 如果发现本地事务没有执行,则给MQ服务端返回结果,告知MQ服务端可废弃该事务消息。

  • 如果检查发现本地事务已经实际已经成功执行了,则MQ服务端的消息为正常状态。

  • 消息订阅方获取到正常消息后,执行第二个本地事务。如果第二个本地事务执行成功,则最终实现两个不同数据库上的事务同时成功。如果失败,借助MQ框架自身的重试机制,多次重试,实现数据的最终一致性。

3. 二阶段提交

mysql_two_commit

  • 准备阶段

    • 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
    • 事务参与者收到请求之后,本地执行事务,但不提交。
    • 参与者将自己事务执行情况反馈给协调者,同时等待协调者的下一步通知。
  • 提交阶段

    • 根据准备阶段的结果,执行commit或rollback

4. 三阶段提交

mysql_three_commit

第一阶段:CanCommit

协调者向参与者发送事务执行请求CanCommit,参与者如果可以提交就返回YES响应,否则就返回NO响应。

第二阶段:PreCommit

协调者根据参与者反馈的结果来决定是否继续执行事务的PreCommit操作,根据协调者反馈的结果,有以下两种可能:

1、假如协调者收到参与者的反馈结果都是YES,那么就会执行PreCommit操作。

  • 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  • 参与者接收到PreCommit请求后,执行事务操作,但不提交
  • 事务操作执行成功,则返回ACK响应,然后等待协调者的下一步通知。

2、假如有任何一个参与者向协调者发送了NO响应,或者等待超时之后,协调者没有收到参与者的响应,那么就中断事务。

  • 协调者向所有参与者发送中断请求。
  • 参与者收到中断请求之后(或超时之后,仍未收到协调者的请求),执行事务中断操作。

第三阶段:DoCommit

1、执行提交

  • 协调者收到ACK之后,向所有的参与者发送DoCommit请求。
  • 参与者收到DoCommit请求之后,提交事务。
  • 事务提交之后,向协调者发送ACK响应。
  • 协调者收到ACK响应之后,完成事务。

2、中断事务

  • 在第二阶段,协调者没有收到参与者发送的ACK响应,那么就会执行中断事务。

5. TCC

TCC方案分为Try Confirm Cancel三个阶段,属于补偿性分布式事务。

Try:尝试待执行的业务

这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源

Confirm:执行业务

这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。

Cancel:取消执行的业务

若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作。

TCC方案适用于一致性要求极高的系统,比如金钱、交易相关,基于补偿的原理,
因此,需要编写大量的补偿代码,比较冗余。市面可以参考的开源TCC框架,比如TCC-transaction。

6. Seata 框架(开源项目名:Seata)
地址:https://github.com/seata/seata

GTS 把分布式事务定义为由若干本地事务(分支)组成的全局事务。被全局事务管理的全部分支,将在协调器的协调下,保证一起成功或一起回滚。

GTS 定义了一个事务模型,把整个全局事务过程模型化为 TM、RM、TC 三个组件之间协作的机制。

  • Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

mysql_seata

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
  • XID 在微服务调用链路的上下文中传播。
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  • TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

GTS 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前的数据镜像组织成回滚undo日志,执行SQL,并得到redo日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。这样可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。

7. SAGA

SAGA模型把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块,当事务中任意一个本地事务出错时,
可以通过调用对应的补偿方法恢复之前的事务,从而达到数据的最终的一致性。SAGA的事务管理器负责在事务失败时执行补偿逻辑,
可以通过调用执行模块的逆向操作(例如执行子事务时同时生成逆向SQL)或调用业务开发人员提供的补偿方法(需要保证补偿的幂等性)来实现。

可以看到,SAGA虽然对业务造成一定的侵入,但当相对TCC已经有好很多了,而且,事务管理器理论上可以做到向后补偿(撤销所有已完成操作,
恢复到事务开始状态)或向前补偿(继续完成未完成事务,使业务请求得到成功处理,更符合业务预期)。

《MySQL运维内参》
//todo
https://developer.aliyun.com/article/592937

数据库优化参数

  1. InnoDB 刷脏页的控制策略要用到 innodb_io_capacity 这个参数了,它会告诉 InnoDB 你的磁盘能力。这个值我建议你设置成磁盘的 IOPS。磁盘的 IOPS 可以通过 fio 这个工具来测试,下面的语句是我用来测试磁盘随机读写的命令:
    fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest

  2. 参数 innodb_max_dirty_pages_pct 是脏页比例上限,默认值是 75%。

  3. innodb_flush_neighbors 值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,自己刷自己的。

    • 找“邻居”这个优化在机械硬盘时代是很有意义
    • SD 这类 IOPS 比较高的设备 建议设置0
  4. sql_safe_updates
    把 sql_safe_updates 参数设置为 on。这样一来,如果我们忘记在 delete 或者 update 语句中写 where 条件,或者 where 条件里面没有包含索引字段的话,这条语句的执行就会报错。

重建表相关

在 MySQL 5.6 版本开始引入的 Online DDL。

mysql_recreateTab

  1. 建立一个临时文件,扫描表 A 主键的所有数据页;
  2. 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;
  3. 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态;
  4. 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 的状态;
  5. 用临时文件替换表 A 的数据文件。

alter table t engine=InnoDB == alter table t engine=innodb,ALGORITHM=inplace;
从 MySQL 5.6 版本开始,alter table t engine = InnoDB(也就是 recreate)

analyze table t 其实不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了 MDL 读锁;

optimize table t 等于 recreate+analyze。

alter table t engine=innodb,ALGORITHM=copy;

其他

一致性读、当前读、可重复读、读提交

可重复读的核心就是一致性读(consistent read);

而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
当前读

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
除了 update 语句外,select 语句如果加锁,也是当前读。

参考

https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html
等记不清的公众号文章

mylomen

本人从事 JAVA 开发10多年,将之前整理的笔记分享出来,希望能够帮助到努力的你。

文章评论