黑白梦黑白梦

  • 文章
  • 专栏
  • 文章
  • 专栏
全部文章

Prisma 实现乐观锁与悲观锁

发布于 2024-09-09更新于 2026-09-29约 6 分钟

在数据库并发事务控制中,为防止多个事务并发读写导致的数据覆盖或脏写问题,主要采用乐观并发控制与悲观并发控制两种策略。本文介绍两者的核心概念、常用场景,并给出在 Prisma ORM 中的具体实现方案。

乐观锁

  • 核心概念:乐观锁(Optimistic Concurrency Control, OCC)假设多事务并发修改同一资源的冲突概率较低。在读取和业务计算阶段不对数据库物理行加锁,仅在最终提交更新时比对版本标识(如 version 整数版本号或更新时间戳)。若版本一致则提交修改并将版本递增;若版本不一致,说明记录在此期间已被其他事务修改,当前更新操作返回受影响行数为 0,由应用层决定重试或报错。
  • 常用场景:
    • 读多写少、并发修改冲突发生概率较低的业务系统(如内容管理系统文章编辑、用户资料维护)。
    • 交互流程周期较长或跨 HTTP 请求的业务操作(如用户打开表单后耗时数分钟才提交保存,若使用悲观锁长时间占用数据库连接与行锁会导致连接池耗尽)。
    • 对系统并发吞吐要求高、允许并发冲突时快速失败并由前端提示用户刷新的场景。

实现方案

Prisma 在数据模型中添加版本标识字段并在更新条件中限定版本号来实现乐观并发控制。参考 Prisma 乐观锁官方场景文档。

以电影院选座避免双重预订为例,在数据模型中添加 version 字段:

prisma
model Seat {  
  id        Int   @id @default(autoincrement())  
  userId    Int?  
  claimedBy User? @relation(fields: [userId], references: [id])  
  movieId   Int  
  movie     Movie @relation(fields: [movieId], references: [id])  
  version   Int
}

model Movie {  
  id    Int    @id     @default(autoincrement())  
  name  String @unique  
  seats Seat[]  
}

执行带版本校验的更新操作:

JavaScript
const movieName = 'Hidden Figures'  
  
// 查找第一个可用座位
const availableSeat = await prisma.seat.findFirst({  
  where: {  
    movie: {  
      name: movieName,  
    },  
    claimedBy: null,  
  },  
})  
  
// 如果没有座位,则抛出错误
if (!availableSeat) {  
  throw new Error(`Oh no! ${movieName} is all booked.`)  
}  
  
// 通过匹配当前 version 执行更新,若该行已被其他事务修改则返回 count 为 0
const seats = await prisma.seat.updateMany({
  data: {
    claimedBy: userId,
    version: {
      increment: 1,
    },
  },
  where: {
    id: availableSeat.id,
    version: availableSeat.version,
  },
})

if (seats.count === 0) {  
  throw new Error('座位已被预订,请重试')  
}
  • 说明:updateMany 方法在 where 条件中同时匹配主键 id 与读取时的 version。若读取与更新之间发生并发写入,数据库中记录的 version 已发生变化,更新条件不匹配导致返回的 count 为 0,从而避免并发覆盖。

悲观锁

  • 核心概念:悲观锁(Pessimistic Concurrency Control, PCC)假设并发冲突发生概率较高。在事务读取数据阶段即通过数据库底层排他锁(如 SELECT ... FOR UPDATE)或共享锁(如 SELECT ... FOR SHARE)锁定目标数据行。在锁持有期间,其他事务针对同一数据行的加锁读取或更新写入会被阻塞挂起,直至当前事务提交(COMMIT)或回滚(ROLLBACK)释放锁资源。
  • 常用场景:
    • 并发竞争激烈、冲突发生概率极高的强一致性业务(如电商秒杀活动、库存扣减、账户转账、余额提现)。
    • 依赖严格串行化处理且并发冲突后重试代价较大的业务场景。
    • 单次事务执行周期短、数据库锁持有时间短且可控的场景。

实现方案

Prisma Client API 当前未直接提供内置的 FOR UPDATE 方法,可通过交互式事务(Interactive Transactions)结合 $queryRaw 执行原生 SQL 加锁查询实现。参考 Prisma 原生 SQL 查询文档。

执行流程如下:

  1. 开启交互式事务:调用 prisma.$transaction(async (tx) => { ... }) 获得独立事务实例。
  2. 行级加锁读取:在事务内执行 SELECT * FROM "User" WHERE id = 1 FOR UPDATE,锁定目标行并阻塞其他事务的并发加锁操作。
  3. 数据更新与提交:在锁保护下执行更新操作,事务完成时数据库自动释放行锁。

示例代码:

JavaScript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function main() {
  // 开始交互式事务
  await prisma.$transaction(async (tx) => {
    // 使用 FOR UPDATE 锁定记录
    const user = await tx.$queryRaw`SELECT * FROM "User" WHERE id = 1 FOR UPDATE`;

    // 在锁保护下执行更新操作
    await tx.user.update({
      where: { id: 1 },
      data: { status: 'INACTIVE' },
    });
  });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

锁机制对比

维度 乐观锁 悲观锁
加锁时机 不加数据库物理锁,提交时通过版本号比对 读取数据时立即通过数据库加行级/表级锁
性能与吞吐 无锁等待开销,读密集型场景吞吐量高 存在锁等待与阻塞,写冲突激烈时排队开销大
冲突处理 冲突发生时快速失败,由应用层决定重试或中断 冲突发生时排队等待锁释放,减少因冲突导致的失败
死锁风险 无数据库行级锁持有,无死锁风险 多事务交叉加锁时可能引发死锁,需按序加锁或配置超时
适用场景 读多写少、交互周期长、允许重试的场景 写多读少、竞争激烈、要求强一致性的场景
目录
乐观锁实现方案悲观锁实现方案锁机制对比

本文收录于专栏

一些好用的 npm 前端开源库

收集一些好用的前端开源库,主要是 npm 包

0 篇文章更新于 2026-08-17
上一篇在编辑器搭建简易好用的算法刷题编码环境下一篇使用 iSH 在 iOS 搭建终端

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com