3.MySql锁与事务

按模式分类

乐观锁

  • 业务实现
    1
    2
    -- 如果更新失败则重新尝试
    update value = value where version = curversion

悲观锁

  • select…for update 注意weher条件要明确指定主键
  • 对符合 WHERE 条件的行加锁(默认行级锁)
  • 必须在事务中(BEGINSTART TRANSACTION)使用,否则锁会立即释放。
  • 如果 WHERE 条件未命中索引,InnoDB 会升级为 表级锁(全表扫描代价高)。
  • 例如库存扣减场景
    1
    2
    3
    START TRANSACTION;
    -- 检查库存,计算新值
    COMMIT;

按加锁粒度分类

全局锁

  • Flush tables with read lock (FTWRL)
  • 让整个数据库处于只读状态,很危险不建议,只要数据库引擎支持事务操作就使用官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
  • 如果要全库只读,为什么不使用set global readonly=true的方式?
    • 1.在有些系统中,readonly的值会被用来做其他逻辑,比如判断主备库。所以修改global变量的方式影响太大。
    • 2.在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。

表级锁

  • 表锁
    • lock tables … read/write
    • lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
  • 元数据锁(meta-data-lock MDL)
    • MDL 不需要显式使用,在访问一个表的时候会被自动加上,
    • 当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
    • MDL作用是防止DDL(对表数据的增删改查)和DML(对表结构的增删改查以及新增表)并发的冲突
  • 意向锁
    • 当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。加意向锁的目的是为了快速判断表里是否有记录被加锁。

行级锁

  • 记录锁,锁住的是一条记录。而且记录锁是有S锁和X锁之分的,满足读写互斥,写写互斥
  • 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
  • Next-Key Lock 称为临键锁,是 Record Lock+Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

按属性分类

共享锁和排他锁

  • 共享锁(Shared),简写为 S 锁,又称读锁。select …..lock in share mode

  • 排他锁(Exclusive),简写为 X 锁,又称写锁 。sleect … for update

  • image-20201022215240544
  • 有两个规定

    • 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
    • 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。

按状态分类

意向共享锁和意向排他锁

  • 使用意向锁(Intention Locks)可以更容易支持多粒度封锁。

  • 在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。

  • 意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁

  • 规定:

    • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
    • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
  • image-20201022220409460
    • 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁;
    • 这里兼容关系针对的是表级锁,而表级的 IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)
  • 自增锁

    • 完成对自增长值插入的sql语句后立刻释放。表示为当插入一个自增数据后,无论事务是否提交,下次插入的数据均为该自增数据的下一个自增值。

按算法分类

间隙锁(GAP)

  • 间隙锁是一个在索引记录之间的间隙上的锁。但不包括记录本身
  • 保证某个间隙内的数据在锁定情况下不会发生任何变化。比如mysql默认隔离级别下的可重复读(RR)
  • 如果Where条件全部命中,则不会用Gap锁,只会加记录锁。如果where条件部分命中或者全不命中则会加Gap锁。

记录锁(Record Lock)

  • 记录锁 是直接锁定索引中的一条具体记录的锁。
  • 仅锁定单行,不涉及范围。

临键锁(Next-Key Lock)

  • 临键锁记录锁(Record Lock) + 间隙锁(Gap Lock) 的组合,锁定索引记录及其前面的间隙

  • 例如:如果表中存在 id=3id=7 的记录,临键锁可能锁定区间 (3, 7](左开右闭区间)。

  • 目的

    • 防止其他事务在锁定范围内插入新记录(避免幻读)。
    • 阻止其他事务修改或删除锁定范围内的记录。
    • 左开右闭是为了避免双重锁定,如果采用闭区间(如 [10,20]),则记录 1020 会被相邻的锁重复锁定([10,20][20,30] 会重叠锁定 20)。左开右闭确保了边界值(如 20)只被右侧的临键锁锁定一次,避免冲突。
  • MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

封锁协议

三级封锁协议

  • 一级封锁协议

    • 事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。

    • Mysql一级封锁协议
  • 二级封锁协议

    • 在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。

    • Mysql二级封锁协议
  • 三级封锁协议

    • 在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。

    • Mysql三级封锁i协议

两段锁协议(2PL)

  • 一个事务中一旦开始释放锁,就不能再申请新锁了。事务的加锁和解锁严格分为两个阶段,第一阶段加锁,第二阶段解锁。引入2PL是为了保证事务的隔离性,保证并发调度的准确性,多个事务在并发的情况下等同于串行执行。
  • 在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段,其余时间为加锁阶段。

Mysql隐式和显示锁定

  • MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
  • InnoDB 也可以使用特定的语句进行显示锁定:

Mysql如何处理死锁

  • 等待,直到超时
  • 发起死锁检测,主动回滚一条事务,让其他事务继续执行

