从整体角度理解服务中的行锁
1 简介
MySQL 的行锁(FOR UPDATE)。 在理解 MySQL 的 行锁(FOR UPDATE) 时,我们可以从以下整体性、层次性、动态性和关联性方面进行分析。
2 整体性(Holism):行锁在数据库系统中的作用
在数据库事务管理中,数据一致性 是核心目标,而锁机制是实现一致性的重要手段。
行锁(Row Lock) 是 MySQL InnoDB 存储引擎提供的细粒度锁,通过索引加锁来减少并发冲突。
FOR UPDATE 语句用于在事务中锁定查询到的行,防止其他事务并发修改。
作用:
确保数据一致性,防止丢失更新问题(Lost Update)
避免幻读,适用于可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)
保证操作的原子性,确保多个事务不会同时修改相同数据
3 层次性(Hierarchy):MySQL 锁的层次
在 MySQL 事务管理中,锁具有不同的粒度,从粗到细包括:
表锁(Table Lock):锁住整张表,影响所有行,适用于 MyISAM(影响整个业务)。
页锁(Page Lock):锁定某一页的数据,影响一部分行,适用于 B+ 树索引。
行锁(Row Lock):锁住特定行,FOR UPDATE 主要作用于行锁,减少并发冲突,提高吞吐量。
行锁的具体表现:
SELECT ... FOR UPDATE:锁定查询到的行,防止其他事务修改。
SELECT ... LOCK IN SHARE MODE:共享锁,允许其他事务读取,但不允许修改。
隐式锁 vs 显式锁:
显式锁:使用 FOR UPDATE 或 LOCK IN SHARE MODE
隐式锁:在 UPDATE 或 DELETE 语句执行时,自动加锁
4 动态性(Dynamism):事务执行中的锁行为
锁的行为随着事务执行而动态变化:
事务开启(BEGIN)
查询数据并加锁(SELECT … FOR UPDATE)
执行更新操作
事务提交(COMMIT,释放行锁)或事务回滚(ROLLBACK,释放行锁)
示例:
BEGIN;
SELECT * FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
注意事项:
事务未提交时,行锁不会释放
长时间持有锁可能导致死锁或阻塞其他事务
加锁必须基于索引,否则可能升级为表锁
5 关联性(Interrelation):FOR UPDATE 在业务中的应用
在高并发业务场景下,行锁常用于防止数据并发修改导致不一致,典型应用包括:
库存扣减(防止超卖)
银行转账(确保余额正确性)
任务分配(防止任务重复执行)
分布式锁(结合数据库实现分布式事务)
使用GORM 实现 FOR UPDATE(库存扣减示例)
下面我们实现一个GORM 的服务,用 FOR UPDATE 机制确保并发扣减库存的正确性。
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
stock INT NOT NULL
);
INSERT INTO products (name, stock) VALUES ('iPhone 15', 10);
-
实现的GORM 代码
// Product 模型 type Product struct { ID uint `gorm:"primaryKey"` Name string `gorm:"type:varchar(255)"` Stock int } var db *gorm.DB func main() { // 连接 MySQL dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local" var err error db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("数据库连接失败:", err) } r := gin.Default() r.POST("/buy/:id", buyProduct) // 购买商品接口 r.Run(":8080") // 启动服务 } // 购买商品(FOR UPDATE 防止超卖) func buyProduct(c *gin.Context) { var product Product productID := c.Param("id") // 开启事务 tx := db.Begin() if err := tx.Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "无法开启事务"}) return } // 查询产品并加锁 if err := tx.Raw("SELECT * FROM products WHERE id = ? FOR UPDATE", productID).Scan(&product).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "查询产品失败"}) return } // 检查库存 if product.Stock <= 0 { tx.Rollback() c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) return } // 扣减库存 if err := tx.Exec("UPDATE products SET stock = stock - 1 WHERE id = ?", productID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "库存扣减失败"}) return } // 提交事务 if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "事务提交失败"}) return } c.JSON(http.StatusOK, gin.H{"message": "购买成功", "剩余库存": product.Stock - 1}) }
6 总结
多维度 FOR UPDATE 的体现
整体性 确保数据一致性,防止超卖
层次性 只锁定特定行,避免影响其他行
动态性 锁的释放和持有受事务控制
关联性 适用于库存扣减、银行转账、任务调度等场景
最佳实践:
基于索引加锁,避免表锁
减少事务持有时间,防止长时间锁定
考虑 MySQL 隔离级别,防止幻读
使用超时机制(innodb_lock_wait_timeout)避免死锁
这种方式的整体思维,帮助理解 MySQL 行锁(FOR UPDATE) 的本质及应用,并通过GORM 提供了一个可落地的实现方案。
- 点赞
- 收藏
- 关注作者
评论(0)