乐观锁与悲观锁是两种常见的并发控制机制,用于解决多用户同时操作同一数据时的一致性问题
一、悲观锁(Pessimistic Locking)
1. 原理
假设:并发冲突很可能发生,因此在读取数据时就加锁,防止其他事务修改。适用于写操作频繁、冲突概率高的场景。2. MySQL 中的实现
通过
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE(8.0 后推荐用FOR SHARE)实现行级锁(InnoDB 引擎)。
-- 排他锁(写锁):其他事务不能读(除非快照读)、不能写 SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 共享锁(读锁):允许多个事务读,但阻止写 SELECT * FROM accounts WHERE id = 1 FOR SHARE;
⚠️ 必须在事务中使用,否则锁会立即释放。
3. Gin + GORM 示例(悲观锁)
func TransferHandler(c *gin.Context) { tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() var fromAccount Account // 悲观锁:锁定 from 账户 if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", 1).First(&fromAccount).Error; err != nil { tx.Rollback() c.JSON(400, gin.H{"error": "账户不存在"}) return } var toAccount Account if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", 2).First(&toAccount).Error; err != nil { tx.Rollback() c.JSON(400, gin.H{"error": "目标账户不存在"}) return } if fromAccount.Balance < 100 { tx.Rollback() c.JSON(400, gin.H{"error": "余额不足"}) return } fromAccount.Balance -= 100 toAccount.Balance += 100 tx.Save(&fromAccount) tx.Save(&toAccount) tx.Commit() c.JSON(200, gin.H{"msg": "转账成功"}) }
✅ 优点:强一致性,避免脏读/丢失更新
❌ 缺点:性能差(锁等待)、易死锁、降低并发
二、乐观锁(Optimistic Locking)
1. 原理
假设:并发冲突很少发生,因此不加锁,只在更新时检查数据是否被他人修改。通常通过 版本号(version)字段 或 时间戳 实现。2. MySQL 中的实现
表结构需包含
version字段(整型):
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), stock INT, version INT DEFAULT 0 );
更新时带上版本号条件:
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5; -- 只有 version 未变才更新
如果返回
affected_rows == 0,说明数据已被他人修改,需重试或报错。
3. Gin + GORM 示例(乐观锁)
GORM 内置支持乐观锁(需使用
gorm.DeletedAt同包下的Version字段):
type Product struct { ID uint `gorm:"primarykey"` Name string Stock int Version uint32 // GORM 自动识别为乐观锁字段 } func ReduceStock(c *gin.Context) { var product Product id := c.Param("id") // 第一次读取 if err := db.First(&product, id).Error != nil { c.JSON(404, gin.H{"error": "商品不存在"}) return } // 业务逻辑:扣减库存 if product.Stock <= 0 { c.JSON(400, gin.H{"error": "库存不足"}) return } // 尝试更新(GORM 自动在 WHERE 中加入 version 条件) product.Stock-- result := db.Save(&product) if result.Error != nil { c.JSON(500, gin.H{"error": "数据库错误"}) return } if result.RowsAffected == 0 { // 乐观锁失败:版本不匹配 c.JSON(409, gin.H{"error": "库存已被其他请求修改,请重试"}) return } c.JSON(200, gin.H{"msg": "扣减成功", "stock": product.Stock}) }
✅ 优点:高并发、无锁、性能好
❌ 缺点:冲突时需重试、不适合高频写冲突场景
三、乐观锁 vs 悲观锁 对比
四、在 Gin 项目中的选型建议
???? 高并发场景(如秒杀)通常不直接依赖数据库锁,而是:
使用 Redis 预减库存 + 队列异步落库结合 Lua 脚本保证原子性数据库仅做最终一致性校验
五、GORM 乐观锁注意事项
字段名必须为Version(类型 uint32 或 int)GORM 在 Save() 或 Update() 时自动添加 WHERE version = ? 并递增若使用 Updates(map),需手动包含 version 字段
六.总结
悲观锁:适合强一致性、低并发写场景,用FOR UPDATE + 事务。乐观锁:适合高并发、冲突少场景,用 version 字段 + 重试机制。在 Gin + GORM 项目中,根据业务特性选择合适方案,必要时结合缓存(Redis)提升性能。
实际项目中,混合使用也很常见:核心资金用悲观锁,普通业务用乐观锁。
七.Redis 预减库存 + 消息队列异步落库
在高并发场景(如秒杀、抢购)中,直接操作数据库扣减库存极易导致性能瓶颈、超卖甚至系统崩溃。因此,业界普遍采用 “Redis 预减库存 + 消息队列异步落库” 的架构来兼顾 高性能、一致性与可靠性
1、整体架构图
用户请求 │ ▼ [ Gin Web 服务 ] ←─┐ │ │ ▼ │ [ Redis 预减库存 ] │ ←─ 库存校验 & 原子扣减(Lua 脚本) │ │ ▼ │ [ 发送消息到 MQ ] ─┘ → [ Kafka / RabbitMQ / RocketMQ ] │ ▼ [ 异步消费服务 ] │ ▼ [ MySQL 落库 ] ←─ 订单创建、库存最终扣减、记录日志 │ ▼ [ 返回结果给用户(可延迟)]
✅ 核心思想:
快速响应:Redis 操作毫秒级,用户几乎无等待削峰填谷:MQ 缓冲瞬时高并发最终一致:异步确保数据持久化
2、核心步骤详解
步骤 1:初始化库存到 Redis
系统启动或活动开始前,将商品库存同步到 Redis。使用 String 类型 或 Hash 存储,如stock:product:1001 = 100
// 初始化库存(管理后台或定时任务调用) redisClient.Set(ctx, "stock:product:1001", 100, 0)
步骤 2:用户请求秒杀接口(Gin Handler)
- 参数校验(用户 ID、商品 ID)防重放:检查是否已下单(可用 Redis Set
user:1001:product:1001)Lua 脚本原子扣减库存若库存 > 0,则 DECR 并返回成功否则返回“库存不足”发送消息到 MQ(仅当 Redis 扣减成功)⚠️ 关键:Redis 扣减必须是原子操作,防止超卖!
步骤 3:Lua 脚本实现原子预减库存
-- stock_decrease.lua local key = KEYS[1] local userId = ARGV[1] -- 1. 检查是否已抢购(防重) if redis.call("EXISTS", "seckill:user:" .. userId .. ":product:" .. string.match(key, ":(%d+)$")) == 1 then return -2 -- 已参与 end -- 2. 获取当前库存 local stock = tonumber(redis.call("GET", key)) if not stock or stock <= 0 then return -1 -- 库存不足 end -- 3. 扣减库存 redis.call("DECR", key) -- 4. 记录用户已参与(防重,TTL 可选) redis.call("SET", "seckill:user:" .. userId .. ":product:" .. string.match(key, ":(%d+)$"), "1", "EX", 3600) return stock - 1
返回值含义:
-2:已抢过-1:库存不足>=0:剩余库存,表示成功
步骤 4:Gin 处理秒杀请求(Go 代码)
// main.go 或 handler/seckill.go func SeckillHandler(c *gin.Context) { userID := c.GetString("user_id") // 假设已鉴权 productID := c.Param("product_id") // 构造 Redis Key stockKey := fmt.Sprintf("stock:product:%s", productID) userProductKey := fmt.Sprintf("seckill:user:%s:product:%s", userID, productID) // 执行 Lua 脚本 result, err := redisClient.Eval( ctx, luaScript, // 上述 Lua 脚本内容 []string{stockKey}, userID, ).Result() if err != nil { c.JSON(500, gin.H{"error": "系统繁忙"}) return } switch ret := result.(type) { case int64: if ret == -1 { c.JSON(400, gin.H{"error": "库存不足"}) return } if ret == -2 { c.JSON(400, gin.H{"error": "您已参与过本次秒杀"}) return } default: c.JSON(500, gin.H{"error": "未知错误"}) return } // 成功!发送消息到 MQ(异步落库) msg := SeckillMessage{ UserID: userID, ProductID: productID, Timestamp: time.Now(), } // 序列化并发送到 Kafka / RabbitMQ if err := mqProducer.Send("seckill_queue", msg); err != nil { // 注意:此处即使 MQ 发送失败,Redis 已扣减,需有补偿机制! log.Printf("MQ send failed: %v", err) // 可考虑回滚 Redis(复杂),或依赖后续对账 } // 立即返回用户“抢购成功,请等待订单生成” c.JSON(200, gin.H{ "msg": "抢购成功!正在生成订单...", "queue_status": "processing", }) }
步骤 5:异步消费服务(Worker)
// worker/seckill_worker.go func StartSeckillWorker() { for msg := range mqConsumer.Subscribe("seckill_queue") { var seckillMsg SeckillMessage if err := json.Unmarshal(msg, &seckillMsg); err != nil { continue } // 开启事务,落库 tx := db.Begin() defer tx.Rollback() // 1. 再次校验(兜底):MySQL 中库存是否足够? var product Product if err := tx.Where("id = ? AND stock > 0", seckillMsg.ProductID).First(&product).Error; err != nil { log.Printf("MySQL 库存不足或商品不存在: %v", seckillMsg) continue // 丢弃消息 or DLQ } // 2. 创建订单 order := Order{ UserID: seckillMsg.UserID, ProductID: seckillMsg.ProductID, Status: "created", } if err := tx.Create(&order).Error != nil { continue } // 3. 扣减 MySQL 库存 if err := tx.Model(&Product{}). Where("id = ? AND stock = ?", seckillMsg.ProductID, product.Stock). Update("stock", gorm.Expr("stock - 1")).Error; err != nil { continue } tx.Commit() log.Printf("订单创建成功: %v", order.ID) } }
???? 兜底校验很重要!防止 Redis 与 MySQL 数据不一致(如 Redis 重启未同步)。
3、关键设计点与注意事项
(user_id, product_id))Redis 宕机高可用部署(Redis Cluster / Sentinel)超卖Lua 脚本保证原子性 + MySQL 兜底校验用户重复提交Redis 记录 user:product 防重键(带 TTL)
4、扩展:失败补偿与对账
定时对账任务:每天对比 Redis 初始库存、Redis 当前库存、MySQL 已售数量,发现差异则告警或自动修复。死信队列(DLQ):处理多次失败的消息,人工介入。前端轮询/WebSocket:告知用户“订单已生成”,提升体验。5、总结
✅ 优势:
高并发:Redis 承载 10w+ QPS防超卖:Lua 原子操作系统解耦:MQ 异步削峰最终一致:异步落库 + 兜底校验❌ 复杂度:
需维护 Redis + MQ + 对账系统调试和监控难度增加???? 适用场景:秒杀、抢购、限量发放等高并发、低转化率业务
到此这篇关于Mysql数据库乐观锁与悲观锁示例详解的文章就介绍到这了,