死锁检测

  • 死锁检测的原理是构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在环,存在即有死锁。

回滚

  • 检测到死锁之后,选择插入更新或者删除的行数最少的事务回滚,基于 INFORMATION_SCHEMA.INNODB_TRX 表中的 trx_weight 字段来判断。

如何安全的给小表加字段

  • ,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
  • MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。
    • ALTER TABLE tbl_name NOWAIT add column …
    • ALTER TABLE tbl_name WAIT N add column …

如何避免发生死锁

  • 使用事务,不使用 lock tables
  • 保证没有长事务。
  • 操作完之后立即提交事务,特别是在交互式命令行中。
  • 如果在用 (SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE),尝试降低隔离级别。
  • 修改多个表或者多个行的时候,将修改的顺序保持一致
  • 创建索引,可以使创建的锁更少。
  • 最好不要用 (SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE)
  • 如果上述都无法解决问题,那么尝试使用 lock tables t1, t2, t3 锁多张表

并发一致性问题

丢失修改

  • 丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。一般在现实生活中常会遇到,例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改并提交生效,T2 随后修改,T2 的修改覆盖了 T1 的修改。

  • Mysql修改丢失

脏读

  • 读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T1 修改一个数据但未提交,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据

  • Mysql脏读

不可重复读

  • 不可重复读指在一个事务内多次读取同一数据集合。在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。例如:T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

  • Mysql不可重复读

幻影读

  • 幻读本质上也属于不可重复读的情况,T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。

  • Mysql幻影读

不可重复读和幻影读的区别

  • 不可重复读是读取同样的数据,前后读取结果不一致,重点是数据的修改,幻影读是同样的条件,检索的结果集不一致,重点是新增或者删除
  • 解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

并发一致性问题总结

  • 产生并发不一致性问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。

数据库事务

ACID

  • 原子性(Atomicity):事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些操作即可。
  • 一致性(Consistency):数据库在事务的执行前后都保持着一致性的状态,在一致性的状态下,所有事务对一条数据的读取结果都时相同的。
  • 隔离性(Isolation):一个事务在提交事务前的所有修改,对其他事务来说是不可见的
  • 持久性(Durability):一旦事务提交,则其所作的修改将会永久的保存到数据库中,即使系统发生崩溃,事务执行的结果也不能丢失。使用重做日志来保证持久性。系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。
  • 解释
    • 只有满足一致性,事务的执行结果才是正确的。
    • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
    • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
    • 事务满足持久化是为了能应对系统崩溃的情况。

隔离级别

  • 未提交读(READ UNCOMMITED)
    事务中的修改,即使不提交,对其他事务也是可见的
  • 读已提交(READ COMMITED)(默认)
    一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
  • 可重复读(REPEATABLE READ)
    保证在同一个事务中多次读取同一数据的结果是一样的。
  • 可串行化(SERIALIZABLE)
    强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。
  • 隔离级别 脏读 不可重复读 幻读 锁策略
    读未提交 (Read Uncommitted) 可能 可能 可能 无锁(或极弱锁)
    读已提交 (Read Committed) 不可能 可能 可能 行级锁(短暂持有) + MVCC
    可重复读 (Repeatable Read) 不可能 不可能 不可能 临键锁(Next-Key Lock) + MVCC
    串行化 (Serializable) 不可能 不可能 不可能 表级锁或严格的行级锁
  • RR级别下并没有完全解决幻读问题,但通过使用Next-Key Locks(一种Gap Lock和Record Lock的组合锁)的机制,它在大多数情况下可以防止幻读的发生。以下是RR级别下,幻读会发生的情况
    • 快照读(非锁定读):在RR隔离级别下,同一个事务内混合“快照读”和“当前读”,
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      -- 事务 A (RR 隔离级别)
      START TRANSACTION;

      -- 快照读 (基于 Read View V1), 假设返回记录数: 5
      SELECT * FROM users WHERE age BETWEEN 20 AND 30;

      -- 此时事务 B 插入一条 age=25 的新记录并提交
      -- COMMIT; (事务 B)

      -- 当前读 (读取最新已提交数据, 获取 Next-Key Locks)
      SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
      -- 这个 SELECT FOR UPDATE 看到了事务 B 新插入的 age=25 的记录!
      -- 事务 A 的第二次查询比第一次查询多出了一行 (幻读发生)

      COMMIT; -- 事务 A
    • UPDATE/DELETE 操作“看到”幻影行并修改它们
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      -- 事务 A (RR 隔离级别)
      START TRANSACTION;

      -- 快照读 (基于 Read View V1), 假设返回记录数: 5
      SELECT COUNT(*) FROM users WHERE age BETWEEN 20 AND 30; -- 结果: 5

      -- 此时事务 B 插入一条 age=25 的新记录并提交
      -- COMMIT; (事务 B)

      -- UPDATE 是当前读! 它会看到事务 B 提交的新记录 (age=25)
      UPDATE users SET status = 'active' WHERE age BETWEEN 20 AND 30;
      -- 这个 UPDATE 语句实际影响了 6 行记录 (包括事务 B 插入的那一行)

      -- 再次快照读 (基于 Read View V1), 仍然看不到新行
      SELECT COUNT(*) FROM users WHERE age BETWEEN 20 AND 30; -- 结果: 5 (快照读不变)

      -- 但是! 执行当前读:
      SELECT COUNT(*) FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 结果: 6 (看到新行)
      -- 或者提交后看数据,会发现 6 行被更新了

      COMMIT; -- 事务 A
      • UPDATEDELETE 语句在 RR 级别下也是当前读。它们在查找要修改的行时,会读取数据库的最新已提交状态(并获取 Next-Key Locks)。因此,它们能“看到”并修改在事务开始之后、但在 UPDATE/DELETE 执行之前由其他已提交事务插入的“幻影行”。

