惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
NISL@THU
NISL@THU
T
Threat Research - Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
Lohrmann on Cybersecurity
V
Visual Studio Blog
Cyberwarzone
Cyberwarzone
D
Docker
The Hacker News
The Hacker News
C
CERT Recently Published Vulnerability Notes
Vercel News
Vercel News
Project Zero
Project Zero
S
Schneier on Security
aimingoo的专栏
aimingoo的专栏
I
Intezer
腾讯CDC
M
MIT News - Artificial intelligence
Hugging Face - Blog
Hugging Face - Blog
P
Palo Alto Networks Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
AWS News Blog
AWS News Blog
GbyAI
GbyAI
MongoDB | Blog
MongoDB | Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Vulnerabilities – Threatpost
G
Google Developers Blog
N
Netflix TechBlog - Medium
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
A
Arctic Wolf
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
Cisco Talos Blog
Cisco Talos Blog
Recent Announcements
Recent Announcements
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
T
Threatpost
Latest news
Latest news
Blog — PlanetScale
Blog — PlanetScale
Security Latest
Security Latest
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
The GitHub Blog
The GitHub Blog
T
Tor Project blog
P
Proofpoint News Feed

博客园 - 一名程序媛呀

Anki插件开发必知必会:钩子函数与右键菜单定制 httpx 传参总报错?这次把 GET、POST、文件上传到响应处理的坑给你一次填平 从卡顿到丝滑:FastAPI 调用外部 API 的正确姿势(httpx 实战) 还在 XHR、Fetch 和 Axios 之间纠结?我踩过的坑,希望你一个都不用碰到 你的REST接口还在“过度投喂”数据吗?——FastAPI + GraphQL实战避坑指南 Uvicorn、Gunicorn 傻傻分不清?FastAPI 生产部署避坑指南 Termux里的二进制和脚本,到底怎么运行才不踩坑?Termux-service 保活妙招! 刚部署的 LibreTranslate 频频翻车?我掏出了 20 年前的 StarDict 词典,用 FastAPI 搭了个本地词典翻译 API 别再用网页翻译看源码了!你的私人翻译神器LibreTranslate,部署避坑指南来了 掏出手机就能搭个 WebDAV 同步服务器?这操作有点香 别只盯着GitBook了!这个文档神器让你的笔记秒变网站 写爬虫时用了代理还被封?Python 代理的那些隐藏坑,我替你踩明白了 FastAPI 身份验证总踩坑?这份 FastAPI Users “避坑指南”请收好 旧手机别扔!用 Termux 搭个私人云盘,比网盘香多了 你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透 写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了 前端包管理咋选?我从npm叛逃到pnpm的血泪史(附避坑指南) 聊聊 fetch 使用中我踩过的那些坑和正确打开方式 FastApiAdmin 后端接口开发好了,前端管理界面怎么调用与显示? 给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍 我用了FastApiAdmin后,连夜把踩过的坑都整理出来了 告别 Typora 后的新欢:我把所有笔记迁移到了 Obsidian 这个“第二大脑” 你的Agent API还在裸奔?从认证到沙箱,我用FastAPI搭了几道防线 让 FastAPI Agent 思考不阻塞:手把手教你实现异步任务与后台处理方案 让FastAPI Agent真正记住你:聊聊会话记忆与持久化存储的落地实践 FastAPI Agent 函数调用实战:我让 AI 学会了“自己动手查天气“ 初探:用 FastAPI 搭建你的第一个 AI Agent 接口 FastAPI 少有人提的实用技巧:把 Depends 依赖提到路由层,代码少写60% FastAPI 生产环境静态文件完全指南:从 /favicon.ico 404 到 HSTS 混合内容,一次全根治 用了loguru我才明白,Python日志还能这么写 FastAPI 后台任务:BackgroundTasks 的使用场景与注意事项 FastAPI配置管理避坑指南:从硬编码到 .env 与 pydantic_settings 类,连路由用法都给你捋清楚 FastAPI 文件上传避坑全指南:分块存盘、类型校验与安全兜底 FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上 FastAPI + SQLAlchemy 2.0 通用CRUD操作手册 —— 从同步到异步,一次讲透 FastAPI 生产环境避坑指南:用 Alembic 管理数据库迁移,别再手动改表结构了! FastAPI服务半夜又挂了?先别急着重启,查查你的数据库连接池“池子”是不是漏了 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 Vue 3 组合式 API 香是香,但从Vue2迁移时你可别像我当初一样踩进这 3 个深坑里 我用fastapi-scaff搭了个项目,两天工期缩到两小时,老板以为我开挂了 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! FastAPI自动生成的API文档太丑?我花了一晚上把它改成了客户愿意付费的样子 告别手写 API 胶水代码:FastAPI 与 Vue 的“契约自动机” OpenAPI 实战 FastAPI + Vue 前后端分离实战:我的项目结构“避坑指南” FastAPI + Celery 实战:异步任务里调用 Redis 和数据库的全解析,及生产级组织方案 FastAPI里玩转Redis和数据库的正确姿势,别让异步任务把你坑哭了! FastAPI + Celery 实战:异步任务的坑与解法,我帮你踩了一遍 FastAPI子应用挂载:别再让root_path坑你一夜 FastAPI项目半夜报警吵醒你?聊聊告警这事儿怎么搞! 别再数据线了!用FastAPI 5分钟搭个局域网文件+剪贴板神器 FastAPI单元测试实战:别等上线被喷才后悔,TestClient用对了真香! FastAPI状态共享秘籍:别再让中间件、依赖和路由“各自为政”了! FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了! 从0到1,FastAPI + PostgreSQL + Tortoise ORM 实战避坑指南 FastAPI + PostgreSQL 实战:给应用装上“缓存”和“日志”翅膀 FastAPI + PostgreSQL 实战:从入门到不踩坑,一次讲透
FastAPI订单防超卖实战:从数据库锁到Saga分布式事务,这一篇给你理清了
一名程序媛呀 · 2026-04-22 · via 博客园 - 一名程序媛呀

