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 状态码表示冲突
