Prisma 实现乐观锁与悲观锁

乐观锁是一种思想,不实际加锁,通过版本号控制一致性;悲观锁是在数据库中加行锁或表锁。Prisma 的文档中有实现乐观锁的示例,也可以通过执行原生 SQL 语句实现悲观锁。

乐观锁

Prisma 实现乐观锁,可在表中添加一个 version 字段,并在更新操作中使用该字段来确保数据的一致性。

https://www.prisma.io/docs/orm/prisma-client/queries/transactions#scenario-reserving-a-seat-at-the-cinema

比如在官网的文档中,给了一个具体的示例,展示了如何在电影订票系统中使用乐观锁,来避免双重预订问题。

在模型中添加一个 version 字段:

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[]  
}

代码示例:

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.`)  
}  
  
// 通过乐观锁定来获得座位,所有其他尝试预定同一个座位会有一个过时的版本
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 方法会检查 version 字段是否匹配。如果在此期间有其他事务修改了该记录,version 字段将不匹配,更新操作将失败,从而避免数据不一致的问题。

通过这种方式,Prisma 可以有效地实现乐观锁,确保数据的一致性和安全性。

悲观锁

Prisma 目前不直接支持悲观锁(例如 SELECT FOR UPDATE 或 SELECT FOR SHARE),可使用 $executeRaw 方法来执行原生 SQL 查询,从而实现悲观锁。

https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries

操作流程:

  1. 开始事务:使用 prisma.$transaction 方法开始一个事务。
  2. 锁定记录:使用 SELECT * FROM "User" WHERE id = 1 FOR UPDATE 锁定指定的记录。这将防止其他事务在当前事务完成之前修改该记录。
  3. 更新记录:在锁定的记录上执行更新操作。
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();
  });