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)这些区间上锁
    • 临键锁:间隙锁 + 记录锁,通常是左开右闭区间,临键锁会把当前记录和记录的前面间隙都上锁,做法和间隙锁一样,也就是间隙锁 + 记录锁的组合。
  • 根据锁的属性分类

    • 共享锁:又称为读锁,是读取操作创建的锁,共享锁不会阻塞其他读操作的共享锁,但是会阻塞排他锁,只有当所有共享锁都被释放了,才能再加上排他锁

    • 排他锁:又称为写锁,常见于写,更新操作,排他锁会阻塞所有锁,包括其他共享锁,排他锁,加上排他锁的数据,只有持锁事务才能对数据读写

  • 根据锁的状态分类

    • 意向锁:当事务对表中的某行记录加共享锁/排它锁后,会给整个表加上意向锁,表示这个表已经有人在修改了,避免了需要加表锁对表中每行数据都检查是否加锁的低效行为
    • 意向共享锁:当一个事务给整张表加共享锁时,需要先获得意向共享锁
    • 意向排他锁:当一个事务给整张表加排他锁时,需要先获得意向排他锁
posted @ 2026-05-01 17:30  不会coding的喵酱  阅读(5)  评论(0)    收藏  举报