当前位置: 代码网 > it编程>数据库>Mysql > Mysql数据库乐观锁与悲观锁示例详解

Mysql数据库乐观锁与悲观锁示例详解

2026年04月02日 Mysql 我要评论
乐观锁与悲观锁是两种常见的并发控制机制,用于解决多用户同时操作同一数据时的一致性问题一、悲观锁(pessimistic locking)1. 原理假设:并发冲突很可能发生,因此在读取数据时就加锁,防止

乐观锁悲观锁是两种常见的并发控制机制,用于解决多用户同时操作同一数据时的一致性问题

一、悲观锁(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 悲观锁 对比

特性悲观锁乐观锁
并发性能低(串行化)高(无锁)
一致性保障强(事务隔离)最终一致(需处理冲突)
适用场景写多读少、冲突频繁读多写少、冲突较少
实现复杂度简单(sql 加锁)需版本字段 + 重试逻辑
死锁风险
典型应用银行转账、订单支付商品库存、点赞、评论计数

四、在 gin 项目中的选型建议

场景推荐锁类型说明
转账、资金结算悲观锁强一致性要求高,不能出错
秒杀、抢购库存扣减乐观锁 + 重试 或 redis 预减库存高并发下悲观锁性能差
用户资料编辑乐观锁冲突少,体验好
订单状态变更(如支付)悲观锁 或 状态机校验防止重复支付/状态错乱

💡 高并发场景(如秒杀)通常不直接依赖数据库锁,而是:

  • 使用 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)

  1. 参数校验(用户 id、商品 id)
  2. 防重放:检查是否已下单(可用 redis set user:1001:product:1001
  3. lua 脚本原子扣减库存
    • 若库存 > 0,则 decr 并返回成功
    • 否则返回“库存不足”
  4. 发送消息到 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、关键设计点与注意事项

问题解决方案
redis 与 mysql 数据不一致异步消费时做 mysql 库存二次校验;定期对账补偿
mq 消息丢失使用可靠消息(kafka 副本、rabbitmq 持久化 + ack)
重复消费消费端幂等(如订单表加唯一索引 (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数据库乐观锁与悲观锁示例详解的文章就介绍到这了,更多相关mysql乐观锁与悲观锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com