InnoDB中的锁
InnoDB中的锁机制是MySQL中实现事务隔离和数据一致性的核心部分。它通过多种锁类型和等级,控制多个事务对同一数据的并发访问,保证数据的完整性与一致性。
主要锁类型
- 1.行锁(Row Lock)
- 定义:锁定单个行记录。InnoDB采用多版本并发控制(MVCC)结合行锁,支持高并发。
- 作用范围:单个行数据
- 类型:
- 共享锁(S锁,共享锁):允许多个事务同时读取(select … lock in share mode),但不允许修改。
- 排他锁(X锁,排他锁):事务可以修改行数据,其他事务既不能读取(除非使用更低隔离级别),也不能修改。
- 2 表锁(Table Lock)
- 比较少用,但在某些操作(如lock tables)会用到。
- 作用范围:整张表
- 作用:阻止其他事务对表的任何读写操作,确保完整一致性。
锁的粒度和等级
- 粒度
- 行级锁:锁定单个行,细粒度,支持高并发。
- 表级锁:锁定全表,粗粒度,适用于大量DML操作。
- 隔离级别对锁的影响
MySQL的事务隔离级别影响锁的行为:
- Read Uncommitted:可能会读取未提交事务的行(脏读),锁较少。
- Read Committed:只能读取已提交的行,使用行锁。
- Repeatable Read(InnoDB默认):通过MVCC和行锁实现,支持一致性读。
- Serializable:最严格,几乎类似表锁,保证完全隔离。
锁的应用场景
- select… for update:获得行的排他锁,阻止其他事务修改,常用于更新前锁定。
- select … lock in share mode:获得共享锁,允许读取但阻止其他事务取得排他锁。
不同类型锁的特点
锁类型 | 作用范围 | 并发支持 | 特点 |
---|---|---|---|
共享锁(S) | 行或表 | 多事务可同时读 | 允许读取,不允许修改 |
排他锁(X) | 行或表 | 不能同时读写 | 只允许自己操作,其他等待 |
意向锁(IS, IX) | 表锁的辅助手锁 | 执行时帮助锁定层次关系 | 通知其他事务锁定意向 |
自增锁(NEXT KEY) | 内部机制 | 维护自增主键唯一性 | 防止自增列出现重复 |
锁的兼容性与冲突
- 兼容性:如果事务获取了某个数据的共享锁,其他事务可以立即获得该数据的共享锁,这种情况叫锁兼容。如果事务获取了某个数据的共享锁或排他锁,其他事务想要获取该数据的排他锁,必须要等到该行的锁释放掉。
- 冲突:主要发生在:
- 一个事务试图获得排他锁,而另一个事务已持有共享锁或排他锁。
- 两个事务都试图获得互斥的锁。
死锁
- 当多个事务相互等待对方释放锁时,会发生死锁。
- InnoDB检测到死锁后,会选择中断其中一个事务(回滚)解决。
条件
- 互斥条件:资源(锁)同时只能由一个事务使用。
- 占有并等待:一个事务至少持有一个资源,同时等待获取其他被占用资源。
- 非剥夺条件:已占有的资源不能强制收回,只能由持有事务主动释放。
- 循环等待条件:多个事务形成环状等待关系。
示例流程:
- 事务A:
锁住table1
等待锁住table2
- 事务B:
锁住table2
等待锁住table1
- 此时:
A等待B释放table2
B等待A释放table1
- 两个事务都在等待对方释放资源,导致僵局(死锁)。
MVCC(多版本并发控制)
- InnoDB通过MVCC实现非锁定读(一致性读),大幅提升并发性能。
- 读操作不会等待写锁释放,而是读取数据的快照副本。
- 核心思想:每个数据行都存储多个版本(快照),不同事务可以看到不同的版本。
工作原理
- 版本控制:每次对数据的修改(INSERT、UPDATE、DELETE)都会生成一个新的版本。
- 版本识别:每个事务都有一个事务ID(TS)或时间戳。
- 读取数据:
- 读取操作根据事务的TS,只读取“版本号” ≤ 自己TS的最新版本。
- 这样,读取的是事务开始时“的视图”,实现一致性。
在InnoDB中实现
- 行版本存储:
- 每行除了实际数据,还有两个隐藏字段:
- 创建版本(XMIN):该版本由哪个事务创建。
- 删除版本(XMAX)(可空):标记哪个事务删除了这个版本。
- 每行除了实际数据,还有两个隐藏字段:
- 事务管理:
- 事务有一个事务ID(或时间戳)。
- 读取时,只访问那些版本号≤自己事务ID的最新版本。
- 写入时,创建新的版本。
- 事务有一个事务ID(或时间戳)。
总结
-
MVCC让数据库在进行多事务同时读写时:
- 读操作不加锁(非锁定读)
- 写操作仍通过锁控制
- 不同事务可以“同时”访问不同版本的数据
这样极大提升了并发能力,并保证了事务的隔离性。
意向锁
- InnoDB 允许事务在行锁和表锁同时存在。为支持在不同粒度上进行加锁,InnoDB 支持意向锁。
- 意向锁,将锁定的对象分为多个层级,意向锁意味着事务有意向在更细粒度上加锁。如果需要对行加锁,需要先对表加意向锁再对行加锁。
作用和目的
-
协调锁的层次关系:在InnoDB中,为了支持多粒度锁(表锁和行锁),需要用意向锁告诉其他事务:自己打算在某个部分(比如表的某个行)上加锁。
-
减少锁冲突:其他事务在请求锁时,通过判断意向锁,可以快速知道表是否已被部分锁定,从而避免不必要的等待。
-
避免死锁:通过表的意向锁协调不同粒度的锁请求,降低死锁风险。
-
意向锁的作用:如果没有意向锁,想要给一个表加表锁必须要检查该表是否有表锁和每一行是否有锁。而如果在加行锁前给这个表加上了意向锁,这时只需要检查表锁和意向锁就可以了,不需要检查每一行的锁。
常见的意向锁类型
锁类型 | 作用范围 | 数据库层次 | 描述 |
---|---|---|---|
意向共享锁(IS,Intention Shared) | 表 | 表锁 | 表明自己打算在某个或多个行上获取共享锁(S锁) |
意向排他锁(IX,Intention Exclusive) | 表 | 表锁 | 表明自己打算在某个或多个行上获取排他锁(X锁) |
意向锁在 InnoDB 中就是表级别的锁,支持两种意向锁:
- 意向共享锁(IS Lock),事务有意向对表中某些行加共享锁
- 意向排他锁(IX Lock),事务有意向对表中某些行加排他锁
表级锁的兼容性
- 注意:意向锁不会和行级锁冲突,意向锁之间也不会冲突,意向锁只会和共享表锁和排他表锁冲突
用法和流程
- 事务请求锁的流程
- 当事务要在表中的某些行上加锁(S或X锁)时,必须先在表上加对应的意向锁(IS或IX)。
- 这样做的好处:
- 其他事务可以通过意向锁快速得知表是否已有锁定意图,不会不必要地阻塞。
- 维护锁的层次结构和一致性。
- 实际操作中的表现
- 如果一个事务想要在某行上加S锁:它会在该行加共享锁,同时在对应的表上加意向共享锁(IS)(如果没有的话)。
- 如果一个事务想在行上加X锁:它会在该行加排他锁,同时在表上加意向排他锁(IX)。
示例
假设有两笔事务:
- 事务A打算在某行加X锁(X相当于写锁)
- 事务B想在该行加S锁(读取)
在加锁流程中:
- 事务A会在表上加IX锁。
- 事务A在目标行上加X锁。
- 事务B想在该行获取S锁时,会检查表的锁状态,发现表上有IX锁(由A持有),这表示有人在该表上准备加排他锁。
- 根据规则,B会等待A释放锁。
类似的,如果B试图在某行加S锁,且没有冲突的话,也会在表上加IS锁。
总结
特点 | 内容 |
---|---|
作用 | 表明事务在表中的某些行上打算加锁,是一种“计划性”锁,用于锁层次协调。 |
类型 | IS(意向共享锁)、IX(意向排他锁) |
作用范围 | 仅在表级别,用于锁的层次协调 |
重要性 | 支持多粒度锁,避免锁冲突发生,提升系统并发能力 |
一致性非锁定读
特点
- 一致性非锁定读,读取正在执行 delete 和 update 操作的行时,不会等待该行上锁的释放,而是读取该行的一个快照数据(该行之前的版本)。非锁定读极大的提高了数据库的并发性,InnoDB 默认时这种读取方式,也就是说默认普通的 select 语句是不会加锁的,而是通过读取快照实现数据一致性。
- 上面提到的快照数据就是该行数据的历史版本,由此带来的并发控制称为多版本并发控制(MVCC)。
- READ COMMITTED(以提交读)和 REPEATABLE READ(可重复读)在 InnoDB 中使用的是一致性非锁定读,但是读取的快照不同。提交读级别读取的是最新版本的快照,可重复读级别读取的是事务开始时数据的快照。
REPEATABLE READ(可重复读)
含义
- 事务开始时的快照在整个事务期间保持不变。
- 事务中多次读取同一数据,结果一致,即使其他事务也在修改。
- InnoDB默认的隔离级别。
READ COMMITTED(已提交读)
含义
- 每次读取都是“当前事务开始时的快照”,但每次读取都会重新扫描最新的提交版本。
- 事务中多次读取相同数据,可能会得到不同的结果(如果其他事务提交了修改)。
区别:
特性 | REPEATABLE READ | READ COMMITTED |
---|---|---|
读取快照 | 事务开始时的快照 | 每次读取时最新提交状态 |
重复读一致性 | 保持 | 不保证(可能不同) |
读到的值 | “快照”版本 | 最新提交值 |
可重复读级别通过读快照,可以解决前面提到的幻读问题,但是有些情况需要锁定读
原理
-
InnoDB为每个行文档都维护了隐藏的系统列(如DB_TRX_ID和DB_ROLL_PTR和版本信息):
- DB_TRX_ID:标识该行最后一次被修改的事务ID
- DB_ROLL_PTR:指向旧版本的链表(多版本链)
-
当进行一致性非锁定读时:
- 读取只看自己事务开启时的“快照”中的版本,不会受其他未提交事务写操作的影响。
- 允许多个事务同时读数据,而不发生阻塞。
工作流程图
假设有两个事务:
时间点 | 操作描述 |
---|---|
事务A开始 | 事务A开始对数据进行读取,读取点为“快照”时间 |
事务B提交 | 事务B对某行做了修改,提交成功 |
事务A继续读取 | 事务A读取数据时,看到的版本仍是事务开始时的快照(没有事务B的修改) |
如果事务A执行SELECT,它会通过MVCC机制:
- 读取的是事务开始时的版本快照。
- 不加锁,不会阻塞其他写操作,也不会被其他写操作阻塞。
实现细节-快照一致性
- 事务开启时,系统会生成一个快照版本号(Snapshot Version)。
- 读取数据时,只会显示在自己快照版本之前(或等于自己快照版本)的版本。
- 即使其他事务修改数据,也不会影响正在读取的快照内容。
总结
特性 | 内容 |
---|---|
非锁定 | 读操作不加锁,不阻塞其他事务,避免读写冲突。 |
一致性 | 读到的是自己事务开始时(或某个时间点)的“快照”,保证读的一致性。 |
依赖MVCC | 通过维护多版本,实现多事务并发读写的高效隔离。 |
隔离级别 | 在REPEATABLE READ 和READ COMMITTED 中均支持,一致性非锁定读是它的基础。 |
一致性锁定读
- InnoDB 默认使用一致性非锁定读。某些情况用户需要显式加锁保证数据的一致性,支持两种一致性锁定读的操作
- select … for update:对读取的行加一个排他锁
- select … lock in share mode:对读取的行加一个共享锁
- 如果只对读取的行加锁会有幻读问题:
- 锁定读时使用键值间隙锁(Next-Key Lock),就是行锁加间歇锁,可以解决幻读问题。
- Record Lock:单行记录的锁
- Gap Lock:间歇锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并锁定记录本身
- 非锁定读使用快照和锁定读使用间歇锁可以基本解决幻读问题,但是极特殊情况还是有可能发生幻读。
特点
- 非锁定:读取操作不加锁,不阻塞其他事务。
- 一致性:读取数据是事务开始时的快照,不会受到其他已提交事务的影响。
- **基于MVCC(多版本并发控制)**实现。
简单例子:
- 事务A开始,读取数据A,得到的版本是事务A开始时的快照。
- 其他事务(如B)可以在这期间修改数据并提交,事务A的读取不会受到影响。
与锁定读的区别
功能 | 一致性锁定读(快照读) | 锁定读(如SELECT ... FOR UPDATE ) |
---|---|---|
是否加锁 | 不加锁,非锁定 | 加锁(行锁或表锁) |
阻塞情况 | 不阻塞 | 可能阻塞等待其他事务的释放 |
读到数据 | 事务开始时的快照 | 最新数据(可能被其他事务锁定阻塞) |
适用场景 | 高并发、只读操作、保证一致性 | 更新、需要锁保证数据完整 |
- 一致性锁定读 是锁定读中的一个类型
- 在一个事务中,先读到某个数据,然后修改它,为了确保在修改前,没有其他事务修改过,需要用到锁定读,避免“脏读”或“不可重复读”。
需求 | 方法 | 是否需要特殊操作 | 说明 |
---|---|---|---|
一致性、非阻塞读 | 使用普通select | 是,不需要 | InnoDB的默认行为,保证读取时一致快照 |
需要锁定数据以防止其他修改 | select ... for update 或 lock in share mode | 需要 | 用于控制并发读写,保证数据一致性 |
一致性锁(非锁定读)与其他锁的基本关系
类型 | 作用 | 是否加锁 | 在什么场景使用 | 与其他锁关系 |
---|---|---|---|---|
一致性非锁定读(快照读) | 读到事务开始时的“快照”版本,不阻塞其他事务 | 不加锁(MVCC实现) | 查询、统计、只读操作 | 不阻塞,也不被阻塞,支持高并发,保证数据一致性 |
锁定读取(行锁、表锁) | 以锁定的方式读取,阻止其他事务读写 | 加锁(排他或共享) | 更新数据、需要控制并发一致性 | 锁定读会阻塞其他写操作,或者等待其他事务释放锁 |
意向锁(IS、IX) | 表层次上的“预备锁”,告诉其他事务你的计划 | 加在表上 | 支持多粒度锁、避免锁冲突 | 表级意向锁用于协调实际锁(比如行锁),避免冲突和死锁 |
关系
- 在实际操作中,当你用SELECT没有加任何锁时,InnoDB会选择使用快照读,这不是“锁”本身,而是用多版本机制保证数据一致。
- 如果你用SELECT … FOR UPDATE或LOCK IN SHARE MODE,则会执行锁定读,在数据上加锁,阻止其他事务同时修改或读取(取决于锁类型)。
- 意向锁是锁粒度的预备措施,帮助协调实际的行锁或表锁,不直接参与读操作,只是标明“我打算加锁”。
- 一致性锁(快照读)不用加锁,但依赖MVCC保证读取的一致性。
- 其他锁(行锁、表锁)会加锁,阻塞或等待,确保并发场景下数据的同步和一致。
- 两者在实际环境中配合使用:快照读提供高效的只读快照,锁定读用在需要确保强一致性或同步更新的场景。
总结
InnoDB的锁机制:
- 支持细粒度的行级锁,提升并发性能。
- 利用锁和MVCC结合,平衡并发性和数据一致性。
- 提供多种锁类型,适应不同的应用场景。
- 配合事务隔离级别,确保数据的隔离性和一致性。
- 但也需要注意死锁和锁等待,合理设计事务逻辑。