文章目录
一、mysql事务原理分析
1. 事务是什么?
技术背景
在数据库中,多个连接并发同时访问,容易引发数据混乱,需要事务来保证操作的完整性和正确性。
事务是指用户定义的一系列操作,这些操作要么都做,要么都不做,是一个不可分割的单位。
事务语句
- 显示开启事务
begin / start transaction
- 提交事务,并使得已对数据库做的所有修改持久化
commit
- 回滚事务,回滚到事务刚开始的状态
rollback
- 创建一个保存点,一个事务可以有多个保存点
savepoint identifier
- 删除一个保存点
release savepoint identifier
- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier
ACID特性(分析事物)
特性 | 含义 | 实现机制 |
---|---|---|
原子性(Atomicity) | 事务中所有操作要么全部完成,要么全部不完成,不可分割 | 通过 Undo log 实现回滚能力 |
一致性(Consistency) | 事务执行前后,数据库的完整性约束没有被破坏,数据保持一致 | 依赖约束,外键等规则强制实现 |
隔离性(Isolation) | 多个事务并发执行时,事物之间不互相影响 | 通过 MVCC 和锁机制实现 (4种隔离级别) |
持久性(Durability) | 一旦事务提交,其对数据库的修改应该永久保存,即使系统崩溃防止异常数据丢失 | 基于 Redo log 实现崩溃恢复 |
undo log 与 redo log
Undo Log(回滚日志)
作用:用来记录数据修改前的状态,用于事务回滚或 MVCC 的快照读
特点:InnoDB 引擎特有,保证事务原子性
Redo Log(重做日志)
作用:记录事务对数据页的修改,确保事务持久化
特点:顺序写入,高效持久化,崩溃回复时用于重建数据
*undo log 会随着事务提交后被清理掉,并不是永久保存
2. 事务隔离级别
技术背景 (并发异常导致引入事务)
在多事务并发执行时,若没有适当的隔离机制,可能会出现以下问题:
-
脏读: 一个事务读到另一个未提交事务修改的数据
-
不可重复读:同一事务多次读取同一数据返回不同结果
-
幻读:同一事务中两次查询同一个范围内记录的结果不一样; (当前读和快照读的不一致)
eg. 不可重复读
eg. 幻读
四种隔离级别
隔离级别(并发性向下越低) | ‘是否可能’ 有脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted 读未提交 | √ | √ | √ |
read committed 读已提交 | × | √ | √ |
repeatable read 可重复读 [mysql默认] | × | × | √ |
serializable 可串行化 | × | × | × |
隔离级别操作命令
1.read uncommitted (读未提交,三种异常都会发生)
2.read committed (读已提交,无脏读,读取最新版本的行数据)
3.repeatable read (可重复读,无脏读、不可重复读,读取事务开始前版本的行数据
innodb默认隔离级别,通过 读操作加锁也可解决幻读问题)
4.serializable (可串行化,无异常但并发能力低,通常不采用)
----------------------------------------
- 查看当前会话隔离级别
SELECT @@tx_isolation;
- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 设置全局隔离级别(需重启或新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
3. *MVCC 多版本并发控制
技术背景(引入mvcc原因)
1.MVCC 是 InnoDB 实现高并发的关键技术,通过保存数据的多个版本,使得读写操作可以并发执行而互不阻塞,保证读一致性(不加锁也能读到合理数据);
2.用来实现一致性的非锁定读;非锁定读是指不需要等待访问的行上X锁的释放
3.原理:通过undo log对每条记录保存多个版本,不同事务读取时根据自己的Read View(事务执行期间的快照)判断可见性。
*Undo Log 会随着事务提交后被清除(并不是永久保存)
Read View
属性 | 作用 |
---|---|
m_ids | 当前活跃的事务ID集合(还未提交的事务) |
min_trx_id | 当前活跃事务中最小的事务ID |
max_trx_id | 下一个即将分配的事务ID(并不一定是最大的事务ID) |
creat_trx_id | 每一行数据的创建事务ID(trx_id字段) |
事务可见性判断
事务可以看见事务本身的修改,事物间的修改不可见
- trx_id < min_trx_id (已提交 可见)
说明该记录在创建read view 之前已经提交,所以对当前事务可见; - trx_id >= max_trx_id (后启动,不可见)
说明该记录是在创建read view 之后启动事务生成的,所以对当前事务不可见 - min_trx_id <= trx_id < max_trx_id(需要判断是否在m_ids集合)
a. 在列表中;生成该版本记录的事务仍处于活跃状态,该版本记录对当前事务不可见;
b. 不在列表中;生成该版本记录的事务已经提交,该版本记录对当前事务可见;
不同隔离级别下MVCC行为
在 read committed 和 read repeatable 隔离级别下,MVCC 采用 read view 来实现的,它们的区别在于创建 read view 时机不同.
- read committed(读已提交):每次读取数据时生成新的read view,读到别人刚提交的数据(变化的快照)
- repeatable read(可重复读):启动事务时,生成新的read view,一直使用到事务提交或事务回滚为止,保证事务内读到的数据不变
快照读 vs 当前读的区别?
- 快照读(Snapshot Read):简单的sql select语句,读取记录的可见版本,非锁定读,走MVCC快照;
当前读(Current Read):DML操作以及 sql语句加读写锁操作 ,读取当前记录的最新版本,加锁读,拿最新版本; - 举例:share mode共享读锁、updata写锁排他锁
4. mysql锁机制
mysql支持多种 锁的粒度
- 表级锁:对整张表加锁,开销小但并发度低
- 页级锁:对数据页加锁(主要用于 MyISAM 索引缓存,InnoDB 不使用)
- 行级锁:对记录加锁,开销大但并发度高(InnoDB 支持)
常见锁类型
全局锁: 会锁定整个数据库,其他事务无法进行读写操作
FLUSH TABLES WITH READ LOCK; - 全局只读锁(用于一致性备份)
LOCK TABLES table_name WRITE; - 全局写锁(用于表结构变更)
表级锁 :MySAM采用
意向共享锁(IS):事务打算对表中某些记录加共享锁
意向排他锁(IX):事务打算对表中某些记录加排他锁
意向锁不会阻塞其他意向锁,但会与表级的共享或排他锁发生冲突
行级锁 :innodb使用MVCC + 行锁结合实现高并发控制
共享锁(S Lock):允许其他事务读取同一行,但阻止写入
排他锁(X Lock):阻止其他事务获取任何类型的锁
锁的算法
InnoDB 主要使用 三种行锁算法 来保证事务隔离性
-
记录锁(Record Lock)
锁定索引上的某一行记录,防止其他事务对该记录进行修改或删除。SELECT * FROM user WHERE id = 5 FOR UPDATE; - 若 id 有索引,这条语句会对 id=5 的那一条记录加记录锁
-
间隙锁(Gap Lock)
锁定某一范围之间的间隙,不锁定已有记录,防止其他事务在该范围内插入新记录,避免“幻读”。
特点:仅适用于 范围查询,例如 <, >, BETWEEN。SELECT * FROM user WHERE id < 5 FOR UPDATE; - 会锁住 (5, ∞) 范围的间隙,阻止插入 id > 5 的新记录。
-
临键锁(Next-Key Lock)
记录锁与间隙锁的组合,锁定记录及其前面的间隙,是InnoDB 默认的行锁算法
目的:在可重复读(RR)隔离级别下防止幻读。SELECT * FROM user WHERE id BETWEEN 5 AND 10 FOR UPDATE; - 会加多个临键锁,如: (4, 5],锁住 id=5 (5, 6],锁住 id=6 … (9, 10],锁住 id=10
5. 死锁
死锁的概念与产生原因
死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象;在 InnoDB 中,死锁一旦被检测到,会自动回滚其中一个事务来解除死锁。常见场景包括:
1. 相反加锁顺序:两个事务以不同顺序获取相同资源,互相等待对方释放;通过调整加锁顺序解决
【eg】 事务A 锁定资源X 并请求资源Y,同时事务B 锁定资源Y 并请求资源X
- 事务A
LOCK row 1;
LOCK row 2;
- 事务B
LOCK row 2;
LOCK row 1; - 等待A释放,造成死锁
2. 锁冲突导致死锁:一个事务未能及时释放锁,另一个事务等待同一资源,导致互相阻塞。常见于高并发写操作 + 高隔离级别
解决建议: 降低隔离级别至Read Committed,避免不必要的间隙锁。
减小锁粒度,避免大范围更新或范围查询
案例理解: mysql连接池的应用场景,只封装写事务
如何避免死锁
方法 | 说明 |
---|---|
统一加锁顺序 | 用一致的顺序加锁,避免环形等待 |
控制事务粒度与范围 | 缩小每个事务锁定的数据范围和执行时间 |
减少范围查询 | 减少 BRTWEEN/</> 查询,避免间隙锁干扰 |
合理设置索引 | 避免全表扫描导致不必要的行锁或表锁 |
设置死锁重试机制 | 捕获错误码 1213,程序自动重试一次事务操作 |
适当降低隔离级别 | 如使用 Read Committed 替代 Repeatable Read(锁冲突) |
优秀笔记:
1. MYSQL事务原理分析
2. MYSQL事务原理分析(三)
参考学习:https://github.com/0voice