在高并发的MySQL数据库场景中,锁是解决数据竞争、保证事务隔离性和数据一致性的核心机制。但MySQL锁类型繁多(全局锁、表锁、行锁、间隙锁...),底层逻辑抽象,稍不注意就会引发死锁、锁等待、幻读等问题。本文将从理论原理 到实战案例,全方位拆解MySQL锁机制,让你不仅"知其然",更"知其所以然"。
一、MySQL锁的核心分类维度
MySQL锁的分类可从多个维度切入,理解维度是掌握锁的基础:

-
粒度维度:锁的作用范围越大,并发度越低,实现越简单;反之则并发度高,实现复杂。
-
锁类型维度:共享锁(S)允许多个事务读,排他锁(X)独占资源(读写均阻塞)。
-
意向锁维度:协调表锁与行锁的关系,避免全表扫描检测行锁。
二、全局锁:锁住整个数据库
1. 定义与用途
全局锁是对整个数据库实例 加锁,加锁后所有库的所有表都处于只读状态(DML/DDL/更新事务均被阻塞)。MySQL中通过FLUSH TABLES WITH READ LOCK (FTWRL)实现,主要用于全库逻辑备份(确保备份数据的一致性)。
2. 实战案例
-- 会话1:加全局锁
FLUSH TABLES WITH READ LOCK;
-- 会话1:执行全库备份(如mysqldump)
mysqldump -uroot -p test > test_backup.sql;
-- 会话1:解锁
UNLOCK TABLES;
-- 会话2:加锁期间执行写操作(会阻塞,直到解锁)
INSERT INTO product(name, stock) VALUES('Android', 50); -- 阻塞
3. 注意事项
InnoDB引擎支持MVCC(多版本并发控制) ,可通过mysqldump --single-transaction实现一致性备份(无需加全局锁),因为该参数会启动一个只读事务,利用undo log获取快照数据。全局锁仅适用于MyISAM等不支持事务的引擎。
三、表级锁:锁住整张表
表级锁是作用于整张表 的锁,开销小、加锁快,但并发度低。主要包括表锁 、元数据锁(MDL) 、意向锁三类。
1. 表锁(Table Lock)
(1)语法与特性
-- 加读锁
LOCK TABLES product READ;
-- 加写锁
LOCK TABLES product WRITE;
-- 解锁
UNLOCK TABLES;
-
读锁(READ):持有读锁的会话可读表,不可写;其他会话可读,写阻塞。
-
写锁(WRITE):持有写锁的会话可读可写;其他会话读写均阻塞。
(2)实战案例
-- 会话1:加product表读锁
LOCK TABLES product READ;
-- 会话1:读操作(正常)
SELECT * FROM product; -- 成功
-- 会话1:写操作(失败)
UPDATE product SET stock = 99 WHERE id = 1; -- 报错:Table 'product' was locked with a READ lock and can't be updated
-- 会话2:写操作(阻塞)
UPDATE product SET stock = 99 WHERE id = 1; -- 阻塞,直到会话1解锁
2. 元数据锁(MDL)
(1)定义与特性
MDL是MySQL5.5引入的隐式锁,访问表时自动加锁(无需手动操作),用于保证表结构变更(DDL)与数据操作(DML)的一致性。
-
读操作(SELECT)加MDL读锁(共享);
-
写操作(INSERT/UPDATE/DELETE)加MDL写锁(排他);
-
DDL操作(ALTER TABLE)加MDL排他锁(会阻塞所有读写操作)。
(2)坑点案例:长事务阻塞DDL
-- 会话1:启动长事务(未提交)
BEGIN;
SELECT * FROM product WHERE id = 1; -- 加MDL读锁
-- 会话2:执行DDL(阻塞,因为会话1的MDL读锁未释放)
ALTER TABLE product ADD COLUMN price DECIMAL(10,2); -- 阻塞
-- 会话3:新的读操作(也会阻塞!因为MDL锁队列是"先到先得")
SELECT * FROM product; -- 阻塞
解决方案:
-
避免长事务(及时提交/回滚);
-
用
ALTER TABLE ... WAIT N指定等待超时(MySQL8.0+支持):ALTER TABLE product ADD COLUMN price DECIMAL(10,2) WAIT 5; -- 等待5秒超时则放弃
3. 意向锁(IS/IX)
(1)作用
意向锁是表级锁,用于告知数据库"某个事务正在/将要锁住表中的行",避免表锁加锁时全表扫描检测行锁(提升性能)。
-
意向共享锁(IS):事务准备加行级共享锁(S)前加IS锁;
-
意向排他锁(IX):事务准备加行级排他锁(X)前加IX锁。
(2)兼容性规则
| 表读锁(READ) | 表写锁(WRITE) | IS锁 | IX锁 | |
|---|---|---|---|---|
| 表读锁 | 兼容 | 不兼容 | 兼容 | 兼容 |
| 表写锁 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
| IS锁 | 兼容 | 不兼容 | 兼容 | 兼容 |
| IX锁 | 兼容 | 不兼容 | 兼容 | 兼容 |
四、行级锁:InnoDB的核心锁机制
行级锁是InnoDB引擎的特性,仅作用于索引行 (无索引则退化为表锁),并发度最高。核心包括记录锁 、间隙锁 、Next-Key锁。
1. 共享锁(S)与排他锁(X)
(1)语法
-- 加共享锁(S):允许其他事务读,阻塞写
SELECT * FROM product WHERE id = 1 LOCK IN SHARE MODE;
-- 加排他锁(X):阻塞其他事务读写
SELECT * FROM product WHERE id = 1 FOR UPDATE;
(2)实战案例:库存扣减的并发控制
-- 会话1:开启事务,加排他锁查库存
BEGIN;
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- stock=100
-- 会话2:同样加排他锁查库存(阻塞,直到会话1提交)
BEGIN;
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 阻塞
-- 会话1:扣减库存并提交
UPDATE product SET stock = 99 WHERE id = 1;
COMMIT;
-- 会话2:此时获取锁,查询到stock=99
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- stock=99
2. 记录锁(Record Lock)
记录锁是锁住索引记录本身的行锁,仅作用于存在的索引行。例如:
-- 表user的id为主键,数据:(1,Alice),(5,Bob),(10,Charlie)
BEGIN;
SELECT * FROM user WHERE id = 5 FOR UPDATE; -- 仅锁住id=5的记录
此时其他事务修改id=5的记录会阻塞,但修改id=1/10或插入id=3的记录不受影响。
3. 间隙锁(Gap Lock)
(1)定义
间隙锁是锁住索引记录之间的间隙 (或第一条记录前/最后一条记录后),防止"幻读"(同一事务两次查询返回不同行数)。仅适用于InnoDB的RR(可重复读)隔离级别(RC隔离级别无间隙锁)。
(2)实战案例:解决幻读
-- 表user的id为主键,数据:(1,Alice),(5,Bob),(10,Charlie)
-- 会话1:RR隔离级别下,加锁查询间隙
BEGIN;
SELECT * FROM user WHERE id > 3 AND id < 8 FOR UPDATE; -- 锁住间隙(1,5)和(5,10)
-- 会话2:插入id=6的记录(阻塞,因为间隙锁禁止插入)
INSERT INTO user(id, name) VALUES(6, 'David'); -- 阻塞
-- 会话1:提交后,会话2才能插入
COMMIT;
4. Next-Key Lock
Next-Key Lock是记录锁+间隙锁的组合,是InnoDB默认的行锁方式(RR隔离级别下)。例如:
-- 表user的id为主键,数据:(1,Alice),(5,Bob),(10,Charlie)
BEGIN;
SELECT * FROM user WHERE id <=5 FOR UPDATE; -- 锁住id=5的记录 + 间隙(-∞,1)和(1,5)
此时其他事务:
-
修改id=5的记录 → 阻塞;
-
插入id=3的记录 → 阻塞;
-
修改id=10的记录 → 正常。
五、实战避坑:锁相关问题与解决方案
1. 死锁:事务互相等待对方释放锁
(1)死锁案例
-- 会话1
BEGIN;
UPDATE product SET stock = 99 WHERE id = 1; -- 持有id=1的X锁
-- 会话2
BEGIN;
UPDATE product SET stock = 49 WHERE id = 2; -- 持有id=2的X锁
-- 会话1
UPDATE product SET stock = 49 WHERE id = 2; -- 等待会话2释放id=2的X锁
-- 会话2
UPDATE product SET stock = 99 WHERE id = 1; -- 等待会话1释放id=1的X锁 → 死锁
(2)死锁排查
通过SHOW ENGINE INNODB STATUS;查看死锁日志:
------------------------
LATEST DETECTED DEADLOCK
------------------------
...
TRANSACTION 12345:
UPDATE product SET stock = 49 WHERE id = 2
TRANSACTION 12346:
UPDATE product SET stock = 99 WHERE id = 1
...
(3)解决方案
-
固定SQL执行顺序:所有事务按相同的索引顺序操作数据;
-
设置锁超时 :通过
innodb_lock_wait_timeout(默认50秒)控制锁等待时间; -
减少锁持有时间:事务中尽量晚加锁,早提交。
2. 索引失效导致行锁升级为表锁
InnoDB行锁依赖索引,若查询条件无索引(或索引失效),会退化为表级锁!
(1)案例演示
-- 表product的name为普通索引,id为主键
-- 会话1:查询条件用name,但故意让索引失效(如函数操作)
BEGIN;
SELECT * FROM product WHERE UPPER(name) = 'IPHONE' FOR UPDATE; -- 索引失效,表锁
-- 会话2:修改任意记录(阻塞,因为表锁)
UPDATE product SET stock = 98 WHERE id = 2; -- 阻塞
(2)解决方案
-
避免在索引列上做函数/运算操作(如
UPPER(name)、id+1); -
确保查询条件使用有效索引(通过
EXPLAIN检查执行计划)。
3. 乐观锁:无锁化并发控制
乐观锁假设"并发冲突概率低",通过版本号/时间戳实现,无需加锁,适合读多写少场景。结合MyBatis-Plus实现如下:
(1)表结构(新增version字段)
CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '商品名称',
`stock` INT NOT NULL COMMENT '库存',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
(2)Java代码实现(Spring Boot + MyBatis-Plus)
pom.xml核心依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
实体类Product.java:
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
/**
* 商品实体类
* @author ken
*/
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer stock;
@Version // 乐观锁版本号注解
private Integer version;
}
Service实现类ProductServiceImpl.java:
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Product;
import com.jam.demo.mapper.ProductMapper;
import com.jam.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
/**
* 商品服务实现类
* @author ken
*/
@Slf4j
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deductStockOptimistic(Long id) {
if (ObjectUtils.isEmpty(id)) {
log.error("商品ID不能为空");
return false;
}
Product product = getById(id);
if (ObjectUtils.isEmpty(product) || product.getStock() <= 0) {
log.error("商品库存不足或不存在");
return false;
}
// 扣减库存
product.setStock(product.getStock() - 1);
// MyBatis-Plus自动拼接WHERE version=#{oldVersion},更新成功则version+1
return updateById(product);
}
}
Controller类ProductController.java:
package com.jam.demo.controller;
import com.jam.demo.service.ProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 商品控制器
* @author ken
*/
@RestController
@RequestMapping("/product")
@RequiredArgsConstructor
@Tag(name = "商品管理接口", description = "商品库存操作接口")
public class ProductController {
private final ProductService productService;
@PostMapping("/deduct/optimistic")
@Operation(summary = "乐观锁扣减库存", description = "基于Version字段实现无锁并发控制")
public String deductStockOptimistic(
@Parameter(description = "商品ID", required = true) @RequestParam Long id) {
boolean success = productService.deductStockOptimistic(id);
return success ? "库存扣减成功" : "库存扣减失败(并发冲突)";
}
}
六、锁机制优化最佳实践
-
优先使用行级锁:通过索引实现行锁,避免表锁降低并发;
-
减少锁持有时间:事务中仅在必要时加锁,逻辑尽量简洁,及时提交;
-
合理选择隔离级别:读多写少用RC(减少间隙锁),写多读少用RR(保证一致性);
-
避免长事务:长事务会长期占用锁,引发锁等待和死锁;
-
索引设计合理化:确保查询条件走索引,防止行锁升级为表锁;
-
乐观锁替代悲观锁:读多写少场景用乐观锁(如Version),降低锁竞争。
七、总结
MySQL锁机制是并发控制的基石,其核心是**"粒度越小,并发越高,实现越复杂"**。全局锁和表锁适用于简单场景,而行锁(尤其是Next-Key Lock)是InnoDB高并发的关键。掌握锁的底层原理(如MVCC、索引依赖)、避开常见坑(索引失效、死锁)、结合实战优化(乐观锁/悲观锁选型),才能真正驾驭MySQL的并发能力。
锁的本质是平衡"并发效率"与"数据一致性",理解业务场景,选择合适的锁策略,才是最优解。