Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:25 +08:00
commit c3294f28aa
60 changed files with 10297 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
# Complete Examples
## Example 1: Banking Transfer
**Input:** Transfer money between accounts with strict consistency.
**Implementation:**
```typescript
import { Prisma } from '@prisma/client';
async function transferMoney(
fromAccountId: string,
toAccountId: string,
amount: number
) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
try {
const result = await prisma.$transaction(
async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: fromAccountId }
});
if (!fromAccount) {
throw new Error('Source account not found');
}
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
const toAccount = await tx.account.findUnique({
where: { id: toAccountId }
});
if (!toAccount) {
throw new Error('Destination account not found');
}
await tx.account.update({
where: { id: fromAccountId },
data: { balance: { decrement: amount } }
});
await tx.account.update({
where: { id: toAccountId },
data: { balance: { increment: amount } }
});
const transfer = await tx.transfer.create({
data: {
fromAccountId,
toAccountId,
amount,
status: 'COMPLETED',
completedAt: new Date()
}
});
return transfer;
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 10000
}
);
return { success: true, transfer: result };
} catch (error) {
if (error.code === 'P2034') {
throw new Error('Transaction conflict - please retry');
}
throw error;
}
}
```
## Example 2: Inventory Reservation
**Input:** Reserve inventory items for an order.
**Implementation:**
```typescript
async function reserveInventory(
orderId: string,
items: Array<{ productId: string; quantity: number }>
) {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await prisma.$transaction(
async (tx) => {
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId }
});
if (!product) {
throw new Error(`Product ${item.productId} not found`);
}
if (product.stock < item.quantity) {
throw new Error(
`Insufficient stock for ${product.name}`
);
}
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
await tx.reservation.create({
data: {
orderId,
productId: item.productId,
quantity: item.quantity,
reservedAt: new Date()
}
});
}
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 3000,
timeout: 8000
}
);
return { success: true };
} catch (error) {
if (error.code === 'P2034' && attempt < maxRetries - 1) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 200)
);
continue;
}
throw error;
}
}
throw new Error('Reservation failed after retries');
}
```
## Example 3: Seat Booking with Status Check
**Input:** Book a seat with concurrent user protection.
**Implementation:**
```typescript
async function bookSeat(
userId: string,
eventId: string,
seatNumber: string
) {
try {
const booking = await prisma.$transaction(
async (tx) => {
const seat = await tx.seat.findFirst({
where: {
eventId,
seatNumber
}
});
if (!seat) {
throw new Error('Seat not found');
}
if (seat.status !== 'AVAILABLE') {
throw new Error('Seat is no longer available');
}
await tx.seat.update({
where: { id: seat.id },
data: {
status: 'BOOKED',
bookedAt: new Date()
}
});
const booking = await tx.booking.create({
data: {
userId,
seatId: seat.id,
eventId,
status: 'CONFIRMED',
bookedAt: new Date()
}
});
return booking;
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
return { success: true, booking };
} catch (error) {
if (error.code === 'P2034') {
throw new Error(
'Seat was just booked by another user - please select another seat'
);
}
throw error;
}
}
```

View File

@@ -0,0 +1,63 @@
# Database-Specific Defaults
## PostgreSQL
Default: `ReadCommitted`
Supported levels:
- `Serializable` (strictest)
- `RepeatableRead`
- `ReadCommitted` (default)
Notes:
- PostgreSQL uses true Serializable isolation (not snapshot isolation)
- May throw serialization errors under high concurrency
- Excellent MVCC implementation reduces conflicts
## MySQL
Default: `RepeatableRead`
Supported levels:
- `Serializable`
- `RepeatableRead` (default)
- `ReadCommitted`
- `ReadUncommitted` (not recommended)
Notes:
- InnoDB engine required for transaction support
- Uses gap locking in RepeatableRead mode
- Serializable adds locking to SELECT statements
## SQLite
Default: `Serializable`
Supported levels:
- `Serializable` (only level - database-wide lock)
Notes:
- Only one writer at a time
- No true isolation level configuration
- Best for single-user or low-concurrency applications
## MongoDB
Default: `Snapshot` (similar to RepeatableRead)
Supported levels:
- `Snapshot` (equivalent to RepeatableRead)
- `Majority` read concern
Notes:
- Different isolation model than SQL databases
- Uses write-ahead log for consistency
- Replica set required for transactions

View File

@@ -0,0 +1,124 @@
# 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.