先说案例:
凌晨三点,运营电话打爆了你的手机,说系统里的库存变成了 -888 。你眯着眼睛看日志,发现就剩最后一件库存的时候,同时进来了仨请求,全下单成功了。
恭喜你,喜提“反向仓库”成就,不仅没赚到钱,还得挨个打电话道歉求退款 🎯。

刚用FastAPI重构完老项目,觉得异步真香, async/await 写得飞起,唯独忘了数据库那块还有个叫“事务”的爹。

当作复盘吧,今天来聊聊怎么把FastAPI里的事务玩明白,从单库防超卖到跨服务的Saga分布式,一篇给你捋直了。

🔒 别再裸奔了:先搞懂事务这层保护膜

咱们写业务代码,最怕的就是“半拉子工程”。扣了钱库存没减,或者减了库存钱没扣着。这时候必须得把 ACID 刻在烟上吸进肺里,尤其是原子性。

简单打个比方:转账就像你左手倒右手,你不能接受左手已经空了,右手还没接到吧?事务就是那个 begin commit rollback 的保镖。

用大白话翻译一下:

- begin:搬个小板凳坐下,我要开始搞事情了,闲人勿扰。

- commit:好了,活儿干完了,完美收工,落子无悔。

- rollback:我去,写了个Bug,刚才那几步全当没发生,桌子擦干净重来。

在FastAPI里用SQLAlchemy异步操作,千万别再用老一套的 session.commit() 了,容易忘关。官方文档虽然那么写,但根据我以往的经验,一定要用 async with session.begin() ,这才是亲妈生的写法。

async def create_order(db: AsyncSession, order_data):
    # 只要缩进在这个代码块里,sqlalchemy自动帮你开了事务
    async with db.begin():
        new_order = Order(**order_data)
        db.add(new_order)
        # 这里可能还要调别的扣库存方法,放心,一荣俱荣一损俱损
        await flush_and_lock_stock(db, order_data.items)
    # 出了这个缩进还没抛异常?自动 commit!
    # 中间哪行代码崩了?自动 rollback!省心到想哭。
    return new_order

这里千万别学我当初偷懒,在循环里写 commit ,不仅慢得像蜗牛,一旦中间挂了,数据就是“脏”的,找都找不回来。

💊 嵌套事务与保存点:给自己留个后悔药

