MySQL锁分类及实现
一、锁分类
MySQL锁主要分为3大类:
- 表级锁:存储引擎为
Myisam。锁住整个表,特点是开销小,加锁快,锁定力度大,发生锁冲突的概率最高,并发度最低。 - 页级锁:存储引擎为
BDB。锁住某一页的数据(16kb左右),特点:开销和枷锁时间介于表级和行级之间;会出现死锁,锁定力度介于表锁和行锁之间,并发度一般。 - 行级锁:存储引擎为
innodb。锁住某一行的数据,特点:锁的实现更加复杂,开销大,加锁速度慢。
根据以上特点,仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
二、行级锁分类
行级锁主要分为以下7类:
- 共享/排他锁(shared/exclusive lock)
- 意向锁(intention lock)
- 记录锁(record lock)
- 间隙锁(gap lock)
- 临建锁(next-key lock)
- 插入意向锁(insert intention lock)
- 自增锁(auto-inc lock)
2.1 共享/排他锁:
2.1.1 共享锁
共享锁:又称S锁、读锁,可以允许读,但不能写。共享锁可以与共享锁一起使用。
select ... from table_name lock in share mode;
2.1.2 排他锁
排他锁:又称X锁、写锁,不能允许读,也不能允许写,排他锁不能与其他所一起使用。
select ... from table_name for update;
在mysql中,update,delete,insert,alter这些写的操作默认都会加上排他锁。select默认不会加任何锁类型。一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发操作有较大的影响。
共享/排他锁的释放方式:commit、rollback
2.2 意向锁
innoDB为了支持多粒度的锁,即允许行级锁和表级锁共存,而引入意向锁。意向锁是指未来的某个时刻,事务可能要加共享/排他锁,先提前声明一个意向。这样如果有人尝试对全表进行修改,就不需要判断表中的数据是否被加锁了,只需要通过等待意向互斥锁被释放就行了。
- 意向共享锁(
IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。 - 意向互斥锁(
IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。
无法手动创建
2.3 记录锁
单个行记录上的锁。记录锁总是会锁住索引记录,如果innoDB存储引擎表在建立的时候没有设置任何一个索引,那么innoDB存储引擎会使用隐式的主键来进行锁定。
创建方式参考排他锁
2.4 间隙锁
间隙锁锁住记录中的间隔,即范围查询的记录。
select * from table_name where id between 1 and 10 for update;
这个脚本会锁住1到10的数据,以防止其他事务修改该区间的记录;
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(
Read Committed,RC),间隙锁则会自动失效
查看innodb_locks_unsafe_for_binlog是否禁用:
show variables like 'innodb_locks_unsafe_for_binlog';
2.5 临建锁
临建锁是记录锁和间隙锁的组合,锁的范围既包含记录又包含索引区间。默认情况下,innoDB使用临建锁来锁定记录。但当查询的索引含有唯一属性的时候,临建锁会进行优化,将其降级为记录锁,即仅锁住索引本身,不是范围。
临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
2.6 插入意向锁
插入意向锁(Insert Intention Lock) 是一种特殊的间隙锁(Gap Lock),主要用于优化并发插入操作的性能。其核心思想是允许不同事务在同一间隙内插入互不冲突的行,从而减少锁竞争。
当多个事务向同一个索引间隙插入数据时,通过插入意向锁 避免不必要的锁等待。提高插入操作的并发性,减少对同一间隙的插入冲突。
例如,事务A持有间隙锁(Gap Lock),事务B仍可以插入不冲突的位置(前提是插入的值不属于该间隙锁范围)。
2.7 自增锁
自增锁(AUTO-INC Lock)是实现自增主键(AUTO_INCREMENT)的核心机制,是一种特殊的表级别的锁,专门针对事务插入auto-increment类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
自增锁的行为由innodb_autoinc_lock_mode参数控制,该参数有以下三种模式(MySQL 8.0前):
| 参数值 | 名称 | 行为特点 |
|---|---|---|
| 0 | 传统模式 | 所有插入语句均使用表级自增锁(AUTO-INC Lock),严格保证连续性,但性能最差。 |
| 1 | 连续模式 | 默认模式。单条插入用轻量级锁,批量插入(如INSERT ... SELECT)用表级锁。 |
| 2 | 交错模式 | 所有插入语句均用轻量级锁(仅保证唯一性,不保证连续性),性能最好。 |
注意:MySQL 8.0+:默认 innodb_autoinc_lock_mode = 2(交错模式),且自增值的生成逻辑优化为原子操作(无锁化趋势)。
三、乐观锁
3.1 定义
乐观锁:乐观的假定大概率不会发生并发更新冲突,访问、处理数据的过程中不加锁,只在更新数据时根据版本号或时间戳判断是否有冲突,有则处理,无责提交事务。
如果系统并发量非常大,悲观锁会带来非常大的性能问题,选择使用乐观锁,现在大部分应用属于乐观锁
3.2 实现方式
3.2.1 CAS算法
在MySQL中,CAS算法通过版本号或时间戳 + UPDATE ... WHERE实现乐观锁。具体来说,UPDATE语句会先读取当前值,然后在更新时判断当前值是否与预期值相等,如果相等则更新为新值,否则不更新。
版本号:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改的时候,version值就会+1。
当线程A要更新数据值的时候,在读取数据的同时也会读取version值,在提交更新的时候会再次读取那条数据的version值,如果两个version值一样的话,才会更新数据,否则就重试刚刚的更新数据操作,直到更新成功。
因为如果没有对比version值,那么线程A在修改数据的期间,有其他线程修改相同的那条数据并提交,那么就会导致数据出错,出现旧数据覆盖新数据等这种情况。提交版本的version值必须要大于记录当前版本的version值才能执行更新。
UPDATE users
SET balance = 200, version = version + 1
WHERE id = 1 AND version = 2; -- 仅当版本号是 2 时才更新
时间戳:
MySQL中的timestamp,或者自己在表中新增一个字段用于存储时间戳,就是在执行数据更新时,除了比较版本号之外,还需要比较时间戳。在执行数据更新时,同时需要更新时间戳的数据,一般有两种方式:
- 一种是在数据库层面通过触发器等方式自动更新时间戳字段,
- 另一种是在应用层面手动更新时间戳字段
底层原理:
- 依赖
InnoDB行锁:执行UPDATE时会加锁,条件检查与更新是原子操作(即使高并发,同一行只能有一个事务更新成功)。 - 无覆盖风险:若两个事务同时更新同一行,只有一个会成功(另一事务的
WHERE条件失效)。
3.2.2 write_condition机制
可以先理解为是版本号机制的一种体现(版本号机制是为表添加一个版本号的字段,write_condition是根据表中的某个字段的状态来判断数据是否被修改过)
通过SQL的UPDATE ... WHERE语句附加条件,例如:
UPDATE users SET balance = 200 WHERE id = 1 AND balance = 100; -- 仅当 balance 当前是 100 时才更新
底层原理:
- 依赖事务隔离级别(如
REPEATABLE READ):在事务中,通过读到的旧值(可能加锁)判断条件是否成立。 - 非原子性风险:多个事务同时读取到旧值,可能导致条件成立但实际覆盖冲突(如经典“丢失更新”问题)。
3.3 核心区别总结
| 特性 | write_condition(普通条件更新) | CAS(乐观锁实现) |
|---|---|---|
| 原子性 | ❌非原子(条件检查与更新分离) | ✅ 原子(检查+更新一气呵成) |
| 并发安全性 | 可能丢失更新(需配合悲观锁) | 高并发安全(无需锁) |
| 适用场景 | 简单业务逻辑(低并发) | 高并发、数据竞争激烈的场景 |
3.4 如何选择?
- 普通
write_condition:适合对并发要求不高的场景,例如后台任务或低频更新。 CAS乐观锁:适合高频并发场景(如电商库存扣减、账户余额修改等),需要显式加入版本号或时间戳字段,并结合重试机制。
实际开发中,CAS是MySQL中实现并发安全的常用手段,而write_condition只是一个普通SQL条件,除非配合锁或事务隔离级别使用,否则无法避免并发问题。
四、悲观锁
悲观锁:悲观的假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。
优点:
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
缺点:
(a)在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
(b)在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数
具体参考MySQL锁机制
五、其他锁
接下看讲一下其他的锁:
5.1 死锁
产生是因为线程锁之间交替等待产生的。值两个或两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。
MySQL处理死锁的方法:根据数据写的数据量的大小来回滚小事务。
5.2 表级锁
表级读锁:
lock table table_name read;
表级写锁:
lock table table_name write;
释放锁
unlock tables;
六、建议
- 控制事务的大小(操作写的数据量)
- 使用锁的时候尽量要配合与携带索引的字段使用,避免升级为表锁
- 范围查询,尽量减少基于范围查询的事务的大小
- 如果业务必须要使用锁,锁的冲突特别高的话,改为表锁
- 可以根据项目自身的情况调节事务的
innodb_flush_log_at_trx_commit

浙公网安备 33010602011771号