🎹 InnoDB事务
2022年6月9日
- db
🎹 InnoDB事务
事务 [
Transaction
]:一系列操作,要么全部成功,要么全部失败,不允许出现中间状态
1. 四大特性
1) 原子性[Atomicity]
- 事务的最小工作单元,一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,
- 若事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
- InnoDB 通过
undo log
&redo log
保证
2) 一致性[Consistency]
- 事务开始和结束后,数据库的完整性不会因为事务的执行而受到破坏
- 是目的,原子性 + 隔离性 + 持久性 共同作用保证了一致性
- InnoDB 通过 undo log 保证
3) 隔离性[Isolation]
- 数据库允许多个并发事务同时对其数据进行读写和修改的能力
- 不同事务之间互不影响,MySQL 提供
四
种隔离级别 - InnoDB 通过
MVCC
&锁
保证
4) 持久性[Durability]
- 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
- InnoDB 通过
redo log
保证
2. 并发下容易产生的问题
dirty read
]
1) 脏读 [- 场景:
- 事务A 读到了 事务B 修改过的但没有提交 的数据,此时若 事务B 发生了回滚,那么 事务A 之前读到的数据就是过期的脏数据
non-repeatable read
]
2) 不可重复读 [- 场景:
- 在一个事务中,发生多次读取同一数据的情况,每次读取同一数据时出现数据不一致的情况
- 侧重于
update
操作
phantom read
]
3) 幻读 [- 场景:
- 在一个事务中,发生多次读取符合某种查询条件的记录数据,前后读取出现记录数据数量不一致的情况
- 侧重于
insert
|delete
3. 隔离级别
查看事务的隔离级别命令: [MySQL 8.0.26]
SELECT @@transaction_isolation
修改事务的隔离级别命令:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
1) 读未提交 [Read Uncommitted]
- 一个事务还没提交时,它做的变更就能被其他事务看到
2) 读提交 [Read Committed]
- 一个事务只有被提交了,它的变更才能被其他事务看到
- 可通过
MVCC
保证 - 只会对索引增加
Record Lock
,不会添加Gap Lock
和Next-Key Lock
,并发度要比 RR 高很多 - 只支持
row
格式的binlog
,若指定了mixed
作为 binlog 格式,在RC下,服务器会自动使用基于row
格式的日志记录
3) 可重复读 [Repeatable Read]
- 一个事务执行过程中,不论什么时刻读到的统一数据都是一致的
- 可通过
MVCC
保证,无需加锁影响性能 - InnoDB 默认隔离级别
- 通过
next-key lock
锁住记录之间的“间隙” & 记录本身,防止其他事务在这个记录之间插入新的记录,可以避免幻读,但锁的粒度变大,死锁的概率也随之增大 - 同时支持
statement
、row
以及mixed
三种
4) 串行化 [Serializable]
- 多个事务对同一记录进行读写操作时,若发生读写冲突,则相关事务会串行执行,必须前面的事务执行完成,当前事务才能执行
- 性能最差,却最安全
- 通过加锁保证
不同隔离级别下可能出现的并发问题:
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
RU | 🐠 | 🐠 | 🐠 |
RC | 🐋 | 🐋 | |
RR | 🐙 | ||
Serial |
4. MVCC
1) 底层原理
聚簇索引中 2 个隐藏列
- 事务 id [
trx_id
]6
字节,每次递增 1- 当一个事务对某条聚族索引记录进行改动时,就会把该事务的事务 id 记录在
trx_id
隐藏列里
- 回滚指针 [
roll_pointer
]7
字节- 每次对某条聚族索引记录进行改动时,都会把旧版本的记录写入到
undo log
中 roll_pointer
指向这个旧版本记录
Read View
4 个字段
creator_trx_id
- 创建该 Read View 的事务的事务 id
m_ids
- 创建 Read View 时当前数据库中活跃且未提交的事务 id 列表
min_trx_id
m_ids
中最小事务 id 的事务 id
max_trx_id
- 创建 Read View 时当前数据库中应该给下一个事务分配的事务 id
2) 针对 RC & RR 的不同实现
RC 的实现
- 在事务执行过程中,每次查询操作都会生成一个新的
Read View
,只能解决脏读,会出现 不可重复读 - 在事务期间读取数据的时候,在找到数据后,先会将该记录的
trx_id
和该事务的 Read View 里的字段做个比较:- 若
trx_id
<=creator_trx_id
&&trx_id
not inm_ids
- 说明这条记录的事务早就在该事务前提交过了,或是本事务自己修改的,该记录对该事务可见
- 若
trx_id
>creator_trx_id
&&trx_id
not inm_ids
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,但该事务不在
m_ids
列表里,说明已提交,则该记录对该事务可见
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,但该事务不在
- 若
trx_id
>creator_trx_id
&&trx_id
inm_ids
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,但该事务还在
m_ids
列表里,说明还未提交,因此沿着undo log
链条往下找旧版本的记录,直到满足上面 2 条情况
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,但该事务还在
- 若
RR 的实现
- 启动事务时生成一个
Read View
,然后整个事务期间用同一个初始创建的 Read View - 在事务期间读取数据的时候,在找到数据后,先会将该记录的
trx_id
和该事务的 Read View 里的字段做个比较:trx_id
<=creator_trx_id
&&trx_id
not inm_ids
- 说明这条记录的事务早就在该事务前提交过了,或是本事务自己修改的,该记录对该事务可见
- 若
trx_id
>creator_trx_id
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,此时不读取该条记录,而是沿着
undo log
链条往下找旧版本的记录,直到找到trx_id
<= 该事务 id 的第一条记录
- 说明该事务读到的是和自己同时启动的另外一个事务修改的数据,此时不读取该条记录,而是沿着
3) 优点
MVCC
使得数据库普通读不会对数据加锁,普通的 SELECT 请求不会加锁,提高了数据库的并发处理能力- 普通读:
select * from chap where id = 22
- 当前读 [会加锁]:
select * from chap where id = 22 lock in share mode
select * from chap where id = 22 for update
5. 三大日志
WAL: 所有的修改都先被写入到日志中,然后再写磁盘
1) binlog
- 是什么?
- 记录数据库 表结构 & 表数据 的变更,例如 create | truncate | delete | alter | update | insert,不会记录 select
- 是什么样式?
- 逻辑日志
statement
- 存储 sql 语句 + 事务 id + 执行时间
- 可能造成数据主从的不一致,例如使用了
now()
uuid()
函数等 - RR 隔离级别下支持
row
- 将 sql 语句做的修改转换成具体数值 + 事务 id + 执行时间
- 内容更长,体积更大,同步 + 恢复时 IO 消耗更大,需要借助 binlog 工具解析,但是结果准确
mixed
混合模式,MySQL会判断这条 SQL 语句是否可能引起数据不一致,如果是,就用 row 格式,否则就用 statement 格式
RR 隔离级别支持,RC 只能全部都是 row 格式
- 存在于哪些存储引擎中?
- MySQL 所有存储引擎均支持
- 什么情况下会用到?
- 数据恢复 & 主从复制等数据同步操作
- 什么时候进行写入?
- 事务提交时进行记录,写入成功说明该事务已经进入
commit
状态
- 刷盘时机?
- 参数:
sync_binlog
- 不同值下的策略:
0 : 写入文件系统,异步刷盘
1 : 同步刷盘,提交事务就会刷盘
N [N > 1] : 累积 N 个事务后刷盘
- 文件大小?
参数:
max_binlog_size
可指定大小大小达到上限,会生成一个新文件继续写入
- 借助的内存空间?
binlog cache
:每个线程独有查看单个线程 binlog cache 的大小指令:
binlog_cache_size
,超过则会暂存磁盘
- 过期清理?
参数:
binlog_expire_logs_seconds
2) redo log
- 是什么?
- 记录在某个页上做了什么修改
- 是什么样式?
- 物理日志,xxxx 页做了 xxx 修改
- 表空间号 + 数据页号 + 偏移量 + 修改数据长度 + 具体修改的数据
- 存在于哪些存储引擎中?
- MySQL InnoDB 独有
- 什么情况下会用到?
- 数据库的崩溃恢复
- 当我们修改数据时,对内存数据进行了修改,但数据还未真正写到磁盘,此时若数据库挂了,就可以通过 redo log 对数据进行恢复
- 什么时候进行写入?
prepare
: 事务开始时,redo log 记录变更信息,执行过程中也不断记录,表明事务进入prepare
状态commit
: binlog 写入成功,事务进入 commit 状态, prepare ==> commit
- 刷盘时机?
- 参数
innodb_flush_log_at_trx_commit
- 不同值下的策略:
0
:异步刷盘,后台每隔 1秒 刷盘,数据库挂掉则最多有 1秒 的数据丢失1
:每次事务提交都进行刷盘 [default],只要事务提交成功则不会有任何数据丢失2
:写入文件系统缓存,异步每隔 1秒 刷盘,数据库挂掉不会有任何数据丢失 & 文件系统挂掉最多有 1秒 的数据丢失
- 特殊情况:
当 redo log buffer 占用空间 > innodb_log_buffer_size 总空间一半时,后台线程也会主动刷盘
- 文件大小?
- 固定 ==
4G
,环形数组形式 - 2 个重要属性:
write pos
checkpoint
: 如果write pos
追上checkpoint
,表示日志文件组满了,这时候不能再写入新的redo log
,此时会出现 MySQL 偶尔变慢的情况
- 借助的内存空间?
redo log buffer
两阶段提交
- 示意图:
- 作用:
- 保证 redo log & binlog 数据的一致性,若是主从复制则主库 & 从库 数据一致
- 判断逻辑:
- prepare 阶段,redo log 写入成功,查看 binlog 状态
- undo log 完成 ==> 事务执行
- undo log 不完整 ==> 事务回滚
- commit 阶段
- binlog 肯定完成 ==> 事务执行
- prepare 阶段,redo log 写入成功,查看 binlog 状态
3) undo log
- 是什么?
- 记录数据的历史版本
- 是什么样式?
- 逻辑日志
- insert 语句对应 undo log 的 delete 日志;update 语句对应 undo log 的 相反 update 日志;delete 语句对应 undo log 的 insert 日志
- 存在于哪些存储引擎中?
- MySQL InnoDB 独有
- 什么情况下会用到?
- 事务失败后的回滚操作
- MVCC
- 文件大小?
- 参数:
innodb_max_undo_log_size
6. 其他日志
error log
- 对 MySQL 的启动、运行、关闭过程进行了记录,可以帮助定位 MySQL 问题
slow query log
- 慢查询日志
- 记录执行时间超过
long_query_time
变量定义的时长的 查询语句
general log
- 记录所有对 MySQL 数据库请求的信息,无论请求是否正确执行