在服务开发中使用间隙锁
1 简介
间隙锁Gap 锁 是 MySQL 的 InnoDB 存储引擎中使用的一种特殊锁,用于锁定索引范围中的空隙,防止其他事务插入数据到该范围中。它是 InnoDB 存储引擎在实现 可重复读 隔离级别时使用的锁。
Gap 锁的使用可以有效避免在 可重复读 隔离级别下出现幻读(即事务读取到的数据,在同一事务内发生了变化),并且可以避免插入死锁的发生。
2. 什么是 Gap 锁
Gap 锁(间隙锁)详解
(1) 定义
Gap 锁(间隙锁)是 MySQL InnoDB 存储引擎 在可重复读(REPEATABLE READ)隔离级别下,为了防止 幻读(Phantom Read) 而使用的一种锁定机制。它的主要作用是:
锁定索引范围的空隙(Gap),即某些不存在的行,防止其他事务向该范围插入数据。
确保查询结果的一致性,避免同一事务中两次查询得到不同的结果。
(2) 设计目的
防止幻读:在 REPEATABLE READ 隔离级别下,防止其他事务在某个索引范围内插入新记录,导致当前事务两次查询结果不一致。
避免数据插入:即使某些数据不存在,Gap 锁仍然会锁住这些空隙,防止其他事务插入新数据。
3 对比Gap 锁 及其适用场景
对比项 Gap 锁(间隙锁) 全局锁(Global Lock)
作用范围 索引范围内的空隙 整个 MySQL 实例
影响范围 仅影响某些索引范围的插入操作 影响所有数据库的写操作
锁定类型 行级锁的一部分(索引级锁) 服务器级锁
适用场景 可重复读隔离级别,防止幻读 备份、迁移
影响的 SQL 操作 INSERT、UPDATE、DELETE 所有写操作
- Gap 锁的使用场景
(1) 防止幻读
事务 1 先执行 SELECT … FOR UPDATE,如果没有 Gap 锁,事务 2 可能会在这个范围插入新数据,导致事务 1 读出的数据发生变化(幻读)。
(2) 确保范围查询的一致性
当执行范围查询(如 WHERE age > 20 FOR UPDATE),Gap 锁会锁定索引范围内的空隙,防止其他事务插入 age=21 的新记录。
4. MySQL 中 Gap 锁的使用示例
(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) 测试 Gap 锁
事务 1:执行范围查询
START TRANSACTION;
SELECT * FROM employees WHERE age BETWEEN 25 AND 35 FOR UPDATE;
这条 SQL 语句将执行以下动作:
锁住已有数据(age = 25, 30, 35)。
锁住 age ∈ (25, 35) 这个范围的“空隙”,防止其他事务插入 age=26, 27,… 34 的新记录。
事务 2:尝试插入数据
INSERT INTO employees (name, age) VALUES ('David', 28);
将被阻塞!
由于 28 处于 25-35 的范围,Gap 锁阻止插入,直到事务 1 提交或回滚。
事务 1 提交
COMMIT;
事务 2 现在可以继续执行插入。
4 在web服务中使用间隙锁
(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: 查询数据并加 Gap 锁
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": "插入失败,可能被 Gap 锁阻止"})
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")
}
5. 小结论
(1) 间隙锁的作用
主要用于防止幻读,确保范围查询的一致性。
锁住索引范围的“空隙”,防止其他事务插入新数据。
(2) Gap 锁 vs. 全局锁
对比项 Gap 锁(间隙锁) 全局锁(Global Lock)
作用范围 某个索引范围 整个 MySQL 服务器
影响操作 INSERT(防止幻读) 所有写操作
适用场景 可重复读隔离级别 备份、迁移
加锁方式 行级索引锁 + Gap 锁 服务器级锁
Gap 锁 主要用于 防止并发事务中的幻读,只影响索引范围的插入操作,不会影响查询和更新。
全局锁 则是 影响整个 MySQL 实例,用于备份、数据迁移等场景。
API 结合 Gap 锁 可以 模拟事务的加锁机制,防止不一致的数据插入。
当你的业务需要 严格控制事务范围内的数据一致性(如银行转账、订单处理),Gap 锁是非常关键的锁机制!
- 点赞
- 收藏
- 关注作者
评论(0)