⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

4.7.2 同时修改了怎么办——冲突检测:并发修改的识别机制

一句话破题

冲突检测的核心是"知道数据被改过"——通过版本号或时间戳,在保存时判断数据是否已被他人修改。

乐观锁 vs 悲观锁

类型策略适用场景
乐观锁先修改,保存时检测冲突冲突概率低
悲观锁先锁定,确保独占修改冲突概率高

乐观锁:版本号方案

prisma
model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  version   Int      @default(1)
  updatedAt DateTime @updatedAt
}
typescript
// 更新时检查版本
async function updatePost(id: string, data: PostData, version: number) {
  const result = await prisma.post.updateMany({
    where: {
      id,
      version  // 只有版本匹配才更新
    },
    data: {
      ...data,
      version: { increment: 1 }
    }
  })
  
  if (result.count === 0) {
    throw new Error('CONFLICT: 数据已被他人修改')
  }
  
  return prisma.post.findUnique({ where: { id } })
}

API 层实现

typescript
// app/api/posts/[id]/route.ts
export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const body = await request.json()
  const { title, content, version } = body
  
  try {
    const post = await updatePost(params.id, { title, content }, version)
    return Response.json(post)
  } catch (error) {
    if (error.message.includes('CONFLICT')) {
      return Response.json(
        { error: '数据已被他人修改,请刷新后重试' },
        { status: 409 }
      )
    }
    throw error
  }
}

前端处理冲突

typescript
async function savePost(post: Post) {
  const response = await fetch(`/api/posts/${post.id}`, {
    method: 'PUT',
    body: JSON.stringify({
      title: post.title,
      content: post.content,
      version: post.version  // 携带当前版本号
    })
  })
  
  if (response.status === 409) {
    // 冲突处理
    const confirmRefresh = confirm('数据已被他人修改,是否刷新获取最新数据?')
    if (confirmRefresh) {
      await refreshPost(post.id)
    }
    return
  }
  
  return response.json()
}

使用 updatedAt 作为版本

typescript
async function updatePost(id: string, data: PostData, lastUpdatedAt: Date) {
  const result = await prisma.post.updateMany({
    where: {
      id,
      updatedAt: lastUpdatedAt  // 时间戳作为版本
    },
    data
  })
  
  if (result.count === 0) {
    throw new Error('CONFLICT')
  }
  
  return prisma.post.findUnique({ where: { id } })
}

悲观锁:SELECT FOR UPDATE

typescript
async function transferBalance(fromId: string, toId: string, amount: number) {
  return prisma.$transaction(async (tx) => {
    // 锁定记录
    const [from, to] = await Promise.all([
      tx.$queryRaw`SELECT * FROM "Account" WHERE id = ${fromId} FOR UPDATE`,
      tx.$queryRaw`SELECT * FROM "Account" WHERE id = ${toId} FOR UPDATE`
    ])
    
    if (from.balance < amount) {
      throw new Error('余额不足')
    }
    
    // 执行转账
    await tx.account.update({
      where: { id: fromId },
      data: { balance: { decrement: amount } }
    })
    
    await tx.account.update({
      where: { id: toId },
      data: { balance: { increment: amount } }
    })
  })
}

使用 ETag 头

typescript
// GET 响应中返回 ETag
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await prisma.post.findUnique({
    where: { id: params.id }
  })
  
  const etag = `"${post.version}"`
  
  return Response.json(post, {
    headers: { 'ETag': etag }
  })
}

// PUT 请求中使用 If-Match
export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const ifMatch = request.headers.get('If-Match')
  const version = ifMatch ? parseInt(ifMatch.replace(/"/g, '')) : null
  
  if (!version) {
    return Response.json(
      { error: '缺少 If-Match 头' },
      { status: 428 }
    )
  }
  
  // ... 使用 version 进行乐观锁更新
}

检测策略选择

场景推荐策略
普通表单编辑乐观锁 + 版本号
协同编辑文档乐观锁 + OT/CRDT
金融交易悲观锁
库存扣减悲观锁或乐观锁重试

本节小结

  • 乐观锁适合冲突概率低的场景
  • 悲观锁适合冲突概率高的关键业务
  • 版本号或 updatedAt 是最常用的乐观锁标识
  • HTTP 409 状态码表示冲突