数据库事务与一致性:Prisma 场景下的实战注意事项
本文目录
事务解决的是同一个数据库事务边界内多条读写要么一起提交、要么一起回滚。它不会自动涵盖 Redis、邮件、支付网关、消息队列或浏览器状态。设计时先画清楚哪些副作用必须原子、哪些可最终一致,以及失败后如何重试或补偿。
Prisma 的事务形态
- 相关联的创建可使用 nested writes,让父子记录在同一事务完成。
- 相互独立的查询/写入可使用批量或事务数组;操作不应依赖前一步运行时结果。
- 需要先读、再按读值决定写入时,使用 interactive transaction,并在回调里只通过 tx 客户端访问数据库。
按项目锁定的 Prisma 版本查对应文档。事务 API、隔离级别支持和错误码可能随版本与数据库 provider 不同;不要把 Prisma ORM 7 的文档示例直接照抄到另一版本而跳过类型与数据库验证。
读改写:优先让约束和条件落在数据库
像“余额足够才扣款”这样的规则,应尽量用条件更新表达,而不是先在应用里读出余额、等待一段时间后再无条件写回。应用层的读值可能已经过期;单进程锁挡不住其他进程或其他服务。
async function reserveInventory(prisma, productId, quantity) {
if (!Number.isSafeInteger(quantity) || quantity <= 0) {
throw new Error('invalid quantity');
}
return prisma.$transaction(async (tx) => {
const changed = await tx.product.updateMany({
where: { id: productId, stock: { gte: quantity }, isActive: true },
data: { stock: { decrement: quantity } },
});
if (changed.count !== 1) throw new Error('unavailable');
return tx.reservation.create({
data: { productId, quantity, status: 'RESERVED' },
});
}, { maxWait: 5000, timeout: 10000 });
}
示例仍需配合数据库约束:库存不能为负、Reservation 的外键和数量约束要在 schema/SQL 中明确。若事务回滚,调用方得到失败;若事务已提交但客户端断线没收到响应,客户端重试仍可能重复预留,因此还需要请求幂等键和唯一约束。
幂等请求与版本冲突
把 client-provided idempotency key 与用户/租户和操作类型绑定,设置唯一约束;相同键重复到达时返回第一次已持久化的结果,而不是再执行一次。不要只对全局 key 去重,也不要假设 upsert 天生幂等:是否幂等取决于唯一约束和更新行为。
低冲突的编辑可以使用乐观并发控制:更新条件包含读到的 version,成功后 version 加一;影响行数为零时提示冲突并让用户刷新。多行不变量可用事务隔离、唯一/检查约束或显式锁,具体方案须根据 PostgreSQL/MySQL 等 provider 的并发语义测试。
隔离级别和事务长度
降低隔离级别可提高并发,但可能出现不可重复读、幻读或写偏差,取决于数据库实现。提高到 Serializable 会增强某些并发保证,却可能让一方以序列化冲突失败;应用必须以幂等方式有限重试整笔事务。重试只覆盖可重试的冲突错误,不能把所有异常都无限循环。
保持事务短:不要在事务里调用邮件、支付、慢速 HTTP、用户交互或等待队列。长事务占连接、持有锁,并可能拖慢 vacuum/版本回收。超过数据库连接池容量的事务会把排队延迟扩散到整个服务。
数据库与外部系统之间的可靠流程
若写业务数据后还要发事件,使用 transactional outbox:在业务事务内一并写 outbox,再由 worker 重试发布;消费者以事件 ID 幂等处理。邮件或支付调用尽量在事务外,使用下游幂等键,记录可恢复状态和对账信息。Saga 补偿不是回滚,它是执行新的业务动作来修正已经完成的步骤,必须明确部分失败和人工介入路径。
错误处理与可观测性
把业务拒绝、唯一键冲突、死锁/序列化冲突、连接失败、超时和 schema 不匹配分开处理。对用户返回稳定、不过度泄露内部细节的错误码;日志保存查询操作、事务结果、重试次数、关联 ID 和安全错误分类,不记录数据库 URL、参数中的秘密或完整个人数据。
测试要覆盖并发的真实场景:两请求同时预留最后一件库存;同一幂等键同时提交;写数据库成功但 API 响应断开;outbox 发布成功后标记前进程退出。单元测试 mock 不会验证隔离级别、唯一索引、锁和真实 provider 的事务行为,关键不变量要在目标数据库集成测试中覆盖。
变更前检查
- 明确不变量并尽可能编码成唯一、外键、检查约束或条件更新。
- 检查 Prisma schema 和真实数据库结构是否一致,生成并审查迁移 SQL。
- 在目标 provider、目标版本和并发压力下验证事务行为与冲突码。
- 为超时、断连、重复请求和进程中断准备恢复流程。
- 生产迁移前备份、估算锁/写入影响,并准备可验证的回滚方案。
参考:Prisma ORM transaction patterns 与 Prisma idempotent API design。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。