该系列文章只是为了扫盲大家对于事务的理解,并不会太深入涉及具体底层实现,但是会介绍部分原理
四大特性
- 原子性(Atomicity)
一个事务必须被视为一个不可风格的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
- 一致性(Consistency)
数据库总是从一个一致性的状态转到另一个一致性的状态。
- 隔离性(Isolation)
通常来说,一个事务所做的修改在未提交之前,对其他事务是不可见的。
- 持久性(Durability)
一旦事务提交,则其所做的修改就会永久保存到数据库中,即使系统崩溃,修改的数据也不会丢失
隔离级别
- 未提交读(Read Uncommitted)
在该级别,事务中的修改,即使未被提交,对其他事务也是可见的。事务可以读取未提交的数据,这也被称为
脏读(Dirty Read)
。
- 提交读(Read Committed)
一个事务开始时,只能看见已经提交的事务所做的修改。换句话说,一个事务从开始到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时也叫做
不可重复读(nonrepeatable read)
,因为两次执行同样的查询,可能会得到不一样的结果。
- 可重复读(Repeatable Read)
解决了
脏读
,该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,该级别无法解决幻读(Phanto Read)
问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB
通过多版本并发控制(MVCC)
解决了幻读问题。
可重复读时MySQL的默认事务隔离级别。
- 可串行化(Serializable)
强制事务串行执行,避免了
幻读
问题
如何保证事务?
Undo Log 实现事务
回滚日志,提供
回滚
操作,记录某数据被修改前的值,可以用来在事务失败时进行rollback,保证事务一致性;同时也用于多版本并发控制
例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Undo日志为<T1, X, 5>
执行过程
假设有A、B两个数据,值分别为1,2。
前提条件:数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录B=2到undo log.
E.修改B=4.
F.将undo log写到磁盘。
G.将数据写到磁盘。
H.事务提交
特点:
A. 更新数据前记录Undo log。
B. 为了保证持久性,必须将数据在事务提交前写到磁盘。只要事务成功提交,数据必然已经持久化。
C. Undo log必须先于数据持久化到磁盘。如果在G,H之间系统崩溃,undo log是完整的,可以用来回滚事务。
D. 如果在A-F之间系统崩溃,因为数据没有持久化到磁盘。所以磁盘上的数据还是保持在事务开始前的状态。
缺点:
- 每个事务提交前将数据和Undo Log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。
- 如果能够将数据缓存一段时间,就能减少IO提高性能。但是这样就会丧失事务的持久性。
因此引入了另外一种机制来实现持久化,即Redo Log
Redo + Undo 实现事务
重做日志,提供
前滚
操作。记录某数据块被修改后的值,防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性
例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Redo日志为<T1, X, 15>
执行过程
假设有A、B两个数据,值分别为1,2
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录A=3到redo log.
E.记录B=2到undo log.
F.修改B=4.
G.记录B=4到redo log.
H.将redo log写入磁盘。
I.事务提交
特点
A. 为了保证持久性,必须在事务提交前将Redo Log持久化。
B. 数据不需要在事务提交前写入磁盘,而是缓存在内存中。
C. Redo Log 保证事务的持久性。
D. Undo Log 保证事务的原子性。
E. 有一个隐含的特点,数据必须要晚于redo log写入持久存储。
IO性能
Undo + Redo的设计主要考虑的是提升IO性能。虽说通过缓存数据,减少了写数据的IO。但是却引入了新的IO,即写Redo Log的IO。如果Redo Log的IO性能不好,就不能起到提高性能的目的。
为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:
A. 尽量保持Redo Log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配。以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
B. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时(如事务提交),将许多日志一起写入磁盘.
C. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,以减少日志占用的空间。例如,Redo Log中的记录内容可能是这样的:
记录1: <trx1, insert …>
记录2: <trx2, update …>
记录3: <trx1, delete …>
记录4: <trx3, update …>
记录5: <trx2, insert …>
D. 因为C的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
E. Redo Log上只进行顺序追加的操作,当一个事务需要回滚时,它的Redo Log记录也不会从
Redo Log中删除掉。
checkpoint
定期将内存缓存区的内容刷新到数据库磁盘文件。当遇到内存不足、缓存区已满等情况时,需要将内容/部分内容(特别是脏数据)转储到数据库磁盘文件中。在转储时,会记录checkpoint发生的”时刻“。在故障恢复时,只需要redo/undo最近的一次checkpoint之后的操作
多版本并发控制(InnoDB)
MVCC是通过保存数据在某个时间点的快照来实现的,它的实现原理主要是依赖记录中的 3个隐式字段(
DB_TRX_ID
、DB_ROLL_PTR
、DB_ROW_ID
),undo log
,read view
来实现的。
MVCC只在Repeatable Read
和Read Committed
两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为Read Uncommitted
总是读取最新的数据行,而不是符合当前事务版本的数据行。而Serializable
则会对所有读取的行加锁。
undo log
在操作任何数据之前,首先将数据备份到undo log中,然后进行数据的修改,所以undo log中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。undo logs分为:
insert undo log
和update undo log
insert undo log
:事务对insert新记录时产生的undolog, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。update undo log
:事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
隐式字段
DB_TRX_ID
(6字节)
(1)记录最后一次修改(insert|update)本行记录的事务id
(2)对于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除DB_ROLL_PTR
(7字节)
(1)指向写入回滚段(rollback segment
)的undo log record
(2)如果一行记录被更新, 则undo log record
包含 ‘重建该行记录被更新之前内容’ 所必须的信息
read view
read view主要是用来做可见性判断的,防止不该被事务看到的数据(例如还没提交的事务修改的数据)被看到。 当创建read view时,会拷贝
trx_sys->descriptors
到read view中(read_view_t->descriptors
)。read_view_t->up_limit_id
是read_view_t->descriptors
这数组中最小的值,read_view_t->low_limit_id
是创建read view
时的max_trx_id
trx_sys
:维护了一个全局的活跃的读写事务id(trx_sys->descriptors
),id从小到大排序。表示在某个时间点,数据库中所有的活跃(已经开始但还没提交)的读写(必须是读写事务,只读事务不包含在内)事务max_trx_id
:下一个事务ID。当创建一个事务时,会直接使用该值当作当前事务的ID,然后其自增repeatable read
:事务在begin/start transaction之后的第一条select读操作后, 会创建一个read viewrepeatable committed
:事务中每条select语句都会创建一个read view
可见性算法
假设需要读取的行的最后提交事务ID为
trx_id_current
,新事务id为new_id
,当前新开事务创建的read view
中最早的事务id为up_limit_id
,最晚的事务id为low_limit_id
trx_id_current < up_limit_id
:该行数据对新事务可见,转5trx_id_current >= low_limit_id
:该行数据对新事务不可见,转4up_limit_id <= trx_id_current < low_limit_id
:如果trx_id_current
在read_view_t->descriptors
中存在,则不可见,转4;否则,可见,转5- 从该行记录的
DB_ROLL_PTR
指针所指向的回滚段中取出undo log record
,赋值给trx_id_current
,转1 - 返回该行数据
案例分析
- MySQL8 设置会话隔离级别:
set session transaction_isolation='read-committed';
- MySQL8 查询会话隔离级别:
SELECT @@transaction_isolation;
Repeatable Read
Read Committed
IO缓存
内核缓存
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存。当数据写入文件时,内核通常会把数据写入其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为
延迟写(delayed write)
。
延迟写减少了磁盘读写次数,但是却降低了一致性,当系统故障时,可能会导致文件内容丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync
、fsync
和fdatasync
三个函数。
磁盘缓存
磁盘自身通常会有硬件缓存机制,对于写操作,有
write back
和write through
两种机制,前者将数据写至缓存就会返回,而后者则会将数据写到磁盘介质上。当使用write back机制时,fsync刷的文件数据可能只是写到磁盘缓存就返回了,导致从应用看来,写数据到磁盘的开销很小(实际上并未执行磁盘写操作);所以,使用write back机制时,即使上层应用显式fsync成功,数据也是可能丢失的,比如缓存里的数据还未刷到磁盘时掉电了,有些存储设备会使用备用电池(BBU,Battery backup unit)
来避免掉电时缓存数据丢失。
如果要保证fsync调用成功后,数据一定持久化到磁盘,则要使用内核的write barrier
机制。该机制通过在IO操作之前和之后显式刷新存储设备的缓存来达到目的,在文件系统mount的时候可以指定是否开启barrier机制,ext4
默认启用barrier机制。
IO函数
sync
:将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。fsync
:只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。fdatasync
:类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。fflush
:标准I/O函数(如:fread,fwrite)会在内存建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync。msync
:对于内存映射(mmap)的文件数据,msync的功能与fsync类似,将内存映射数据刷到磁盘,msync使用时有3个标志。
(1)MS_SYNC
:数据同步刷到磁盘后返回(可能只是写到磁盘缓存,而msync调用后数据是否一定持久化,则要看存储设备使用的缓存机制以及内核write barrier是否启用。)
(2)MS_ASYNC
:对于更新的文件数据会提交IO操作到底层,但不会等IO操作执行完成,而是立即返回
(3)MS_INVALIDATE
:更新文件对应的其它映射数据(如内存区域M1、M2都映射了文件F的数据,如果在msync M1的时候指定了该标记,则M2内存区域里的数据也会被更新)
Spring 事务使用
https://blog.youkuaiyun.com/ACMer_AK/article/details/78873683
https://blog.youkuaiyun.com/ACMer_AK/article/details/88390432
参考文献
高性能MySQL(第3版)
https://segmentfault.com/a/1190000012650596
https://www.jianshu.com/p/8845ddca3b23
https://blog.youkuaiyun.com/weixin_34405925/article/details/85948013
https://blog.youkuaiyun.com/zhouxinlin2009/article/details/89633464
https://blog.youkuaiyun.com/lhb0709/article/details/86077584
https://blog.youkuaiyun.com/u012414189/article/details/84036550