你可能会问:我就想让大事务里的小步骤失败了不影响大局,行不行?

这就得请出 savepoint 了。这就好比打游戏存档,你明知道前面Boss难打,先在门口存个档,万一死了,不用从第一关重来,直接读档再战。

啥时候用?比如下单成功了,你要发个积分。积分系统挂了(比如网络抖动),你总不能因为送积分失败就把整个订单回滚吧?那老板得把你头拧下来。
这时候在扣完库存后设个保存点,发积分挂了就滚回到保存点,订单照样生成,积分人工补发。

async with db.begin() as conn:
    # 扣库存(这步绝不能挂)
    await conn.execute(update(Stock).where(...).values(count=Stock.count-1))
    
    # 埋个复活点
    savepoint = await conn.begin_nested() 
    try:
        # 这步挂了无所谓
        await add_points(conn, user_id, 100) 
        await savepoint.commit()
    except Exception:
        await savepoint.rollback()
        logger.warning("积分加不上,先欠着吧")

🏗️ 分层架构下的事务协调:别把裤子穿反了

稍微正规点的项目都得分 Service 层和 Repository 层对吧?这时候事务放哪?

听句劝:事务边界一定要放在 Service 层(业务逻辑层)。Repository 层就老老实实做单表增删改查,别自作主张 commit 。

看到不少刚入行的小伙伴在 Repo 层每个方法最后都加个 db.commit() ,最后业务逻辑得拼好几个 Repo 方法,结果第一个 Repo 提交了,第二个崩了,神仙难救。

最好的做法是把 db session 对象像接力棒一样从 Service 传到 Repo,在 Service 的入口统一开启 async with db.begin():

🌍 分布式事务 Saga 模式:当微服务把数据库拆得稀碎

上面聊的都是单库操作。现实很骨感:订单库在 MySQL,用户余额在 Redis 或者别的什么独立服务里。扣钱成功但下单失败怎么办?这时候单机事务已经罩不住了。

这就是 Saga 模式的场子了。别被名字唬住,其实就是一个 “正向大管家 + 反向补偿” 的策略。

拿电商订单举个栗子:

1️⃣ 调用库存服务:预扣库存(成功)

2️⃣ 调用支付服务:扣余额(失败!余额不足)

3️⃣ 关键来了:Saga协调器发现支付失败,立马扭头去执行“反向补偿”——调用库存服务的“回滚接口”,把那件预扣的衣服加回去。

在FastAPI里实现Saga,可以用 状态机模式 或者简单点用后台任务队列记录每一步的执行与回滚。

最后啰嗦一句:补偿接口一定要保证 幂等性,别因为网络重试给人家库存加了两次,那就又成羊毛党了。

⚔️ 实战:库存扣减防超卖的终极锁方案

说完理论,来点立马能用的。单纯在代码里 if stock > 0 然后 update 是不行的,并发一来准挂。咱们得让数据库自己锁住那行数据。

# 悲观锁:SELECT ... FOR UPDATE
# 翻译:我要改这行了,你们都起开,等我改完你们再看。
async def deduct_stock_safe(db: AsyncSession, product_id: int, quantity: int):
    async with db.begin():
        # 用 for_update() 锁住查询结果
        stmt = select(Product).where(Product.id == product_id).with_for_update()
        result = await db.execute(stmt)
        product = result.scalar_one()
        
        if product.stock >= quantity:
            product.stock -= quantity
            # 不需要手动 add,被 session 追踪着呢
        else:
            raise ValueError("库存不足,别抢了!")

是不是以为这样就完了?再说个容易翻车的点:
MySQL默认隔离级别下,就算加了行锁,如果查询条件没走索引,它会锁表!直接把整个表给锁了,那你的并发量瞬间归零。所以, product_id 必须是索引或者主键。


好啦,从单机事务的坑到分布式Saga的应对思路,今天算是把压箱底的经验都掏出来了。技术这条路就是这样,很多知识点看文档觉得会了,一上线就翻车。希望今天聊的这些,能让你下次写库存扣减、写支付回调的时候,心里更有底,少加点班 🛠️。

如果觉得有用,别光收藏吃灰,点个赞加关注让我知道,这样我才有动力继续把踩过的坑都抖落出来。👋