Node.js中使用Prisma如何实现乐观锁与悲观锁?

来源:IPIPP.com作者:唐僧头衔:草根站长
导读:本期聚焦于唐僧创作的《Node.js中使用Prisma如何实现乐观锁与悲观锁?》,敬请观看详情。并发更新同一行数据时,常出现后写覆盖先写的丢更新问题,解决思路主要靠乐观锁与悲观锁两种策略。本文围绕Prisma在Node.js环境下的具体落地展开,先讲清version字段配合updateMany的乐观锁实现,包括冲突检测与重试逻辑的编写;再分析利用数据库事务与SELECT FOR UPDATE实现悲观锁的做法,并对比两种方案在性能、死锁风险与适用场景上的差异,最后给出选型建议,帮助在高并发业务中写出数据一致性更可靠的代码。

当两个请求同时读到同一条数据、各自修改后再写回时,后提交的那次写入会悄悄覆盖前一次的结果,这就是典型的丢失更新问题。电商扣库存、账户余额变动、订单状态流转这类场景几乎都会碰到。解决并发写冲突的主流方案就是乐观锁与悲观锁,下面结合Prisma这个流行的Node.js ORM,看看它们分别怎么落地。

Node.js中使用Prisma如何实现乐观锁与悲观锁?

为什么Prisma没有直接提供锁API

Prisma在设计上对底层数据库做了抽象,而不同数据库对锁的支持差异很大:MySQL的SELECT ... FOR UPDATE、PostgreSQL的FOR UPDATE NOWAIT、SQL Server的锁提示语法各不相同,因此Prisma没有提供统一的一等锁接口。但这并不代表做不到:乐观锁完全可以用Prisma自身的查询能力实现,悲观锁则可以借助$queryRaw下发原生SQL来完成。

另外,Prisma的交互式事务$transaction支持传入隔离级别,这在悲观锁方案中很关键。MySQL的InnoDB在REPEATABLE READ下配合行锁可以保证事务内的读取一致性;PostgreSQL的READ COMMITTED虽然默认不能防丢更新,但加上FOR UPDATE后就能锁定目标行。理解这些数据库层的实际行为,才能正确地用Prisma实现锁,否则容易写出看似加锁、实际毫无保护的代码。

用version字段实现乐观锁

乐观锁的思路是提交时才校验冲突,最常见的实现是版本号字段。先在schema中给需要并发控制的表加一个version字段:

model Product {
  id      Int    @id @default(autoincrement())
  stock   Int
  version Int    @default(1)

  @@map("products")
}

关键点在于更新时必须用updateMany而不是updateupdateMany允许把version作为where条件,如果版本号已被别人改过,受影响行数会返回0,从而检测到冲突;而update的where只接受唯一键,无法附加version条件,这一点是很多初学者踩过的坑。

async function deductStock(productId, quantity) {
  for (let i = 0; i < 3; i++) {
    // 读取当前记录
    const product = await prisma.product.findUnique({
      where: { id: productId },
    });
    if (!product || product.stock < quantity) {
      throw new Error('库存不足');
    }

    // 带version条件的条件更新
    const result = await prisma.product.updateMany({
      where: { id: productId, version: product.version },
      data: {
        stock: product.stock - quantity,
        version: { increment: 1 },
      },
    });

    if (result.count === 1) {
      return true; // 更新成功
    }
    // count为0说明版本号已被其他请求改过,进入重试
  }
  throw new Error('并发冲突,重试次数已用尽');
}

上面代码把计算后的stock直接写回,依赖version校验兜底。也可以写得 stricter一些,例如where: { id: productId, stock: { gte: quantity }, version: product.version },把库存校验也放进更新条件,进一步收紧安全性。重试次数要根据业务QPS合理设置,冲突频繁时重试本身会放大数据库压力,这时就该考虑悲观锁了。

另一种不需要version字段的做法是纯条件更新:扣库存时只写where: { id: productId, stock: { gte: quantity } },用updateMany返回的行数判断成败。这本质上是把业务约束下推到SQL的WHERE子句,思路与乐观锁一脉相承,实现更简单,适合单字段约束场景;version方案则适合多个字段需要整体一致性快照的复杂更新。

借助事务与原生SQL实现悲观锁

悲观锁假设冲突一定会发生,读取时就锁住记录,其他事务只能排队等待。在Prisma中需要把原生SQL放进交互式事务里执行:

const [updated] = await prisma.$transaction(
  async (tx) => {
    // 锁定目标行,直到事务结束才释放
    const rows = await tx.$queryRaw`
      SELECT id, stock FROM products WHERE id = ${productId} FOR UPDATE
    `;
    const product = rows[0];
    if (!product || product.stock < quantity) {
      throw new Error('库存不足');
    }

    // 在锁保护下安全更新
    return tx.product.update({
      where: { id: productId },
      data: { stock: product.stock - quantity },
    });
  },
  {
    isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
    timeout: 10000,
  }
);

FOR UPDATE会对匹配的行加排他锁,事务提交或回滚后才释放,期间其他事务执行同样的锁定查询会被阻塞,从而实现串行化访问。Prisma模板字符串里的参数是预编译的,能有效防止SQL注入。还有一个容易忽略的细节:$queryRaw返回的字段名是数据库列名而非模型字段名,PostgreSQL中还会被转为小写,取值时别写错属性。

锁的粒度是这里的核心问题。WHERE条件如果没走索引,MySQL可能把扫描过的行都锁住,甚至触发间隙锁,导致并发能力急剧下降,所以务必确保锁查询的条件列上有索引。同时要固定加锁顺序,比如多个事务都按id升序依次锁定,否则两个事务互相持有对方需要的锁就会死锁。给事务设置合理的timeout也很重要,超时自动回滚可以避免某个慢事务把连接池拖垮。

两种锁如何选择

两者的差异可以从几个维度对比:

维度乐观锁悲观锁
读多写少场景非常合适,冲突少重试少浪费锁资源,吞吐受限
写冲突频繁重试风暴,成功率低排队串行,更稳定
事务持有时间无长事务锁持有到事务结束
死锁风险存在,需控制加锁顺序
实现复杂度低,纯Prisma API中,需要原生SQL

实践中还可以混合使用:低冲突路径走乐观锁快速提交,检测到连续冲突后自动降级为悲观锁串行处理。而对于秒杀这类极端场景,更彻底的做法是把扣减逻辑下推为一条原子UPDATE语句,直接在数据库端完成判断和扣减,绕开应用层的读改写流程,性能和正确性都会更好。

无论选哪种方案,都建议配套做好监控:乐观锁要统计重试率和最终失败率,悲观锁要关注锁等待时间和死锁日志。这些指标能直接反映当前策略是否匹配业务的并发特征,也是后续扩容和调优时最有价值的依据。

Prisma乐观锁悲观锁修改时间:2026-09-07 04:08:44

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/51964.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。