Files
gh-djankies-claude-configs-…/skills/configuring-transaction-isolation/references/race-conditions.md
2025-11-29 18:22:25 +08:00

125 lines
2.7 KiB
Markdown

# Preventing Race Conditions
## Lost Update Problem
**Scenario:** Two transactions read the same value, both update it, one overwrites the other.
**Without Isolation:**
```typescript
const product = await prisma.product.findUnique({
where: { id: productId }
});
await prisma.product.update({
where: { id: productId },
data: { stock: product.stock - quantity }
});
```
Transaction A reads stock: 10
Transaction B reads stock: 10
Transaction A writes stock: 5 (10 - 5)
Transaction B writes stock: 8 (10 - 2)
Result: Stock is 8, but should be 3
**With Serializable Isolation:**
```typescript
await prisma.$transaction(
async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId }
});
if (product.stock < quantity) {
throw new Error('Insufficient stock');
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } }
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
```
One transaction succeeds, the other gets P2034 and retries with fresh data.
## Double-Booking Problem
**Scenario:** Two users try to book the same resource simultaneously.
**Solution:**
```typescript
async function bookSeat(userId: string, seatId: string) {
try {
await prisma.$transaction(
async (tx) => {
const seat = await tx.seat.findUnique({
where: { id: seatId }
});
if (seat.status !== 'AVAILABLE') {
throw new Error('Seat no longer available');
}
await tx.seat.update({
where: { id: seatId },
data: {
status: 'BOOKED',
userId,
bookedAt: new Date()
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
return { success: true };
} catch (error) {
if (error.code === 'P2034') {
throw new Error('Seat was just booked by another user');
}
throw error;
}
}
```
## Phantom Read Problem
**Scenario:** Query for rows matching a condition, insert happens, re-query shows different results.
**Example with RepeatableRead:**
```typescript
await prisma.$transaction(
async (tx) => {
const activeUsers = await tx.user.findMany({
where: { status: 'ACTIVE' }
});
const count = activeUsers.length;
await tx.report.create({
data: {
type: 'USER_COUNT',
value: count,
timestamp: new Date()
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead
}
);
```
RepeatableRead prevents other transactions from changing existing rows, but may still allow new inserts (phantom reads) depending on database implementation.