在服务开发中使用NEXT-KEY锁
1 简介
本文解释Next-Key 锁 是结合了行锁和间隙锁的锁定机制。
在 InnoDB 存储引擎中,Next-Key 锁 会锁住一个索引记录以及它前后的间隙。
它既能锁定某一行数据,也能防止插入新数据行进入该范围。
使用Next-Key 锁用于防止事务插入新的行数据到锁定范围内,以避免幻读的发生。
2 详解 Next-Key 锁
- (1) 定义
Next-Key 锁是 MySQL InnoDB 存储引擎 在可重复读(REPEATABLE READ)隔离级别下,用于 防止幻读(Phantom Read) 的一种锁定机制。
它结合了:
行锁(Record Lock):锁住当前索引的具体行。
Gap 锁(间隙锁):锁住索引范围内的“空隙”。
最终,Next-Key 锁 锁定了索引记录及其前后的间隙,防止:
其他事务修改该行(行锁)
其他事务在该范围内插入新行(Gap 锁)
- (2) 设计目的
防止幻读:在 REPEATABLE READ 隔离级别下,防止其他事务在某个范围插入新记录,导致事务两次查询结果不同。
确保范围查询的一致性:避免在事务过程中,范围内的记录发生变化。
3 Next-Key 锁 vs. 全局锁
对比项 Next-Key 锁 全局锁(Global Lock)
作用范围 某个索引的具体行和其前后的间隙 整个 MySQL 实例
影响范围 行级别和范围内的插入 所有数据库的写操作
锁定类型 索引级锁 服务器级锁
适用场景 事务操作,防止幻读 全库备份、迁移
影响SQL SELECT ... FOR UPDATE, 所有写操作.
INSERT,UPDATE,DELETE
- Next-Key 锁的使用场景
(1) 防止幻读
事务 1 先执行 SELECT … FOR UPDATE,如果没有 Next-Key 锁,事务 2 可能会在该范围插入新数据,导致事务 1 读出的数据发生变化(幻读)。
(2) 确保范围查询的一致性
当执行范围查询(如 WHERE age > 25 FOR UPDATE),Next-Key 锁锁定索引记录及其前后的间隙,防止其他事务插入 age=26, 27, … 的新记录。
5 MySQL 中 Next-Key 锁的使用示例
(1) 创建测试表
CREATE TABLE employees (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL,
INDEX idx_age (age) -- 创建索引
) ENGINE=InnoDB;
(2) 插入初始数据
INSERT INTO employees (name, age) VALUES ('Alice', 25), ('Bob', 30), ('Charlie', 35);
(3) 测试 Next-Key 锁
事务 1:执行范围查询
START TRANSACTION;
SELECT * FROM employees WHERE age BETWEEN 25 AND 35 FOR UPDATE;
这条 SQL 语句会:
锁住已有数据(age = 25, 30, 35)(行锁)。
锁住 age ∈ (25, 35) 这个范围的“空隙”(Gap 锁)。
最终形成 Next-Key 锁,阻止其他事务插入或修改该范围的任何数据。
事务 2:尝试插入数据
INSERT INTO employees (name, age) VALUES ('David', 28);
这将被阻塞!
因为 28 落在 25-35 的范围,Next-Key 锁阻止插入,直到事务 1 提交或回滚。
事务 1 提交
COMMIT;
事务 2 现在可以继续执行插入。
6. 在web服务中使用 Next-Key 锁
(1) 代码示例
var db *sql.DB
func init() {
var err error
dsn := "root:password@tcp(127.0.0.1:3306)/testdb?parseTime=true"
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
}
// 事务 1: 查询数据并加 Next-Key 锁
func transaction1(c *gin.Context) {
tx, err := db.Begin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "事务开启失败"})
return
}
// 执行查询并加锁
_, err = tx.Exec("SELECT * FROM employees WHERE age BETWEEN 25 AND 35 FOR UPDATE")
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
// 模拟长时间事务
time.Sleep(10 * time.Second)
tx.Commit()
c.JSON(http.StatusOK, gin.H{"message": "事务 1 执行完成"})
}
// 事务 2: 插入数据
func transaction2(c *gin.Context) {
_, err := db.Exec("INSERT INTO employees (name, age) VALUES ('David', 28)")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "插入失败,可能被 Next-Key 锁阻止"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "插入成功"})
}
func main() {
r := gin.Default()
r.GET("/tx1", transaction1) // 执行事务 1(锁定范围)
r.GET("/tx2", transaction2) // 执行事务 2(尝试插入)
r.Run(":8080")
}
7 小结
(1) Next-Key 锁的作用
既锁住索引行,又锁住范围,防止其他事务修改或插入新数据。
防止幻读,确保事务的一致性。
(2) Next-Key 锁 vs. 全局锁
对比项 Next-Key 锁 全局锁(Global Lock)
作用范围 索引记录和其前后的间隙 整个 MySQL 服务器
影响操作 INSERT、UPDATE、DELETE(范围锁定) 所有写操作
适用场景 防止幻读,事务一致性 备份、迁移
加锁方式 行级锁和Gap 锁 服务器级锁
Next-Key 锁 主要用于 防止并发事务中的幻读,锁住索引行及其前后的间隙,防止数据变动。
全局锁 则是 影响整个 MySQL 实例,用于备份、数据迁移等场景。
服务 API 结合 Next-Key 锁 可以 模拟事务的加锁机制,确保范围内的数据一致性。
当你的业务需要 严格控制事务范围内的数据一致性(如订单管理、库存系统),Next-Key 锁是非常关键的锁机制!
- 点赞
- 收藏
- 关注作者
评论(0)