目录
事务概念
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务特性
原子性(Atomicity)
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
一致性(Consistency
)
数据库总是从一个一致性的状态转换到另外一个一致性的状态。如:拿转账来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。
隔离性(Isolation
)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 特点 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 所有事务都可以看到其他事务没有提交的修改,实际业务中很少使用 |
读已提交(Read Committed) | 不可能 | 可能 | 可能 | 只能看到其他已经提交的事务 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 可能 | 确保同一个事务在并发读取的时候,读到的结果是一致的。mysql默认的隔离级别 |
串行化(Serializable) | 不可能 | 不可能 | 不可能 | 串行读取数据,锁争取严重 |
不可重复读与幻读的区别是什么呢?
- 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的「数据不一样」。(因为中间有其他事务提交了修改);
- 幻读的重点在于新增或者删除:在同一事务中,同样的条件,第一次和第二次读出来的「记录数不一样」。(因为中间有其他事务提交了插入/删除)。
设置隔离级别的语法
# 查看事务隔离级别 5.7.20 之后
show variables like 'transaction_isolation';
SELECT @@transaction_isolation
# 5.7.20 之后
SELECT @@tx_isolation
show variables like 'tx_isolation'
修改隔离级别的语句是:set [作用域] transaction isolation level [事务隔离级别],
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。
其中作用于可以是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只针对当前回话窗口。隔离级别是 {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} 这四种,不区分大小写。
比如下面这个语句的意思是设置全局隔离级别为读提交级别。
mysql> set global transaction isolation level read committed;
Mysql事务隔离的原理
MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。
读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。所以根本谈不上什么隔离效果,可以理解为没有隔离。
再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
读已提交和可重复读,MySQL 采用了 MVCC (多版本并发控制) 的方式。我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
在很多人介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提
交则是每次执行语句的时候都重新生成一次快照。
持久性(Durability
)
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且「不可能有能做到100%的持久性保证的策略」否则还需要备份做什么。
MVCC原理
在讲解MVCC之前先了解两个概念:快照读和当前读
- 快照读:读取的是快照版本。普通的
SELECT
就是快照读。通过mvcc来进行并发控制的,不用加锁。 - 当前读:读取的是最新版本。
UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE
是当前读。
那么MySQL是如何避免幻读?
- 在快照读情况下,MySQL通过
mvcc
来避免幻读。 - 在当前读情况下,MySQL通过
next-key
来避免幻读(加行锁和间隙锁来实现的)。
MVCC的实现主要依赖两个概念:undo log(回滚日志) 和 read view。
undo log
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- DB_TRX_ID:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
Read view
Read view 是“快照读”sql执行时MVCC提取数据的依据,read view是一个数据结构,包含4个字段
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务编号集合 |
min_trx_id | 最小活跃事务编号 |
max_trx_id | 预分配事务编号,当前最大事务编号+1 |
creator_trx_id | read view创建者的事务编号 |
Read view 匹配条件规则如下:
- 如果数据事务ID
trx_id < min_limit_id
,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。 - 如果
trx_id>= max_limit_id
,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。 - 如果
min_limit_id =<trx_id< max_limit_id
,需腰分3种情况讨论
- (1).如果
m_ids
包含trx_id
,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。- (2)如果
m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;- (3).如果
m_ids
不包含trx_id
,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
MVCC推导过程
下面通过案例对上面的规则进行解释(其中读已提交和可重复读的实现都是按照上面的规则执行的,只不过两者生成的read view的时机不一样)
读已提交(RC):在每次执行快照读时生成一个全新的read view
unlog链如下:
MVCC的原理就是通过undo log链从上到下逐个按照上面提到的匹配规则进行匹配,直到匹配成功的那个,就是当前事务可以看到的快照版本。下面我们代入规则走一遍
在trx_id=4的事务中,第一次生成的read view中m_ids=[2,3,4],min_trx_id=2,max_trx_id=5,creator_trx_id=4,代入规则过程如下:
- 快照trx_id=3:min_trx_id<=3<max_trx_id,进一步判断了3在trx_id在m_ids中,但是不等于creator_trx_id,所以看不到快照3,因此需要进一步判断前一个快照
- 快照trx_id=2:min_trx_id<=3<max_trx_id,进一步判断了2在m_ids中,但是2不风雨creator_trx_id,所以还是看不到,需要继续判断前一个快照
- 快照trx_id=1:由于1<min_trx_id,表明生成该版本的事务在生成Read View前,已经提交,所以可以看到
最终结果就是当前事务4可以看到的是快照trx_id=1的信息,即name=‘A’
而trx_id=4的事务中,第二次生成的read view中m_ids=[2,4],min_trx_id=2,max_trx_id=5,creator_trx_id=4,代入规则过程如下:
- 快照trx_id=3:min_trx_id<=3<max_trx_id,进一步判断了3不在trx_id在m_ids中,说明这个事务在Read View生成之前就已经提交了,所以当前事务4可以看到事务3的快照,判断结束
最终结果就是当前事务4可以看到的是快照trx_id=3的信息,即name=‘C’、
可重复读(RR):仅在第一次执行快照读时生成read view,后续快照读复用c
在trx_id=4的事务中,第一次生成的read view中m_ids=[2,3,4],min_trx_id=2,max_trx_id=5,creator_trx_id=4,代入规则过程如下:
- 快照trx_id=3:min_trx_id<=3<max_trx_id,进一步判断了3在trx_id在m_ids中,但是不等于creator_trx_id,所以看不到快照3,因此需要进一步判断前一个快照
- 快照trx_id=2:min_trx_id<=3<max_trx_id,进一步判断了2在m_ids中,但是2不风雨creator_trx_id,所以还是看不到,需要继续判断前一个快照
- 快照trx_id=1:由于1<min_trx_id,表明生成该版本的事务在生成Read View前,已经提交,所以可以看到
最终结果就是当前事务4可以看到的是快照trx_id=1的信息,即name=‘A’
由于是可重复读,所以事务4中第二个select语句对应的read view就是第一次执行select的时候生成的,所以看到的结果也和第一次一样。
参考:
https://zhuanlan.zhihu.com/p/117476959
https://mp.weixin.qq.com/s/T8ZfheTdJmrfsaTsUOYNrg
https://juejin.cn/post/7016165148020703246