事务特性
A(Atomicity 原子性)
: 一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性- 原子性通过
Undo log
实现, 详情可见
- 原子性通过
C(Consistency 一致性)
: 事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。I(Isolation 隔离性)
: 事务的隔离性是指在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰- 隔离性则是通过本文重点讲述的MVCC机制
D(Durability 持久性)
: 一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中- 持久性通过
Redo log
实现,详情可见
- 持久性通过
隔离级别
Read Uncommitted
(RU: 可读未提交内容): 最低隔离级别,会造成脏读
问题Read Committed
(RC: 可读已提交内容): 事务过程中可以读到其他事物已经提交的事务,导致一次事务中的两次查询结果不同,造成不可重复读
Reapeatable Read
(RR: 可重复读,事务期间不能读取该期间其他事务的提交的结果):每次读取到相同的结果集,不管其他事物是否提交,造成幻读的问题
Serialiazble
(串行化): 事务排队,隔离级别最高,性能最差
并发问题
- 脏读: 读取到未提交的数据 (严重)
- 不可重复读: 一次事务中,两次读取结果不同。 事务A两次读取同一id的内容不同(改id内容在另外一个事务中被修改并提交)
- 幻读: 可重复读隔离级别的事务中,两次读取操作获得的数据条数不同。
- 如果两次都是快照读,则读取结果相同,因为mvcc机制,使一个事务中对一行数据的可见版本不变
- 如果两次都是当前读,则读取接口相同,因为当前读(select … for update)会加排他锁(间隙锁?),其他事务不能在当前事务select造成的锁内插入新数据,因此两次当前读结果也是相同
- 如果第一次使用快照读,第二次使用当前读,那么结果还是不同的。【这几条可以增加对mysql加锁机制的理解】
MySQL事务实现原理(默认RR可重复读)
-
MVCC
- 多版本并发控制
- 解决读写冲突问题 (一条记录更新,不影响其他事务对这条数据的读取,但是其他事务的读取可能不是最新的版本)
- 隐藏列
DB_TRX_ID
: 事务idDB_ROLL_PRT
: 回滚指针,指向前一个版本
- 当前读 - 读取当前最新生效版本,一次事务中的当前读不会考虑历史版。当前读场景:
- update语句
- select语句加锁
select ... for update
select ... lock in share mode
- 快照读 - 读取数据历史版本。快照读才考虑历史版本
- 可见性判断
- 创建快照(select语句)这一刻,还未提交的事务(RR级别这些事务看不到)
- 创建快照之后创建的事务(RR隔离级别,也看不到)
- Read View
- 快照读 活跃事务(还没提交事务)列表
- 列表中的最小事务id
- 列表中的最大事务id
- 可见性判断
快照读过程
select语句会查询出来一条记录(附带隐藏列事务id: DB_TRX_ID),那么这条select出来的记录是不是当前事务该看到的呢?就会有以下判断
- TRX_ID < 最小ID => 可见的,数据可用
- TRX_ID > 最大ID => 查询的记录是快照之后提交的,不可见,数据不可用,通过回滚指针往回找上一个版本,再经过上述可见性判断
- TRX_ID in 列表 => 查询记录时,这个事务还没提交,那么当前事务也看不到这个数据,需要通过指针回滚上一个版本,重复上述判断
通过这一些列判断,获取所有的当前事务可见的值
历史版本怎么查的呢? 肯定不能同时记录多个版本的值在数据库,需要Undolog
-
undo log
- 回滚日志(存在历史版本)
- 保证事务原子性
- 实现数据多版本
- delete undo log: 用户回滚,提交即清理
- update undo log: 用于回滚,同时实现快照读 不能随便删除(当前活跃版本可见之前的版本可删除)
-
redo log
- 实现事务持久性
- 记录数据记录的修改
- 用于异常恢复
- 循环写文件
- Write Pos: 写入位置
- Chick POS: 刷盘文职
- Chick Point -> Write POs: 待落盘数据
-
事务的启动和结束
begin/start transaction
并不是一个事务的起点。其后第一条操作innodb引擎的语句才真正开始了一个事务,创建一个视图(read-view)- 想马上启动一个事务,可以使用语句
start transaction with consistent snapshot
- 想马上启动一个事务,可以使用语句
mysql事务相关语句
- 事务自动提交
- 查看事务是否自动提交:
show variables like 'autocommit
- 设置事务是否自动提交:
set autocommit="OFF"/"ON"
(当前客户端生效)- 如果设置为非自动提交,那么任何一条语句都会开启一个事务(包括select),需要手动commit
- 查看事务是否自动提交:
- 查看当前运行的所有事务
select * from information_schema.innodb_trx\G
Redo log 和binlog
redo log
- redo log是innodb引擎的特色(其他引擎没有)
- redo log是物理log,记录mysql某个数据页改变了什么内容
- redo log是固定大小的(循环写)。redo log起作用的过程
- 用户提交update语句
- 引擎收到命令,先把变更写入redo log
- 然后更新数据内存(此时更新操作就算完成了)
- redo log 是crash safe的
binlog
- binlog是mysql server的功能,和引擎无关,它记录更新语句的原始逻辑,如给哪行哪个字段加1
- binlog是追加写的,无磁盘大小限制(在机器磁盘范围内)
思考
-
redo log落入磁盘是实时的吗?如果是,那每条更新记录也需要一个io,和直接更新mysql 数据项有什么区别呢?如果不是,那redo log如何做到crash safe?
先说结论。基本是实时的,每次事务commit都会讲redo log写入磁盘。之前理解的也对,redo log 的落盘是磁盘顺序io,比直接更新mysql数据文件(随机io)要快的多。
更细致的理解(之前低估了redo log在mysql中的地位), mysql中有一个参数
innodb_flush_log_at_trx_commit
控制每次提交事务时,innodb引擎操作redo log的行为innodb_flush_log_at_trx_commit = 0
: 事务提交时,不主动将redo log数据(日志数据在内存中,redo log buffer),等待主线程定时刷入磁盘innodb_flush_log_at_trx_commit = 1
: 事务提交时,必须将redo log内容落盘。这里包含两个步骤,一是将redo log 内容刷入操作系统缓存(操作系统"延时写"特性),同时会调用系统fsync()命令,将数据从os缓存持久化到磁盘innodb_flush_log_at_trx_commit = 2
: 事务提交时,redo log的内容会被更新到os缓存,但不会主动持久化到磁盘,而是交给操作系统自己去管理(每秒一次持久化操作)
可以看到
0 -> 2 -> 1
,数据准确性依次提高,与此对应,性能依次下降。mysql默认1 -
在RR隔离级别下,对于同一条记录,事务a 读到值为1,事务b读到值为2,在两个事务中同时需要修改这条记录,会产生什么行为?造成什么结果?
这里就需要考虑两阶段锁协议,在一个事务中,锁在使用时创建,只有在事务commit后释放,而不是update完就释放。所以,当不存在两个事务同时更新的情况,一个事务update开始,另外一个事务的update即开始阻塞,只有前面的update更新完,阻塞的update才能继续进行
-
接上一个问题,如果事务a中读到值为1,它需要+1,那么它是在1的基础上加一,即结果为2;但是,事务b已经将结果更新为2。这种情况事务a会把事务b的结果覆盖
这个问题存在两种情况
-
事务a,是先select 得到1,然后本地加1得到2,接着update 2,这种情况似乎没有什么办法,因为select操作没有读到事务b的更新
做了一个实验,和上面的预想略微有些差别。在事务a中select k = 1, 在事务b中update k = 2 并且commit了,此时若还在事务a中update k = 2,再select k,发现值还是为1;(猜测是,在事务a中update是当前读,发现该行记录的最新值已经是2,就不在进行更新操作,即没有创建新版本。事务a中的update k = 2表面上成功,实际上什么也没干,所以在当前事务a中select,还是读到老版本;下面update 其他值可见,是因为底层确实执行了update,更新了最新版本,而且最新版本自己可见); 若在事务a中update k = 3…(不为2),在select则为更新的值
-
事务a,使用update k = k +1 这种计算,此时的update会继续一个当前读,那么更新的结果就是在事务b计算结果的基础上。看起来这种操作才是更合理的做法
-
更新
Remember that even a SELECT statement opens a transaction, so after running some report or debugging queries in an interactive mysql session, either issue a COMMIT or close the mysql session.
- Innodb引擎中,除了显示使用
begin
或者start transaction
可以开启一个事务,其他单语句,甚至是简单的select, 也会开启一个事务。
- Innodb引擎中,除了显示使用