MVCC(多版本并发控制)

  • 多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读的可重复读以及快照读的幻读问题这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
  • 读不加锁,读写不冲突。MVCC 在不加锁的情况下解决了脏读、不可重复读

MVCC基础概念

  • 版本号
    • 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
    • 事务版本号 TRX_ID :事务开始时的系统版本号。
  • 隐藏的列
    • MVCC在每行记录后面都保存着两个隐藏的列,
      • DB_TRX_ID:最后修改改行的事务ID
      • DB_ROLL_PTR(roll_pointer):指向旧版本数据的指针。
        • 每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息
  • undo日志
    • MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。

    • MysqlUndo1

当前读和快照读

  • 快照读
    • MVCC 中的 SELECT 操作是快照中的数据,不需要进行加锁操作。
    • 每一次修改数据都会在undo-log 中存有快照记录,快照读就是读取某一个版本的快照
  • 当前读
    • 当前读是给读操作加上共享锁、排它锁,DML 操作加上排它锁,读取记录的最新版本。
    • 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
    • MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)隐式的加上排他锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。
  • 特性 当前读 快照读
    数据版本 读取最新已提交数据 读取历史快照数据(基于事务/语句时间点)
    加锁 是(排他锁或共享锁)
    并发性能 低(锁竞争可能阻塞其他事务) 高(无锁,读写并行)
    适用操作 FOR UPDATEUPDATEDELETE 、INSERT 普通SELECT
    隔离级别依赖 所有级别(行为一致) 读已提交和可重复读(行为不同)
    触发条件 显式加锁的 SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE
    任何数据修改语句(UPDATE/DELETE/INSERT)在执行前会隐式触发当前读。
    所有普通 SELECT 语句默认使用快照读。
    事务首次读取数据时生成快照,后续读取沿用该快照。

MVCC适用的隔离级别

  • 操作类型 RC 隔离级别 RR 隔离级别
    普通 SELECT 动态快照读(每次生成新 Read View 固定快照读(复用首次 Read View
    写操作/加锁读 当前读(读取最新数据) 当前读(读取最新数据,加间隙锁防幻读)

read-view

  • 进行快照读时,会产生read-view,RC在每一次进行普通SELECT操作前都会生成一个ReadView,而RR只在第一次进行普通SELECT操作前生成一个ReadView,数据的可重复读其实就是read-view的重复使用
  • read-view有四个重要变量
    • m_ids:活跃事务id列表,当前系统中所有活跃的(也就是没提交的)事务的事务id列表。
    • min_trx_id:m_ids 中最小的事务id。
    • max_trx_id:生成 ReadView 时,系统应该分配给下一个事务的id(注意不是 m_ids 中最大的事务id),也就是m_ids 中的最大事务id + 1 。
    • creator_trx_id:生成该 read-view 的事务的事务id。
  • trx_id是版本链中的各个id, 遍历版本链,通过比较trx_id和m_ids中的数的关系来确认当前可见的版本是哪个
    • trx_id == creator_trx_id,版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见。
    • trx_id<min_trx_id,表示此版本是已经提交的事务生成的,由于事务已经提交所以数据是可见的。
    • trx_id>max_trx_id,表示此版本是由将来启动的事务生成的,是肯定不可见的。
    • min_trx_id<=trx_id<=max_trx_id会存在俩种情况
      • trx_id 在数组中,表示此版本是由还没提交的事务生成的,不可见,但是当前自己的事务是可见的。
      • row 的 trx_id 不在数组中,表明是提交的事务生成了该版本,可见。

3.MySql锁与事务
https://x-leonidas.github.io/2025/10/26/05数据库/MySQL/3.MySql锁与事务/
作者
听风
发布于
2025年10月26日
更新于
2025年7月1日
许可协议