125 lines
2.7 KiB
Markdown
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.
|