SQLite WAL + BEGIN IMMEDIATE:给卡密兑换加一把原子锁

举报
yd_268445051 发表于 2026/10/23 12:53:35 2026/10/23
【摘要】 最近做一个卡密兑换小站时,踩了一个经典坑:库存用 JSON 文件存,两个请求同时兑换同一张卡密时都读到"未使用",结果一张卡发出了两份货——超卖了。 为什么 JSON 文件扛不住并发读-改-写三步不是原子的。请求 A 读文件、请求 B 读文件,两边拿到的是同一份快照;各自标记一张卡为已用、写回,后写的覆盖先写的,库存就凭空多出来一张。加文件锁能缓解,但代码复杂度和出错概率都上去了。 换 SQ...

最近做一个卡密兑换小站时,踩了一个经典坑:库存用 JSON 文件存,两个请求同时兑换同一张卡密时都读到"未使用",结果一张卡发出了两份货——超卖了。

为什么 JSON 文件扛不住并发

读-改-写三步不是原子的。请求 A 读文件、请求 B 读文件,两边拿到的是同一份快照;各自标记一张卡为已用、写回,后写的覆盖先写的,库存就凭空多出来一张。加文件锁能缓解,但代码复杂度和出错概率都上去了。

换 SQLite:WAL + 立即事务

把库存迁到 SQLite 后,我用了两个关键设置:

  1. WAL 模式:PRAGMA journal_mode=WAL; 读写不互斥,读性能好很多。
  2. 兑换时用 BEGIN IMMEDIATE:事务一开始就拿写锁,后来的兑换请求只能排队等,从根本上杜绝两个事务同时挑中同一张卡。

核心兑换逻辑(Python 标准库 sqlite3):

import sqlite3

def redeem(db_path, card_key, qty):
    conn = sqlite3.connect(db_path, timeout=10)
    try:
        conn.execute("PRAGMA journal_mode=WAL;")
        conn.execute("BEGIN IMMEDIATE")          # 先拿写锁
        row = conn.execute(
            "SELECT stock FROM cards WHERE key=?", (card_key,)
        ).fetchone()
        if row is None or row[0] < qty:
            conn.execute("ROLLBACK")            # 库存不足,整单回滚
            return None
        items = conn.execute(
            "SELECT id, payload FROM pool WHERE status='free' LIMIT ?",
            (qty,),
        ).fetchall()
        if len(items) < qty:
            conn.execute("ROLLBACK")
            return None
        ids = [i[0] for i in items]
        conn.execute(
            f"UPDATE pool SET status='used' WHERE id IN ({','.join('?'*len(ids))})",
            ids,
        )
        conn.execute(
            "UPDATE cards SET stock=stock-? WHERE key=?", (qty, card_key)
        )
        conn.execute("COMMIT")
        return [i[1] for i in items]
    finally:
        conn.close()

三个容易被忽略的点

  • 先查库存再扣减,且都在同一个事务里。把任何一步挪到事务外,并发窗口就又回来了。
  • 库存不足要整单回滚,不能发一部分。ROLLBACK 后连接别忘了关,WAL 文件会自己 checkpoint。
  • timeout 要给够。BEGIN IMMEDIATE 拿不到锁会抛 database is locked,连接层面的 busy timeout 能让短时排队自动等待,而不是直接报错。

小结

单机小站没必要一上来就上 MySQL/Redis。SQLite 的 WAL + BEGIN IMMEDIATE 用标准库就能实现兑换原子性,我这边 44 项并发与幂等回归测试全过,再没出现过超卖。对同类"库存池 + 按量发放"的场景,这套组合够用且省心。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。