前言
事务是MySQL
之类的关系型数据库的一个重要特性,是保证数据一致性的重要手段,这章我们来学习事务的基本概念,以及事务实现的相关原理。
基本概念
通常我们认为的Transaction
是一个最小的不可分割的单元,通常一个事务对应一个完整的业务。在技术面试中经常被面试官问道ACID
原子性 、一致性、隔离性、持久性。
-
原子性:指事务的最小单元,不可再分离,是一个整体。
-
一致性:事务中的方法要么成功,要么都不成功,比如A向B转账,要不都成功,要不都失败。
-
隔离性:指当多个事务操作数据库中的同一条或者多条记录时,对事务进行隔离开来有序执行,避免对同时对同一数据做操作。
-
持久性:即当成功插入一条数据库记录的时候,数据库必须要保证有一条数据永久的写入到数据库磁盘中。
事务的四大特征当中,所有的操作最后都会走向磁盘,所以持久性是事务操作的目的,原子性是事务实现的基础,隔离性是实现数据安全的一种策略、手段,最终维护的就是数据的一致性,一致性是事务中最重要的。隔离性是为了保证一致性的实现的手段跟策略。
ACID四大特征中,最难理解的不是一致性,而是事务的隔离性。
按照严格的要求标准,只有同时满足ACID特性才是真正的事务。但是在各大数据厂商的实现中,真正满足ACID的事务非常少。例如MySQL
NDB Cluster
事务不满足持久性和隔离性。InnoDB
默认事务级别是可重复读,不满足隔离性。Oracle默认的是事务隔离级别为READ COMMITED
,不满足隔离性.....与其说ACID是事务满足的条件,不如说是衡量事务的四个维度。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 | 示例 |
---|---|---|---|---|
READ_UNCOMMITED | 是 | 是 | 是 | |
READ_COMMITED | 否 | 是 | 是 | |
REPEATABLE_READ | 否 | 否 | 是 | MySQL |
SERIALIZABLE | 否 | 否 | 否 |
事务的实现
-
redo log
redo log 叫做重做日志,使用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后,会把所有修改信息都会存在该日志中。
MySQL
为了提升性能不会把每次的修改都实时同步到磁盘,而是会存到Buffer Pool
缓冲池里头,把这个当作缓存来使用,然后使用后台线程去做缓冲池和磁盘之间的同步。如果还没来得及同步的时候宕机了,会导致丢失部分已提交的事务修改信息!引入redo log 记录已成功提交事务的修改消息,并且会把redo log持久化到磁盘,系统重启自后再读取redo log 恢复更新数据。
-
总结: redo log是用来恢复数据用于保障已提交事务的持久化特性。
-
undo log
-
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log 主要记录的数据的逻辑变化,为了在发生错误的时候进行回滚之前的操作。需要之前的操作的都记录下来,然后在发生错误的时候进行回滚。
-
总结: undo log是回滚数据用于保障未提交事务的原子性。
-
假设有两个数值,分别为A和B,值为1和2。
-
start transaction;
-
记录 A =1 到 undo log;
-
update A =3 ;
-
记录 A=3 到 redo log;
-
记录B=2 到 undo log;
-
update B =4 ;
-
记录 B=4 到 redo log;
-
将 redo log刷新到磁盘;
-
commit;
在1-8步的任意一步宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响,如果8-9 之间宕机,恢复之后可以选择回滚,也可以选择事务继续,完成事务提交。因为此时redo log已经持久化。 如果在第9步,提交后宕机,那么可以在系统恢复后,根据redo log将数据刷回磁盘。
-
-
-
所以,redo log其实保障的是事务的持久性和一致性,而undo log则保障了事务的原子性。
-
锁&
MVCC
-
锁在上一章已经详细介绍过
-
当有多个请求来读取表中的数据的时候不采取任何操作,但是请求里面有读的请求,又有修改请求的时候,必须有一种措施来进行并发的控制,不然有可能造成不一致
-
-
为了解决上面的问题,
MySQL
引入了读写锁来解决这样的问题,读锁是共享的,不会造成堵塞;写锁是排他的,会一直阻塞,直到写入完成释放锁。 -
mvcc
InnoDB
的mvcc
,是通过在每行记录的后面保存两个隐藏的列来实现。这两个列,一个保存了行的创建时间,一个保存了行的过期时间。当然存储的并不是时间的值,而是系统的版本号。 摘抄于《高性能MySQL》
。主要的实现思想是通过数据多版本来实现读写分离,从而实现不加锁做到读写并行。MVCC
在MySQL
中的实现是依赖于 undo log & read view-
undo log: undo log 中记录某行数据的多个版本的数据
-
read view: 用来判断当前版本的数据可见性
-
Read View中存储了活跃的事务ID列表(m_ids)事务ID是事务开始时分配的,是由
InnoDB
分配的,大小可以决定事务的先后顺 序,因此可以通过ID的大小关系来决定版本的可见性
-
-
MVCC
实现原理
MVCC
读写锁的应用
-
MySQl
在存储的每列数据隐藏了三列-
DB_TRX_ID
6 byte, 创建这条记录/最后一次更新这条记录事务id -
DB_ROLL_PTR
7 byte ,回滚指针,指向这条记录的上一个版本,(存储于 rollback segment里) -
DB_ROW_ID
6 byte, 隐含的自增ID,如果数据表没有主键,InnoDB
会自动以DB_ROW_ID
产生一个聚簇索引。
-
-
另外每条记录的头信息里面(record header) 有一个专门的bit(delete flag)表示当前的flag是否删除。
-
记录的历史版本是存放在rollback segment里(undo log)
update 非主键语句的效果
-
历史记录被复制到rollback segment中形成 undo log ,
DB_TRX_ID
和DB_ROLL_PTR
不动 -
新记录的
DB_TRX_ID
= 当前事务ID,DB_ROLL_PTR
指向老记录形成的undo log -
这样就能通过
DB_ROLL_PTR
找到这条记录的历史版本。如果对同一条记录执行连续的update操作,新记录的undo log 会形成一个链表,遍历这个链表可以看到这条记录的变迁。
-
-
MySQL
的一致性读一致性读是通过read view 来实现的。
trx_id_t low_limit_id; // The read should not see any transaction with trx id >= this value. In other words, this is the "high water mark". 事务ID的最大值 trx_id_t up_limit_id; // The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the "low water mark". 事务ID的最小值
select操作返回结果的可见性是由以下规则决定的:
-
DB_TRX_ID
<up_limit_id
此记录的最后一次修改在read view 创建之前,可见 -
DB_TRX_ID
>low_limit_id
此记录的最后一次修改在read view 创建后,不可见,需要用DB_ROLL_PTR
查找undo log(此记录的上一次修改),然后根据undo log 的DB_TRX_ID
再次计算可见性 -
up_limit_id
<=DB_TRX_ID
<=low_limit_id
需要判断read view是否有DB_TRX_ID
-
如果当前read view 不包含
DB_TRX_ID
也就是改记录是在修改 read view 之前创建, 则是可见的 -
如果当前read view 包含
DB_TRX_ID
也就是该记录最后一次修改read view 还没有来得及保存,是不可见的。
-
-
如果该记录的delete_flag为true,则说明这条记录被删除了,不返回,如果为false,则可以安全返回。
-
-
隔离级别
RR RC RU
和MVCC
RR: read view 是在first touch read时创建,也就是执行事务中第一条select语句的瞬间,后续所有的select都复用这个read view,所以能保证每次读取的一致性。
RC:每次读取,都会创建一个新的read view,这样就能读取到其他事务已经commit的内容。
所以对
InnoDB
来说RR虽然比RC隔离级别高,但是开销反而相对较少。RU:相对来说实现简单,不用care read view,也不需要考虑
DB_TRX_ID
DB_ROLL_PTR
,直接读取最新的record就可以了。 -
二级索引与
MVCC
在二级索引中并不包含
DB_TRX_ID
和DB_ROLL_PTR
隐藏列,可以比较容易的实现mvcc
,二级索引一般只包含一个delete_flag.-
如果根据二级索引来查询,因为二级索引不维护版本信息,无法判断二级索引的可见性,还是需要回到聚簇索引中来查找记录
-
二级索引中有个
MAX_TRX_ID
记录最后一次更新事务的ID
如果当前语句关联的read_view的
up_limit_id > MAX_TRX_ID
,说明在创建read_view时最后一次更新二级索引的事务已经结束,也就是说二级索引里的所有记录对于当前查询都是可见的,此时可以直接根据二级索引的deleted flag来确定记录是否应该被返回。 -
-
Purge
delete 和 update 都不会直接删除原有的数据
delete from table where id =1
假设ID是主键索引,根据 undo log 的知识,MySQL
仅仅是将主键ID=1 的这一列数据的delete flag 设置为1,记录并没有被立即删除。真正的删除这行数据的时候 在purge操作完成。purge 用于最终完成delete和update操作,这样操作是因为
InnoDB
引擎支持MVCC
,所以记录不能在事务提交的时候立即处理。这个时候有可能有其他的事务正在引用这行。所以存储引擎需要保持记录之前的版本。否可以删除,由purge 来判断。 purge 会根据该行记录是否有事务的引用,来执行真正删除的逻辑。
mvcc
详细的实现原理参考:
https://blog.csdn.net/joy0921/article/details/80128857
数据库事务的相关命令
-
查看存储引擎
show create table
表名 -
更改存储引擎
alter table 表名 engine = new engine
; -
回滚:
rollback
. -
声明事务开始:
begin
; -
事务提交:
commit
; -
查询自动提交功能状态:
select @@autocommit
0是 off ,1 是 on; -
设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL