Mysql常见问题汇总(2)- 事务&锁篇
1 事务的基本特性ACID
- 原子性
- 一个事务中的所有操作是一个整体,是不可再分割的。要么全部成功,要么全部失败
- 通过Undo log保证,撤销已经执行的事务
- 一致性
- 事务的数据状态只会从一个一致性状态转移到另一个一致性状态,通常是一个符合数据规则的状态转移到另一个数据规则的状态
- 例如转账系统:
- A给B 转账 200 块钱,A 有200,B没有钱
- 转账前 A和B的总钱数是200,转账后还是200.
- 隔离性
- 一个事务中的操作,在最终提交之前对于另一个事务来说是不可见的。(这里就牵扯到了不可见的程度是什么样的,也就是所谓的隔离级别)
- 通过mvcc + 锁机制保证
- 持久性
- 事务一旦提交,修改就会永久保存在数据库中
- 通过redo log 刷盘持久化,在innodb中,redo log 写盘,事务进入prepare 状态,如果prepare成功,binlog写盘,再继续将事务日志持久化到binlog中,如果持久化成功,事务进入commit状态,就会在redo log里面写一个commit记录(也就是说redo log 中commit标志能确保事务的成功)
2. 事务的隔离级别
- 读未提交(Read Uncommit):可能会读到别的事务未提交的数据,也就是脏读
- 读已提交 (Read Commit):也叫不可重复读,能读到别的事务已提交的数据,但是存在不可重复读问题
- 可重复读(Repeatable Read):可重复读,Mysql默认隔离级别,解决了不可重复读的问题,在Innodb中,还通过了mvcc + 间隙锁机制解决了幻读的问题。
- 串行化(serializable)
3. 事务并发可能产生的问题
- 脏读
- 脏读指的是,一个事务能读取到其他事务未提交的修改,是非常严重的问题。
- 在read uncommit隔离级别下是存在这个问题的
- 不可重复读
- 不可重复读指的是,在一个事务中,事务先是读到某个数据,后来因为其他事务提交了对这个数据的修改,导致事务读的数据和之前的数据不同。
- 不可重复读在read commit(RC)隔离级别下是存在的
- 不可重复读在repeatable read(RR)隔离级别下是不存在的
- 幻读:
- 幻读指的是原先读到这个范围有10个元素,再之后读读到了11个元素。 同一个事务内,同一范围查询,前后结果行数不一样(多了 / 少了)
- 在Mysql的RR隔离级别下是可以被解决了的,但是如果混合使用当前读(加锁) 和mvcc快照读,还是会出现幻读问题。
4. 多版本并发控制(MVCC)
MVCC通过undo log 和ReadView两大机制实现的。
MVCC 只在 RC(读已提交)和 RR(可重复读)下存在。
数据中存在两个关键的隐藏字段 trx_id,roll_pointer,在undo log中,trx_id会记录上次修改当前记录的事务ID,而roll_pointer回滚指针会指向上一个修改前的版本,通过roll_pointer实现了undo log 中的版本链
readview是快照读的产物,用来判断当前事务能看到版本链上哪个版本的数据
readview中存储了
- m_ids:生成readview时,当前未提交的事务ID集合
- creator_trx_id:创建readview的事务id
- min_trx_id: 最小的未提交事务id
- max_trx_id: 下一个要分配的事务ID
在事务中,根据mysql的隔离级别决定每次快照读是否会产生新的readview,还是复用第一次产生的readview
当进行一次快照读时,从最新版本数据的事务id对比readview中的数据
- 如果最新版本数据的事务id是创建readview的事务id,说明数据更新是在本次事务中做的,可以看到
- 如果最新版本数据的事务id小于最小的事务ID,说明修改该数据的事务已经提交过了,也可以看到当前版本
- 否则(也就是最新版本数据事务id在未提交事务ID集合中,或者大于max_trx_id),则通过roll_pointer找到上一个历史版本,再进行比对
为什么MVCC能在RR下解决幻读和不可重复读问题?
- 不可重复读在read commit(RC)隔离级别下是存在的,本质原因是MVCC机制在RC的操作是,每次快照读都会产生新的readview,就会读到新的数据了
- 不可重复读在repeatable read(RR)隔离级别下是不存在的,还是因为MVCC机制,在RR中的第一次快照读中产生readview,之后的每次快照读都复用之前的readview,这样就看不到新事务的提交了
- 而幻读问题,在RR的隔离级别下,MVCC确保了事务不会读到新增的数据,幻读问题也就消失了, 注意如果混合使用快照读和当前读,还是会出现幻读问题。
rr隔离下 先快照读
select * from t where a > 10
再加锁读(这个时候另一个事务修改了a> 10之后的数据)
select * from t where a > 10 for update
就会出现幻读问题,所以避免使用这两者组合
5. 当前读和快照读是什么
快照读 = 读历史版本,不加锁
- 快照读就是普通的select语句
SELECT * FROM user WHERE id = 1;- 读取的是undo log中的历史版本
- 不加任何锁
- 利用MVCC + ReadView实现
当前读 = 读最新版本,加锁
-
必须读 最新的数据
-
会加锁(行锁、临键锁 Next-Key Lock)
-
不读历史版本,直接读最新数据
-
会阻塞其他事务
SELECT * FROM user WHERE id = 1 FOR UPDATE;
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
UPDATE user SET name = 'a' WHERE id = 1;
DELETE FROM user WHERE id = 1;
INSERT ...
6. 锁的类型
-
根据锁的粒度分类:
- 全局锁:全局锁是对整个数据库实例加锁,常见场景是全库逻辑备份,此时库处于只读状态
- 表锁:锁的是整个表,粒度大,加锁简单,容易冲突。
- 行锁:锁的是表的某一行或多行记录,粒度小,支持并发高, innodb支持行锁,myisam不支持。
- 页锁:锁住相邻的一组记录
- 记录锁:属于行锁的一种,记录锁的范围只是表中某条数据
- 间隙锁:在RR隔离级别中生效,间隙锁是锁住一段区间,间隙锁能很好解决在RR隔离级别下幻读的问题。
- 比如表中主键数据是1,4,5,7,10,就会分成 (-∞,1),(1,4),(4,5),(5,7),(7,10),(10,+∞)这些区间
- 通过访问某个值,范围查找,查询不存在的值都能产生间隙锁,锁的是当前数值前面那个间隙
- 比如
select * from t where id = 5 for update,会锁住(4,5)这个区间 (注意,必须是当前读,使用了for update) select * from t where id between 5 and 10 for update会对(4,5),(5,7),(7,10)这些区间上锁
- 比如
- 临键锁:间隙锁 + 记录锁,通常是左开右闭区间,临键锁会把当前记录和记录的前面间隙都上锁,做法和间隙锁一样,也就是间隙锁 + 记录锁的组合。
-
根据锁的属性分类
-
共享锁:又称为读锁,是读取操作创建的锁,共享锁不会阻塞其他读操作的共享锁,但是会阻塞排他锁,只有当所有共享锁都被释放了,才能再加上排他锁
-
排他锁:又称为写锁,常见于写,更新操作,排他锁会阻塞所有锁,包括其他共享锁,排他锁,加上排他锁的数据,只有持锁事务才能对数据读写
-
-
根据锁的状态分类
- 意向锁:当事务对表中的某行记录加共享锁/排它锁后,会给整个表加上意向锁,表示这个表已经有人在修改了,避免了需要加表锁对表中每行数据都检查是否加锁的低效行为
- 意向共享锁:当一个事务给整张表加共享锁时,需要先获得意向共享锁
- 意向排他锁:当一个事务给整张表加排他锁时,需要先获得意向排他锁

浙公网安备 33010602011771号