MySQL事务以及MVCC原理
数据库事务及四大特性
数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized
同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
- **原子性(Atomicity)😗*事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
- **一致性 (Consistency)😗*事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论事务执行成功否,参与转账的两个账号余额之和应该是不变的。
- **隔离性(Isolation)😗*并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
- **持久性(Durability)😗*事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
事务隔离级别操作
查看/修改MySQL的隔离级别
- 查询全局事务隔离级别:
SELECT @@global.tx_isolation
- 查询当前会话事务隔离级别:
SELECT @@session.tx_isolation
SELECT @@tx_isolation
可以发现「全局事务」和「当前会话事务」隔离级别都为**可重复读
**
- 修改MySQL隔离级别:
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
关键字解释:
- SESSION:当前会话
- GLOBAL:全局会话
- READ UNCOMMITTED:读未提交
- READ COMMITTED:读已提交
- REPEATABLE READ:可重复读
- SERIALIZABLE:串行化
例如:设置当前会话事务的隔离级别为「读未提交」
SET SESSION TRANSACTION IOSLATION LEVEL READ UNCOMMITTED
事务开启提交回滚
查看/设置事务自动提交模式
查看事务自动提交模式:SHOW VARIABLES LIKE 'autocommit';
autocommit 的值是 ON,表示系统开启自动提交模式
设置事务自动提交模式:SET autocommit = 0|1|ON|OFF;
- 值为 0 和值为 OFF:关闭事务自动提交。如果关闭自动提交,用户将会一直处于某个事务中,只有提交或回滚后才会结束当前事务,重新开始一个新事务。
- 值为 1 和值为 ON:开启事务自动提交。如果开启自动提交,则每执行一条 SQL 语句,事务都会提交一次。
开启/提交/回滚事务
开启事务:
begin
/start translation
:执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机start transaction with consistent snapshot
:执行了 start transaction with consistent snapshot 命令,就会马上启动事务
提交事务:
commit
回滚事务:
rollback
使用 BEGIN 或 START TRANSACTION 开启一个事务之后,自动提交将保持禁用状态,直到使用 COMMIT 或 ROLLBACK 结束事务。之后,自动提交模式会恢复到之前的状态,即如果 BEGIN 前 autocommit = 1,则完成本次事务后 autocommit 还是 1。如果 BEGIN 前 autocommit = 0,则完成本次事务后 autocommit 还是 0。
事务并发产生的问题
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
例如:事务A事务读到事务B修改的数据,事务B还未提交到数据库,若后续事务B发生回滚,那么事务A读到此数据的事务后面操作此数据都是不正确的,这也就是脏数据
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
例如:事务A读到事务B提交之前的数据,然后事务B对该数据修改后提交事务,事务A再次读读取该数据后发现两次读取到的数据不一致。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就好像发生了幻觉一样,所以称为幻读。
例如:在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。
事务隔离级别分类
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | √ | √ | √ |
读已提交(read-committed) | × | √ | √ |
可重复读(repeatable-read) | × | × | √ |
串行化(serializable) | × | × | × |
读未提交(READ UNCOMMITTED)
在一个事务中可以读到另一个事务未提交的数据
产生
脏读
、不可重复读
、幻读
读已提交(READ COMMITTED)
在一个事务中可以读到另一个事务中已经提交的数据
避免
脏读
产生
不可重复读
、幻读
事务T1不能够读取到T2未提交修改的数据。读已提交能够避免脏读。但是不能保证可重复读。
可重复读(REPEATABLE-READ)
可重复读为InnoDB默认的数据库隔离级别,在一个事务中不能够看到另一个事务未提交和已提交的事务,保证多次读取数据一致。
在事务开启的时候,对当前数据库拍一个快照,默认SELECT
语句每次从快照中读取数据,从而保证每次读取到的数据一致,这种方式也叫快照读
避免
脏读
和不可重复读
可能产生
幻读
,在某些情况下会产生幻读。
幻读出现的情况
可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读。例如以下两种情况:
情况一:
事务B执行后将B事务id存到该记录。接着事务A执行更新该条数据时将事务A的事务id存到该记录,所以事务A再次查询时能够看到数据
- 事务A执行:
select * from user where id=5;
结果为空 - 事务B执行:
insert into user values(5, '张三');
然后提交事务 - 事务A执行:
update user set name='李四' where id=5
- 事务A执行:
select * from user where id=5
可以查询出id为5,name为李四的结果
情况二:
默认使用快照读数据后,再使用加X锁/S锁读方式变为当前读,即可以读到其他事务新增提交后的数据
- 事务A进行
普通SELECT查询
,得到一个范围。 - 事务B对该范围中增加一条数据并提交
- 事务A使用SELECT加X锁/S锁的方式改为当前读,可以看到事务B新增的数据。
可重复读下解决幻读的方式
在可重复读的情况下事务开启时即手动对SELECT语句加X/S锁
的方式,使快照读
变为当前读
,产生临键锁(记录锁+间隙锁)
来阻塞其他事务对范围数据内的数据添加,从而避免幻读。
其他方式:
将隔离级别改为SERIALIZABLE
或者加表锁
(效率低,不推荐)
串行化(SERIALIZABLE)
会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
避免
脏读
、不可重复读
、幻读
。
性能关系
MVCC原理
undo log
undo log即回滚日志,当在事务中,每个INSERT操作时,就会生成一条相反的DELET语句的undo log日志。每个DELETE操作时,也会生成一条修改为原始值的UPDATE语句的undo log日志。
undo lgo日志包含数据id,事务id等信息,方便事务回滚的时候找到该条日志后执行相应的回滚语句,从而实现事务回滚。
行格式
行格式就是 InnoDB 在保存每一行的数据的时候,究竟是以什么样的格式来保存这行数据的。
一般一行数据会有以下隐藏的数据列:
上图中的列 1、列 2、列 3 一直到列 N,就是我们数据库中表的列,保存着正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这里要重点关注的 DB_ROW_ID
、DB_TRX_ID
、DB_ROLL_PTR
三列:
DB_ROW_ID
:该列占用 6 个字节,是一个行 ID,用来唯一标识一行数据。如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引。DB_TRX_ID
:该列占用 6 个字节,是一个事务 ID。在 InnoDB 存储引擎中,当要开启一个事务的时候,会向 InnoDB 的事务系统申请一个事务 id,这个事务 id 是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务 id 记录在当前行中。DB_ROLL_PTR
:该列占用 7 个字节,是一个回滚指针,这个回滚指针指向一条 undo log 日志的地址,通过这个 undo log 日志可以让这条记录恢复到前一个版本。
MVCC基本概念
MVCC(Multi-Version Concurrency Control):多版本并发控制
MVCC实现读已提交
和可重复读
来尽量避免出现的并发问题,它们是通过 Read View(快照读) 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把
Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
- 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,也就不能保证可重复读和幻读
- 「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View,所以能保证可重复读,但是不能避免
一些情况的幻读
MVCC 的核心思路就是保存数据行的历史版本,通过对数据行的多个版本进行管理来实现数据库的并发控制。
Read View工作原理
当执行INSERT/UPDATE/DELETE
操作数据的时候,会将本事务id
保存在数据行的DB_TRX_ID字
段中,也就是哪个数据行被哪个事务修改都会有记录
并且执行INSERT/UPDATE/DELETE
操作数据的时候都会产生相应的undo log回滚日志,并将回滚日志地址存储在数据行的DB_ROLL_PTR
的字段中,且每次修改都会执行上一个记录的版本,所以每次执行undo log就会将数据恢复到上一个版本。
Read View结构:
creator_tex_id:当开启事务的时候,会向InnoDB事务系统中
申请一个事务id
,并且事务id是一个严格按照事务申请的先后顺序递增的一个数字
。m_ids:在事务开启的时候系统会创建一个事务
数组
,数组
中包含所有活跃的事务id,也就是开启了事务但是还没提交的事务。min_trx_id:活跃事务列表中最小的事务id
max_trx_id:这个并不是活跃事务中的最大值,而是创建 Read View 时,目前最大事务id+1,即下一个将分配的事务id
从申请到 trx_id 到创建数组之间也是需要时间的,这期间可能有其他会话也申请到了 trx_id。
每次开启Read View
后,查询数据行时,会一直会沿着当前最新数据行的事务id
和undo log地址
向上查找,直到查询出满足的版本链数据或者返回为空为止。
行数据事务id在Read View中出现的情况而造成的数据可见性问题如下:
如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。
如果这个值
小于活跃事务中的最小id
,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行是可见的。行数据事务id:5 当前事务:6 活动事务:[6,7] 当前事务开启时,id为5的事务已提交,所以可见。
如果这个值
大于等于事务最大id
,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时该行数据储存的就是修改改行的事务id,所以当前事务对此行数据不可见。行数据事务id:8 当前事务:6 活动事务:[6,7] 开启当前事务后,发现后面开启的事务id为8的事务已经对改数据进行修改了,所以不可见
如果这个值的
大于活跃事务最小id
并且小于最大事务id
该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。
行数据事务id:5 当前事务:6 活动事务:[4,6] 开启当前事务后,其他事务对其修改的已提交,可见
该值在数组中,说明这是一个未提交的事务修改的数据,不可见。
行数据事务id:5 当前事务:6 活动事务:[4,5,6] 开启当前事务后,其他事务对其修改的未提交,不可见
读已提交和可重复读区别
- 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
- 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。