feat(transaction): 添加游戏事务系统 | add game transaction system (#381)

- TransactionManager/TransactionContext 事务管理
- MemoryStorage/RedisStorage/MongoStorage 存储实现
- CurrencyOperation/InventoryOperation/TradeOperation 内置操作
- SagaOrchestrator 分布式 Saga 编排
- withTransactions() Room 集成
- 完整中英文文档
This commit is contained in:
YHH
2025-12-29 10:54:00 +08:00
committed by GitHub
parent 2d46ccf896
commit d4cef828e1
38 changed files with 6631 additions and 16 deletions

View File

@@ -34,6 +34,7 @@ ESEngine provides a rich set of modules that can be imported as needed.
| Module | Package | Description |
|--------|---------|-------------|
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
| [Transaction](/en/modules/transaction/) | `@esengine/transaction` | Game transactions with distributed support |
## Installation

View File

@@ -0,0 +1,261 @@
---
title: "Core Concepts"
description: "Transaction system core concepts: context, manager, and Saga pattern"
---
## Transaction State
A transaction can be in the following states:
```typescript
type TransactionState =
| 'pending' // Waiting to execute
| 'executing' // Executing
| 'committed' // Committed
| 'rolledback' // Rolled back
| 'failed' // Failed
```
## TransactionContext
The transaction context encapsulates transaction state, operations, and execution logic.
### Creating Transactions
```typescript
import { TransactionManager } from '@esengine/transaction';
const manager = new TransactionManager();
// Method 1: Manual management with begin()
const tx = manager.begin({ timeout: 5000 });
tx.addOperation(op1);
tx.addOperation(op2);
const result = await tx.execute();
// Method 2: Automatic management with run()
const result = await manager.run((tx) => {
tx.addOperation(op1);
tx.addOperation(op2);
});
```
### Chaining Operations
```typescript
const result = await manager.run((tx) => {
tx.addOperation(new CurrencyOperation({ ... }))
.addOperation(new InventoryOperation({ ... }))
.addOperation(new InventoryOperation({ ... }));
});
```
### Context Data
Operations can share data through the context:
```typescript
class CustomOperation extends BaseOperation<MyData, MyResult> {
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// Read data set by previous operations
const previousResult = ctx.get<number>('previousValue');
// Set data for subsequent operations
ctx.set('myResult', { value: 123 });
return this.success({ ... });
}
}
```
## TransactionManager
The transaction manager is responsible for creating, executing, and recovering transactions.
### Configuration Options
```typescript
interface TransactionManagerConfig {
storage?: ITransactionStorage; // Storage instance
defaultTimeout?: number; // Default timeout (ms)
serverId?: string; // Server ID (for distributed)
autoRecover?: boolean; // Auto-recover pending transactions
}
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
defaultTimeout: 10000,
serverId: 'server-1',
autoRecover: true,
});
```
### Distributed Locking
```typescript
// Acquire lock
const token = await manager.acquireLock('player:123:inventory', 10000);
if (token) {
try {
// Perform operations
await doSomething();
} finally {
// Release lock
await manager.releaseLock('player:123:inventory', token);
}
}
// Or use withLock for convenience
await manager.withLock('player:123:inventory', async () => {
await doSomething();
}, 10000);
```
### Transaction Recovery
Recover pending transactions after server restart:
```typescript
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
serverId: 'server-1',
});
// Recover pending transactions
const recoveredCount = await manager.recover();
console.log(`Recovered ${recoveredCount} transactions`);
```
## Saga Pattern
The transaction system uses the Saga pattern. Each operation must implement `execute` and `compensate` methods:
```typescript
interface ITransactionOperation<TData, TResult> {
readonly name: string;
readonly data: TData;
// Validate preconditions
validate(ctx: ITransactionContext): Promise<boolean>;
// Forward execution
execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>;
// Compensate (rollback)
compensate(ctx: ITransactionContext): Promise<void>;
}
```
### Execution Flow
```
Begin Transaction
┌─────────────────────┐
│ validate(op1) │──fail──► Return failure
└─────────────────────┘
│success
┌─────────────────────┐
│ execute(op1) │──fail──┐
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ validate(op2) │──fail──┤
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ execute(op2) │──fail──┤
└─────────────────────┘ │
│success ▼
▼ ┌─────────────────────┐
Commit Transaction │ compensate(op1) │
└─────────────────────┘
Return failure (rolled back)
```
### Custom Operations
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
interface UpgradeData {
playerId: string;
itemId: string;
targetLevel: number;
}
interface UpgradeResult {
newLevel: number;
}
class UpgradeOperation extends BaseOperation<UpgradeData, UpgradeResult> {
readonly name = 'upgrade';
private _previousLevel: number = 0;
async validate(ctx: ITransactionContext): Promise<boolean> {
// Validate item exists and can be upgraded
const item = await this.getItem(ctx);
return item !== null && item.level < this.data.targetLevel;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<UpgradeResult>> {
const item = await this.getItem(ctx);
if (!item) {
return this.failure('Item not found', 'ITEM_NOT_FOUND');
}
this._previousLevel = item.level;
item.level = this.data.targetLevel;
await this.saveItem(ctx, item);
return this.success({ newLevel: item.level });
}
async compensate(ctx: ITransactionContext): Promise<void> {
const item = await this.getItem(ctx);
if (item) {
item.level = this._previousLevel;
await this.saveItem(ctx, item);
}
}
private async getItem(ctx: ITransactionContext) {
// Get item from storage
}
private async saveItem(ctx: ITransactionContext, item: any) {
// Save item to storage
}
}
```
## Transaction Result
```typescript
interface TransactionResult<T = unknown> {
success: boolean; // Whether succeeded
transactionId: string; // Transaction ID
results: OperationResult[]; // Operation results
data?: T; // Final data
error?: string; // Error message
duration: number; // Execution time (ms)
}
const result = await manager.run((tx) => { ... });
console.log(`Transaction ${result.transactionId}`);
console.log(`Success: ${result.success}`);
console.log(`Duration: ${result.duration}ms`);
if (!result.success) {
console.log(`Error: ${result.error}`);
}
```

View File

@@ -0,0 +1,355 @@
---
title: "Distributed Transactions"
description: "Saga orchestrator and cross-server transaction support"
---
## Saga Orchestrator
`SagaOrchestrator` is used to orchestrate distributed transactions across servers.
### Basic Usage
```typescript
import { SagaOrchestrator, RedisStorage } from '@esengine/transaction';
const orchestrator = new SagaOrchestrator({
storage: new RedisStorage({ client: redis }),
timeout: 30000,
serverId: 'orchestrator-1',
});
const result = await orchestrator.execute([
{
name: 'deduct_currency',
serverId: 'game-server-1',
data: { playerId: 'player1', amount: 100 },
execute: async (data) => {
// Call game server API to deduct currency
const response = await gameServerApi.deductCurrency(data);
return { success: response.ok };
},
compensate: async (data) => {
// Call game server API to restore currency
await gameServerApi.addCurrency(data);
},
},
{
name: 'add_item',
serverId: 'inventory-server-1',
data: { playerId: 'player1', itemId: 'sword' },
execute: async (data) => {
const response = await inventoryServerApi.addItem(data);
return { success: response.ok };
},
compensate: async (data) => {
await inventoryServerApi.removeItem(data);
},
},
]);
if (result.success) {
console.log('Saga completed successfully');
} else {
console.log('Saga failed:', result.error);
console.log('Completed steps:', result.completedSteps);
console.log('Failed at:', result.failedStep);
}
```
### Configuration Options
```typescript
interface SagaOrchestratorConfig {
storage?: ITransactionStorage; // Storage instance
timeout?: number; // Timeout in milliseconds
serverId?: string; // Orchestrator server ID
}
```
### Saga Step
```typescript
interface SagaStep<T = unknown> {
name: string; // Step name
serverId?: string; // Target server ID
data: T; // Step data
execute: (data: T) => Promise<OperationResult>; // Execute function
compensate: (data: T) => Promise<void>; // Compensate function
}
```
### Saga Result
```typescript
interface SagaResult {
success: boolean; // Whether succeeded
sagaId: string; // Saga ID
completedSteps: string[]; // Completed steps
failedStep?: string; // Failed step
error?: string; // Error message
duration: number; // Execution time (ms)
}
```
## Execution Flow
```
Start Saga
┌─────────────────────┐
│ Step 1: execute │──fail──┐
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ Step 2: execute │──fail──┤
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ Step 3: execute │──fail──┤
└─────────────────────┘ │
│success ▼
▼ ┌─────────────────────┐
Saga Complete │ Step 2: compensate │
└─────────────────────┘
┌─────────────────────┐
│ Step 1: compensate │
└─────────────────────┘
Saga Failed (compensated)
```
## Saga Logs
The orchestrator records detailed execution logs:
```typescript
interface SagaLog {
id: string; // Saga ID
state: SagaLogState; // State
steps: SagaStepLog[]; // Step logs
createdAt: number; // Creation time
updatedAt: number; // Update time
metadata?: Record<string, unknown>;
}
type SagaLogState =
| 'pending' // Waiting to execute
| 'running' // Executing
| 'completed' // Completed
| 'compensating' // Compensating
| 'compensated' // Compensated
| 'failed' // Failed
interface SagaStepLog {
name: string; // Step name
serverId?: string; // Server ID
state: SagaStepState; // State
startedAt?: number; // Start time
completedAt?: number; // Completion time
error?: string; // Error message
}
type SagaStepState =
| 'pending' // Waiting to execute
| 'executing' // Executing
| 'completed' // Completed
| 'compensating' // Compensating
| 'compensated' // Compensated
| 'failed' // Failed
```
### Query Saga Logs
```typescript
const log = await orchestrator.getSagaLog('saga_xxx');
if (log) {
console.log('Saga state:', log.state);
for (const step of log.steps) {
console.log(` ${step.name}: ${step.state}`);
}
}
```
## Cross-Server Transaction Examples
### Scenario: Cross-Server Purchase
A player purchases an item on a game server, with currency on an account server and items on an inventory server.
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'purchase-orchestrator',
});
async function crossServerPurchase(
playerId: string,
itemId: string,
price: number
): Promise<SagaResult> {
return orchestrator.execute([
// Step 1: Deduct balance on account server
{
name: 'deduct_balance',
serverId: 'account-server',
data: { playerId, amount: price },
execute: async (data) => {
const result = await accountService.deduct(data.playerId, data.amount);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await accountService.refund(data.playerId, data.amount);
},
},
// Step 2: Add item on inventory server
{
name: 'add_item',
serverId: 'inventory-server',
data: { playerId, itemId },
execute: async (data) => {
const result = await inventoryService.addItem(data.playerId, data.itemId);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
},
// Step 3: Record purchase log
{
name: 'log_purchase',
serverId: 'log-server',
data: { playerId, itemId, price, timestamp: Date.now() },
execute: async (data) => {
await logService.recordPurchase(data);
return { success: true };
},
compensate: async (data) => {
await logService.cancelPurchase(data);
},
},
]);
}
```
### Scenario: Cross-Server Trade
Two players on different servers trade with each other.
```typescript
async function crossServerTrade(
playerA: { id: string; server: string; items: string[] },
playerB: { id: string; server: string; items: string[] }
): Promise<SagaResult> {
const steps: SagaStep[] = [];
// Remove items from player A
for (const itemId of playerA.items) {
steps.push({
name: `remove_${playerA.id}_${itemId}`,
serverId: playerA.server,
data: { playerId: playerA.id, itemId },
execute: async (data) => {
return await inventoryService.removeItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.addItem(data.playerId, data.itemId);
},
});
}
// Add items to player B (from A)
for (const itemId of playerA.items) {
steps.push({
name: `add_${playerB.id}_${itemId}`,
serverId: playerB.server,
data: { playerId: playerB.id, itemId },
execute: async (data) => {
return await inventoryService.addItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
});
}
// Similarly handle player B's items...
return orchestrator.execute(steps);
}
```
## Recovering Incomplete Sagas
Recover incomplete Sagas after server restart:
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'my-orchestrator',
});
// Recover incomplete Sagas (will execute compensation)
const recoveredCount = await orchestrator.recover();
console.log(`Recovered ${recoveredCount} sagas`);
```
## Best Practices
### 1. Idempotency
Ensure all operations are idempotent:
```typescript
{
execute: async (data) => {
// Use unique ID to ensure idempotency
const result = await service.process(data.requestId, data);
return { success: result.ok };
},
compensate: async (data) => {
// Compensation must also be idempotent
await service.rollback(data.requestId);
},
}
```
### 2. Timeout Handling
Set appropriate timeout values:
```typescript
const orchestrator = new SagaOrchestrator({
timeout: 60000, // Cross-server operations need longer timeout
});
```
### 3. Monitoring and Alerts
Log Saga execution results:
```typescript
const result = await orchestrator.execute(steps);
if (!result.success) {
// Send alert
alertService.send({
type: 'saga_failed',
sagaId: result.sagaId,
failedStep: result.failedStep,
error: result.error,
});
// Log details
const log = await orchestrator.getSagaLog(result.sagaId);
logger.error('Saga failed', { log });
}
```

View File

@@ -0,0 +1,238 @@
---
title: "Transaction System"
description: "Game transaction system with distributed support for shop purchases, player trading, and more"
---
`@esengine/transaction` provides comprehensive game transaction capabilities based on the Saga pattern, supporting shop purchases, player trading, multi-step tasks, and distributed transactions with Redis/MongoDB.
## Overview
The transaction system solves common data consistency problems in games:
| Scenario | Problem | Solution |
|----------|---------|----------|
| Shop Purchase | Payment succeeded but item not delivered | Atomic transaction with auto-rollback |
| Player Trade | One party transferred items but other didn't receive | Saga compensation mechanism |
| Cross-Server | Data inconsistency across servers | Distributed lock + transaction log |
## Installation
```bash
npm install @esengine/transaction
```
Optional dependencies (install based on storage needs):
```bash
npm install ioredis # Redis storage
npm install mongodb # MongoDB storage
```
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Transaction Layer │
├─────────────────────────────────────────────────────────────────┤
│ TransactionManager - Manages transaction lifecycle │
│ TransactionContext - Encapsulates operations and state │
│ SagaOrchestrator - Distributed Saga orchestrator │
├─────────────────────────────────────────────────────────────────┤
│ Storage Layer │
├─────────────────────────────────────────────────────────────────┤
│ MemoryStorage - In-memory (dev/test) │
│ RedisStorage - Redis (distributed lock + cache) │
│ MongoStorage - MongoDB (persistent log) │
├─────────────────────────────────────────────────────────────────┤
│ Operation Layer │
├─────────────────────────────────────────────────────────────────┤
│ CurrencyOperation - Currency operations │
│ InventoryOperation - Inventory operations │
│ TradeOperation - Trade operations │
└─────────────────────────────────────────────────────────────────┘
```
## Quick Start
### Basic Usage
```typescript
import {
TransactionManager,
MemoryStorage,
CurrencyOperation,
InventoryOperation,
} from '@esengine/transaction';
// Create transaction manager
const manager = new TransactionManager({
storage: new MemoryStorage(),
defaultTimeout: 10000,
});
// Execute transaction
const result = await manager.run((tx) => {
// Deduct gold
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
// Add item
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
}));
});
if (result.success) {
console.log('Purchase successful!');
} else {
console.log('Purchase failed:', result.error);
}
```
### Player Trading
```typescript
import { TradeOperation } from '@esengine/transaction';
const result = await manager.run((tx) => {
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
}));
}, { timeout: 30000 });
```
### Using Redis Storage
```typescript
import Redis from 'ioredis';
import { TransactionManager, RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({ client: redis });
const manager = new TransactionManager({ storage });
```
### Using MongoDB Storage
```typescript
import { MongoClient } from 'mongodb';
import { TransactionManager, MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({ db });
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
## Room Integration
```typescript
import { Room } from '@esengine/server';
import { withTransactions, CurrencyOperation, RedisStorage } from '@esengine/transaction';
class GameRoom extends withTransactions(Room, {
storage: new RedisStorage({ client: redisClient }),
}) {
@onMessage('Buy')
async handleBuy(data: { itemId: string }, player: Player) {
const result = await this.runTransaction((tx) => {
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: player.id,
currency: 'gold',
amount: getItemPrice(data.itemId),
}));
});
if (result.success) {
player.send('buy_success', { itemId: data.itemId });
} else {
player.send('buy_failed', { error: result.error });
}
}
}
```
## Documentation
- [Core Concepts](/en/modules/transaction/core/) - Transaction context, manager, Saga pattern
- [Storage Layer](/en/modules/transaction/storage/) - MemoryStorage, RedisStorage, MongoStorage
- [Operations](/en/modules/transaction/operations/) - Currency, inventory, trade operations
- [Distributed Transactions](/en/modules/transaction/distributed/) - Saga orchestrator, cross-server transactions
- [API Reference](/en/modules/transaction/api/) - Complete API documentation
## Service Tokens
For dependency injection:
```typescript
import {
TransactionManagerToken,
TransactionStorageToken,
} from '@esengine/transaction';
const manager = services.get(TransactionManagerToken);
```
## Best Practices
### 1. Operation Granularity
```typescript
// ✅ Good: Fine-grained operations, easy to rollback
tx.addOperation(new CurrencyOperation({ type: 'deduct', ... }));
tx.addOperation(new InventoryOperation({ type: 'add', ... }));
// ❌ Bad: Coarse-grained operation, hard to partially rollback
tx.addOperation(new ComplexPurchaseOperation({ ... }));
```
### 2. Timeout Settings
```typescript
// Simple operations: short timeout
await manager.run(tx => { ... }, { timeout: 5000 });
// Complex trades: longer timeout
await manager.run(tx => { ... }, { timeout: 30000 });
// Cross-server: even longer timeout
await manager.run(tx => { ... }, { timeout: 60000, distributed: true });
```
### 3. Error Handling
```typescript
const result = await manager.run((tx) => { ... });
if (!result.success) {
// Log the error
logger.error('Transaction failed', {
transactionId: result.transactionId,
error: result.error,
duration: result.duration,
});
// Notify user
player.send('error', { message: getErrorMessage(result.error) });
}
```

View File

@@ -0,0 +1,313 @@
---
title: "Operations"
description: "Built-in transaction operations: currency, inventory, trade"
---
## BaseOperation
Base class for all operations, providing a common implementation template.
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
class MyOperation extends BaseOperation<MyData, MyResult> {
readonly name = 'myOperation';
async validate(ctx: ITransactionContext): Promise<boolean> {
// Validate preconditions
return true;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// Execute operation
return this.success({ result: 'ok' });
// or
return this.failure('Something went wrong', 'ERROR_CODE');
}
async compensate(ctx: ITransactionContext): Promise<void> {
// Rollback operation
}
}
```
## CurrencyOperation
Handles currency addition and deduction.
### Deduct Currency
```typescript
import { CurrencyOperation } from '@esengine/transaction';
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
reason: 'purchase_item',
}));
```
### Add Currency
```typescript
tx.addOperation(new CurrencyOperation({
type: 'add',
playerId: 'player1',
currency: 'diamond',
amount: 50,
reason: 'daily_reward',
}));
```
### Operation Data
```typescript
interface CurrencyOperationData {
type: 'add' | 'deduct'; // Operation type
playerId: string; // Player ID
currency: string; // Currency type
amount: number; // Amount
reason?: string; // Reason/source
}
```
### Operation Result
```typescript
interface CurrencyOperationResult {
beforeBalance: number; // Balance before operation
afterBalance: number; // Balance after operation
}
```
### Custom Data Provider
```typescript
interface ICurrencyProvider {
getBalance(playerId: string, currency: string): Promise<number>;
setBalance(playerId: string, currency: string, amount: number): Promise<void>;
}
class MyCurrencyProvider implements ICurrencyProvider {
async getBalance(playerId: string, currency: string): Promise<number> {
// Get balance from database
return await db.getCurrency(playerId, currency);
}
async setBalance(playerId: string, currency: string, amount: number): Promise<void> {
// Save to database
await db.setCurrency(playerId, currency, amount);
}
}
// Use custom provider
const op = new CurrencyOperation({ ... });
op.setProvider(new MyCurrencyProvider());
tx.addOperation(op);
```
## InventoryOperation
Handles item addition, removal, and updates.
### Add Item
```typescript
import { InventoryOperation } from '@esengine/transaction';
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
properties: { enchant: 'fire' },
}));
```
### Remove Item
```typescript
tx.addOperation(new InventoryOperation({
type: 'remove',
playerId: 'player1',
itemId: 'potion_hp',
quantity: 5,
}));
```
### Update Item
```typescript
tx.addOperation(new InventoryOperation({
type: 'update',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1, // Optional, keeps original if not provided
properties: { enchant: 'lightning', level: 5 },
}));
```
### Operation Data
```typescript
interface InventoryOperationData {
type: 'add' | 'remove' | 'update'; // Operation type
playerId: string; // Player ID
itemId: string; // Item ID
quantity: number; // Quantity
properties?: Record<string, unknown>; // Item properties
reason?: string; // Reason/source
}
```
### Operation Result
```typescript
interface InventoryOperationResult {
beforeItem?: ItemData; // Item before operation
afterItem?: ItemData; // Item after operation
}
interface ItemData {
itemId: string;
quantity: number;
properties?: Record<string, unknown>;
}
```
### Custom Data Provider
```typescript
interface IInventoryProvider {
getItem(playerId: string, itemId: string): Promise<ItemData | null>;
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>;
hasCapacity?(playerId: string, count: number): Promise<boolean>;
}
class MyInventoryProvider implements IInventoryProvider {
async getItem(playerId: string, itemId: string): Promise<ItemData | null> {
return await db.getItem(playerId, itemId);
}
async setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void> {
if (item) {
await db.saveItem(playerId, itemId, item);
} else {
await db.deleteItem(playerId, itemId);
}
}
async hasCapacity(playerId: string, count: number): Promise<boolean> {
const current = await db.getItemCount(playerId);
const max = await db.getMaxCapacity(playerId);
return current + count <= max;
}
}
```
## TradeOperation
Handles item and currency exchange between players.
### Basic Usage
```typescript
import { TradeOperation } from '@esengine/transaction';
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
currencies: [{ currency: 'diamond', amount: 10 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
reason: 'player_trade',
}));
```
### Operation Data
```typescript
interface TradeOperationData {
tradeId: string; // Trade ID
partyA: TradeParty; // Trade initiator
partyB: TradeParty; // Trade receiver
reason?: string; // Reason/note
}
interface TradeParty {
playerId: string; // Player ID
items?: TradeItem[]; // Items to give
currencies?: TradeCurrency[]; // Currencies to give
}
interface TradeItem {
itemId: string;
quantity: number;
}
interface TradeCurrency {
currency: string;
amount: number;
}
```
### Execution Flow
TradeOperation internally generates the following sub-operation sequence:
```
1. Remove partyA's items
2. Add items to partyB (from partyA)
3. Deduct partyA's currencies
4. Add currencies to partyB (from partyA)
5. Remove partyB's items
6. Add items to partyA (from partyB)
7. Deduct partyB's currencies
8. Add currencies to partyA (from partyB)
```
If any step fails, all previous operations are rolled back.
### Using Custom Providers
```typescript
const op = new TradeOperation({ ... });
op.setProvider({
currencyProvider: new MyCurrencyProvider(),
inventoryProvider: new MyInventoryProvider(),
});
tx.addOperation(op);
```
## Factory Functions
Each operation class provides a factory function:
```typescript
import {
createCurrencyOperation,
createInventoryOperation,
createTradeOperation,
} from '@esengine/transaction';
tx.addOperation(createCurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
tx.addOperation(createInventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword',
quantity: 1,
}));
```

View File

@@ -0,0 +1,215 @@
---
title: "Storage Layer"
description: "Transaction storage interface and implementations: MemoryStorage, RedisStorage, MongoStorage"
---
## Storage Interface
All storage implementations must implement the `ITransactionStorage` interface:
```typescript
interface ITransactionStorage {
// Distributed lock
acquireLock(key: string, ttl: number): Promise<string | null>;
releaseLock(key: string, token: string): Promise<boolean>;
// Transaction log
saveTransaction(tx: TransactionLog): Promise<void>;
getTransaction(id: string): Promise<TransactionLog | null>;
updateTransactionState(id: string, state: TransactionState): Promise<void>;
updateOperationState(txId: string, opIndex: number, state: string, error?: string): Promise<void>;
getPendingTransactions(serverId?: string): Promise<TransactionLog[]>;
deleteTransaction(id: string): Promise<void>;
// Data operations
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
}
```
## MemoryStorage
In-memory storage, suitable for development and testing.
```typescript
import { MemoryStorage } from '@esengine/transaction';
const storage = new MemoryStorage({
maxTransactions: 1000, // Maximum transaction log count
});
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ No external dependencies
- ✅ Fast, good for debugging
- ❌ Data only stored in memory
- ❌ No true distributed locking
- ❌ Data lost on restart
### Test Helpers
```typescript
// Clear all data
storage.clear();
// Get transaction count
console.log(storage.transactionCount);
```
## RedisStorage
Redis storage, suitable for production distributed systems.
```typescript
import Redis from 'ioredis';
import { RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({
client: redis,
prefix: 'tx:', // Key prefix
transactionTTL: 86400, // Transaction log TTL (seconds)
});
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ High-performance distributed locking
- ✅ Fast read/write
- ✅ Supports TTL auto-expiration
- ✅ Suitable for high concurrency
- ❌ Requires Redis server
### Distributed Lock Implementation
Uses Redis `SET NX EX` for distributed locking:
```typescript
// Acquire lock (atomic operation)
SET tx:lock:player:123 <token> NX EX 10
// Release lock (Lua script for atomicity)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
```
### Key Structure
```
tx:lock:{key} - Distributed locks
tx:tx:{id} - Transaction logs
tx:server:{id}:txs - Server transaction index
tx:data:{key} - Business data
```
## MongoStorage
MongoDB storage, suitable for scenarios requiring persistence and complex queries.
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({
db,
transactionCollection: 'transactions', // Transaction log collection
dataCollection: 'transaction_data', // Business data collection
lockCollection: 'transaction_locks', // Lock collection
});
// Create indexes (run on first startup)
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ Persistent storage
- ✅ Supports complex queries
- ✅ Transaction logs are traceable
- ✅ Suitable for audit requirements
- ❌ Slightly lower performance than Redis
- ❌ Requires MongoDB server
### Index Structure
```javascript
// transactions collection
{ state: 1 }
{ 'metadata.serverId': 1 }
{ createdAt: 1 }
// transaction_locks collection
{ expireAt: 1 } // TTL index
// transaction_data collection
{ expireAt: 1 } // TTL index
```
### Distributed Lock Implementation
Uses MongoDB unique index for distributed locking:
```typescript
// Acquire lock
db.transaction_locks.insertOne({
_id: 'player:123',
token: '<token>',
expireAt: new Date(Date.now() + 10000)
});
// If key exists, check if expired
db.transaction_locks.updateOne(
{ _id: 'player:123', expireAt: { $lt: new Date() } },
{ $set: { token: '<token>', expireAt: new Date(Date.now() + 10000) } }
);
```
## Storage Selection Guide
| Scenario | Recommended Storage | Reason |
|----------|---------------------|--------|
| Development/Testing | MemoryStorage | No dependencies, fast startup |
| Single-machine Production | RedisStorage | High performance, simple |
| Distributed System | RedisStorage | True distributed locking |
| Audit Required | MongoStorage | Persistent logs |
| Mixed Requirements | Redis + Mongo | Redis for locks, Mongo for logs |
## Custom Storage
Implement `ITransactionStorage` interface to create custom storage:
```typescript
import { ITransactionStorage, TransactionLog, TransactionState } from '@esengine/transaction';
class MyCustomStorage implements ITransactionStorage {
async acquireLock(key: string, ttl: number): Promise<string | null> {
// Implement distributed lock acquisition
}
async releaseLock(key: string, token: string): Promise<boolean> {
// Implement distributed lock release
}
async saveTransaction(tx: TransactionLog): Promise<void> {
// Save transaction log
}
// ... implement other methods
}
```

View File

@@ -35,6 +35,7 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中
| 模块 | 包名 | 描述 |
|------|------|------|
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
| [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 |
## 安装

View File

@@ -0,0 +1,261 @@
---
title: "核心概念"
description: "事务系统的核心概念事务上下文、事务管理器、Saga 模式"
---
## 事务状态
事务有以下几种状态:
```typescript
type TransactionState =
| 'pending' // 等待执行
| 'executing' // 执行中
| 'committed' // 已提交
| 'rolledback' // 已回滚
| 'failed' // 失败
```
## TransactionContext
事务上下文封装了事务的状态、操作和执行逻辑。
### 创建事务
```typescript
import { TransactionManager } from '@esengine/transaction';
const manager = new TransactionManager();
// 方式 1使用 begin() 手动管理
const tx = manager.begin({ timeout: 5000 });
tx.addOperation(op1);
tx.addOperation(op2);
const result = await tx.execute();
// 方式 2使用 run() 自动管理
const result = await manager.run((tx) => {
tx.addOperation(op1);
tx.addOperation(op2);
});
```
### 链式添加操作
```typescript
const result = await manager.run((tx) => {
tx.addOperation(new CurrencyOperation({ ... }))
.addOperation(new InventoryOperation({ ... }))
.addOperation(new InventoryOperation({ ... }));
});
```
### 上下文数据
操作之间可以通过上下文共享数据:
```typescript
class CustomOperation extends BaseOperation<MyData, MyResult> {
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// 读取之前操作设置的数据
const previousResult = ctx.get<number>('previousValue');
// 设置数据供后续操作使用
ctx.set('myResult', { value: 123 });
return this.success({ ... });
}
}
```
## TransactionManager
事务管理器负责创建、执行和恢复事务。
### 配置选项
```typescript
interface TransactionManagerConfig {
storage?: ITransactionStorage; // 存储实例
defaultTimeout?: number; // 默认超时(毫秒)
serverId?: string; // 服务器 ID分布式用
autoRecover?: boolean; // 自动恢复未完成事务
}
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
defaultTimeout: 10000,
serverId: 'server-1',
autoRecover: true,
});
```
### 分布式锁
```typescript
// 获取锁
const token = await manager.acquireLock('player:123:inventory', 10000);
if (token) {
try {
// 执行操作
await doSomething();
} finally {
// 释放锁
await manager.releaseLock('player:123:inventory', token);
}
}
// 或使用 withLock 简化
await manager.withLock('player:123:inventory', async () => {
await doSomething();
}, 10000);
```
### 事务恢复
服务器重启时恢复未完成的事务:
```typescript
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
serverId: 'server-1',
});
// 恢复未完成的事务
const recoveredCount = await manager.recover();
console.log(`Recovered ${recoveredCount} transactions`);
```
## Saga 模式
事务系统采用 Saga 模式,每个操作必须实现 `execute``compensate` 方法:
```typescript
interface ITransactionOperation<TData, TResult> {
readonly name: string;
readonly data: TData;
// 验证前置条件
validate(ctx: ITransactionContext): Promise<boolean>;
// 正向执行
execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>;
// 补偿操作(回滚)
compensate(ctx: ITransactionContext): Promise<void>;
}
```
### 执行流程
```
开始事务
┌─────────────────────┐
│ validate(op1) │──失败──► 返回失败
└─────────────────────┘
│成功
┌─────────────────────┐
│ execute(op1) │──失败──┐
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ validate(op2) │──失败──┤
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ execute(op2) │──失败──┤
└─────────────────────┘ │
│成功 ▼
▼ ┌─────────────────────┐
提交事务 │ compensate(op1) │
└─────────────────────┘
返回失败(已回滚)
```
### 自定义操作
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
interface UpgradeData {
playerId: string;
itemId: string;
targetLevel: number;
}
interface UpgradeResult {
newLevel: number;
}
class UpgradeOperation extends BaseOperation<UpgradeData, UpgradeResult> {
readonly name = 'upgrade';
private _previousLevel: number = 0;
async validate(ctx: ITransactionContext): Promise<boolean> {
// 验证物品存在且可升级
const item = await this.getItem(ctx);
return item !== null && item.level < this.data.targetLevel;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<UpgradeResult>> {
const item = await this.getItem(ctx);
if (!item) {
return this.failure('Item not found', 'ITEM_NOT_FOUND');
}
this._previousLevel = item.level;
item.level = this.data.targetLevel;
await this.saveItem(ctx, item);
return this.success({ newLevel: item.level });
}
async compensate(ctx: ITransactionContext): Promise<void> {
const item = await this.getItem(ctx);
if (item) {
item.level = this._previousLevel;
await this.saveItem(ctx, item);
}
}
private async getItem(ctx: ITransactionContext) {
// 从存储获取物品
}
private async saveItem(ctx: ITransactionContext, item: any) {
// 保存物品到存储
}
}
```
## 事务结果
```typescript
interface TransactionResult<T = unknown> {
success: boolean; // 是否成功
transactionId: string; // 事务 ID
results: OperationResult[]; // 各操作结果
data?: T; // 最终数据
error?: string; // 错误信息
duration: number; // 执行时间(毫秒)
}
const result = await manager.run((tx) => { ... });
console.log(`Transaction ${result.transactionId}`);
console.log(`Success: ${result.success}`);
console.log(`Duration: ${result.duration}ms`);
if (!result.success) {
console.log(`Error: ${result.error}`);
}
```

View File

@@ -0,0 +1,355 @@
---
title: "分布式事务"
description: "Saga 编排器和跨服务器事务支持"
---
## Saga 编排器
`SagaOrchestrator` 用于编排跨服务器的分布式事务。
### 基本用法
```typescript
import { SagaOrchestrator, RedisStorage } from '@esengine/transaction';
const orchestrator = new SagaOrchestrator({
storage: new RedisStorage({ client: redis }),
timeout: 30000,
serverId: 'orchestrator-1',
});
const result = await orchestrator.execute([
{
name: 'deduct_currency',
serverId: 'game-server-1',
data: { playerId: 'player1', amount: 100 },
execute: async (data) => {
// 调用游戏服务器 API 扣除货币
const response = await gameServerApi.deductCurrency(data);
return { success: response.ok };
},
compensate: async (data) => {
// 调用游戏服务器 API 恢复货币
await gameServerApi.addCurrency(data);
},
},
{
name: 'add_item',
serverId: 'inventory-server-1',
data: { playerId: 'player1', itemId: 'sword' },
execute: async (data) => {
const response = await inventoryServerApi.addItem(data);
return { success: response.ok };
},
compensate: async (data) => {
await inventoryServerApi.removeItem(data);
},
},
]);
if (result.success) {
console.log('Saga completed successfully');
} else {
console.log('Saga failed:', result.error);
console.log('Completed steps:', result.completedSteps);
console.log('Failed at:', result.failedStep);
}
```
### 配置选项
```typescript
interface SagaOrchestratorConfig {
storage?: ITransactionStorage; // 存储实例
timeout?: number; // 超时时间(毫秒)
serverId?: string; // 编排器服务器 ID
}
```
### Saga 步骤
```typescript
interface SagaStep<T = unknown> {
name: string; // 步骤名称
serverId?: string; // 目标服务器 ID
data: T; // 步骤数据
execute: (data: T) => Promise<OperationResult>; // 执行函数
compensate: (data: T) => Promise<void>; // 补偿函数
}
```
### Saga 结果
```typescript
interface SagaResult {
success: boolean; // 是否成功
sagaId: string; // Saga ID
completedSteps: string[]; // 已完成的步骤
failedStep?: string; // 失败的步骤
error?: string; // 错误信息
duration: number; // 执行时间(毫秒)
}
```
## 执行流程
```
开始 Saga
┌─────────────────────┐
│ Step 1: execute │──失败──┐
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ Step 2: execute │──失败──┤
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ Step 3: execute │──失败──┤
└─────────────────────┘ │
│成功 ▼
▼ ┌─────────────────────┐
Saga 完成 │ Step 2: compensate │
└─────────────────────┘
┌─────────────────────┐
│ Step 1: compensate │
└─────────────────────┘
Saga 失败(已补偿)
```
## Saga 日志
编排器会记录详细的执行日志:
```typescript
interface SagaLog {
id: string; // Saga ID
state: SagaLogState; // 状态
steps: SagaStepLog[]; // 步骤日志
createdAt: number; // 创建时间
updatedAt: number; // 更新时间
metadata?: Record<string, unknown>;
}
type SagaLogState =
| 'pending' // 等待执行
| 'running' // 执行中
| 'completed' // 已完成
| 'compensating' // 补偿中
| 'compensated' // 已补偿
| 'failed' // 失败
interface SagaStepLog {
name: string; // 步骤名称
serverId?: string; // 服务器 ID
state: SagaStepState; // 状态
startedAt?: number; // 开始时间
completedAt?: number; // 完成时间
error?: string; // 错误信息
}
type SagaStepState =
| 'pending' // 等待执行
| 'executing' // 执行中
| 'completed' // 已完成
| 'compensating' // 补偿中
| 'compensated' // 已补偿
| 'failed' // 失败
```
### 查询 Saga 日志
```typescript
const log = await orchestrator.getSagaLog('saga_xxx');
if (log) {
console.log('Saga state:', log.state);
for (const step of log.steps) {
console.log(` ${step.name}: ${step.state}`);
}
}
```
## 跨服务器事务示例
### 场景:跨服购买
玩家在游戏服务器购买物品,货币在账户服务器,物品在背包服务器。
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'purchase-orchestrator',
});
async function crossServerPurchase(
playerId: string,
itemId: string,
price: number
): Promise<SagaResult> {
return orchestrator.execute([
// 步骤 1在账户服务器扣款
{
name: 'deduct_balance',
serverId: 'account-server',
data: { playerId, amount: price },
execute: async (data) => {
const result = await accountService.deduct(data.playerId, data.amount);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await accountService.refund(data.playerId, data.amount);
},
},
// 步骤 2在背包服务器添加物品
{
name: 'add_item',
serverId: 'inventory-server',
data: { playerId, itemId },
execute: async (data) => {
const result = await inventoryService.addItem(data.playerId, data.itemId);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
},
// 步骤 3记录购买日志
{
name: 'log_purchase',
serverId: 'log-server',
data: { playerId, itemId, price, timestamp: Date.now() },
execute: async (data) => {
await logService.recordPurchase(data);
return { success: true };
},
compensate: async (data) => {
await logService.cancelPurchase(data);
},
},
]);
}
```
### 场景:跨服交易
两个玩家在不同服务器上进行交易。
```typescript
async function crossServerTrade(
playerA: { id: string; server: string; items: string[] },
playerB: { id: string; server: string; items: string[] }
): Promise<SagaResult> {
const steps: SagaStep[] = [];
// 移除 A 的物品
for (const itemId of playerA.items) {
steps.push({
name: `remove_${playerA.id}_${itemId}`,
serverId: playerA.server,
data: { playerId: playerA.id, itemId },
execute: async (data) => {
return await inventoryService.removeItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.addItem(data.playerId, data.itemId);
},
});
}
// 添加物品到 B
for (const itemId of playerA.items) {
steps.push({
name: `add_${playerB.id}_${itemId}`,
serverId: playerB.server,
data: { playerId: playerB.id, itemId },
execute: async (data) => {
return await inventoryService.addItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
});
}
// 类似地处理 B 的物品...
return orchestrator.execute(steps);
}
```
## 恢复未完成的 Saga
服务器重启后恢复未完成的 Saga
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'my-orchestrator',
});
// 恢复未完成的 Saga会执行补偿
const recoveredCount = await orchestrator.recover();
console.log(`Recovered ${recoveredCount} sagas`);
```
## 最佳实践
### 1. 幂等性
确保所有操作都是幂等的:
```typescript
{
execute: async (data) => {
// 使用唯一 ID 确保幂等
const result = await service.process(data.requestId, data);
return { success: result.ok };
},
compensate: async (data) => {
// 补偿也要幂等
await service.rollback(data.requestId);
},
}
```
### 2. 超时处理
设置合适的超时时间:
```typescript
const orchestrator = new SagaOrchestrator({
timeout: 60000, // 跨服务器操作需要更长超时
});
```
### 3. 监控和告警
记录 Saga 执行结果:
```typescript
const result = await orchestrator.execute(steps);
if (!result.success) {
// 发送告警
alertService.send({
type: 'saga_failed',
sagaId: result.sagaId,
failedStep: result.failedStep,
error: result.error,
});
// 记录详细日志
const log = await orchestrator.getSagaLog(result.sagaId);
logger.error('Saga failed', { log });
}
```

View File

@@ -0,0 +1,238 @@
---
title: "事务系统 (Transaction)"
description: "游戏事务处理系统,支持商店购买、玩家交易、分布式事务"
---
`@esengine/transaction` 提供完整的游戏事务处理能力,基于 Saga 模式实现,支持商店购买、玩家交易、多步骤任务等场景,并提供 Redis/MongoDB 分布式事务支持。
## 概述
事务系统解决游戏中常见的数据一致性问题:
| 场景 | 问题 | 解决方案 |
|------|------|----------|
| 商店购买 | 扣款成功但物品未发放 | 原子事务,失败自动回滚 |
| 玩家交易 | 一方物品转移另一方未收到 | Saga 补偿机制 |
| 跨服操作 | 多服务器数据不一致 | 分布式锁 + 事务日志 |
## 安装
```bash
npm install @esengine/transaction
```
可选依赖(根据存储需求安装):
```bash
npm install ioredis # Redis 存储
npm install mongodb # MongoDB 存储
```
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Transaction Layer │
├─────────────────────────────────────────────────────────────────┤
│ TransactionManager - 事务管理器,协调事务生命周期 │
│ TransactionContext - 事务上下文,封装操作和状态 │
│ SagaOrchestrator - 分布式 Saga 编排器 │
├─────────────────────────────────────────────────────────────────┤
│ Storage Layer │
├─────────────────────────────────────────────────────────────────┤
│ MemoryStorage - 内存存储(开发/测试) │
│ RedisStorage - Redis分布式锁 + 缓存) │
│ MongoStorage - MongoDB持久化日志
├─────────────────────────────────────────────────────────────────┤
│ Operation Layer │
├─────────────────────────────────────────────────────────────────┤
│ CurrencyOperation - 货币操作 │
│ InventoryOperation - 背包操作 │
│ TradeOperation - 交易操作 │
└─────────────────────────────────────────────────────────────────┘
```
## 快速开始
### 基础用法
```typescript
import {
TransactionManager,
MemoryStorage,
CurrencyOperation,
InventoryOperation,
} from '@esengine/transaction';
// 创建事务管理器
const manager = new TransactionManager({
storage: new MemoryStorage(),
defaultTimeout: 10000,
});
// 执行事务
const result = await manager.run((tx) => {
// 扣除金币
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
// 添加物品
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
}));
});
if (result.success) {
console.log('购买成功!');
} else {
console.log('购买失败:', result.error);
}
```
### 玩家交易
```typescript
import { TradeOperation } from '@esengine/transaction';
const result = await manager.run((tx) => {
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
}));
}, { timeout: 30000 });
```
### 使用 Redis 存储
```typescript
import Redis from 'ioredis';
import { TransactionManager, RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({ client: redis });
const manager = new TransactionManager({ storage });
```
### 使用 MongoDB 存储
```typescript
import { MongoClient } from 'mongodb';
import { TransactionManager, MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({ db });
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
## 与 Room 集成
```typescript
import { Room } from '@esengine/server';
import { withTransactions, CurrencyOperation, RedisStorage } from '@esengine/transaction';
class GameRoom extends withTransactions(Room, {
storage: new RedisStorage({ client: redisClient }),
}) {
@onMessage('Buy')
async handleBuy(data: { itemId: string }, player: Player) {
const result = await this.runTransaction((tx) => {
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: player.id,
currency: 'gold',
amount: getItemPrice(data.itemId),
}));
});
if (result.success) {
player.send('buy_success', { itemId: data.itemId });
} else {
player.send('buy_failed', { error: result.error });
}
}
}
```
## 文档导航
- [核心概念](/modules/transaction/core/) - 事务上下文、管理器、Saga 模式
- [存储层](/modules/transaction/storage/) - MemoryStorage、RedisStorage、MongoStorage
- [操作类](/modules/transaction/operations/) - 货币、背包、交易操作
- [分布式事务](/modules/transaction/distributed/) - Saga 编排器、跨服务器事务
- [API 参考](/modules/transaction/api/) - 完整 API 文档
## 服务令牌
用于依赖注入:
```typescript
import {
TransactionManagerToken,
TransactionStorageToken,
} from '@esengine/transaction';
const manager = services.get(TransactionManagerToken);
```
## 最佳实践
### 1. 操作粒度
```typescript
// ✅ 好:细粒度操作,便于回滚
tx.addOperation(new CurrencyOperation({ type: 'deduct', ... }));
tx.addOperation(new InventoryOperation({ type: 'add', ... }));
// ❌ 差:粗粒度操作,难以部分回滚
tx.addOperation(new ComplexPurchaseOperation({ ... }));
```
### 2. 超时设置
```typescript
// 简单操作:短超时
await manager.run(tx => { ... }, { timeout: 5000 });
// 复杂交易:长超时
await manager.run(tx => { ... }, { timeout: 30000 });
// 跨服务器:更长超时
await manager.run(tx => { ... }, { timeout: 60000, distributed: true });
```
### 3. 错误处理
```typescript
const result = await manager.run((tx) => { ... });
if (!result.success) {
// 记录日志
logger.error('Transaction failed', {
transactionId: result.transactionId,
error: result.error,
duration: result.duration,
});
// 通知用户
player.send('error', { message: getErrorMessage(result.error) });
}
```

View File

@@ -0,0 +1,313 @@
---
title: "操作类"
description: "内置的事务操作:货币、背包、交易"
---
## BaseOperation
所有操作类的基类,提供通用的实现模板。
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
class MyOperation extends BaseOperation<MyData, MyResult> {
readonly name = 'myOperation';
async validate(ctx: ITransactionContext): Promise<boolean> {
// 验证前置条件
return true;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// 执行操作
return this.success({ result: 'ok' });
// 或
return this.failure('Something went wrong', 'ERROR_CODE');
}
async compensate(ctx: ITransactionContext): Promise<void> {
// 回滚操作
}
}
```
## CurrencyOperation
处理货币的增加和扣除。
### 扣除货币
```typescript
import { CurrencyOperation } from '@esengine/transaction';
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
reason: 'purchase_item',
}));
```
### 增加货币
```typescript
tx.addOperation(new CurrencyOperation({
type: 'add',
playerId: 'player1',
currency: 'diamond',
amount: 50,
reason: 'daily_reward',
}));
```
### 操作数据
```typescript
interface CurrencyOperationData {
type: 'add' | 'deduct'; // 操作类型
playerId: string; // 玩家 ID
currency: string; // 货币类型
amount: number; // 数量
reason?: string; // 原因/来源
}
```
### 操作结果
```typescript
interface CurrencyOperationResult {
beforeBalance: number; // 操作前余额
afterBalance: number; // 操作后余额
}
```
### 自定义数据提供者
```typescript
interface ICurrencyProvider {
getBalance(playerId: string, currency: string): Promise<number>;
setBalance(playerId: string, currency: string, amount: number): Promise<void>;
}
class MyCurrencyProvider implements ICurrencyProvider {
async getBalance(playerId: string, currency: string): Promise<number> {
// 从数据库获取余额
return await db.getCurrency(playerId, currency);
}
async setBalance(playerId: string, currency: string, amount: number): Promise<void> {
// 保存到数据库
await db.setCurrency(playerId, currency, amount);
}
}
// 使用自定义提供者
const op = new CurrencyOperation({ ... });
op.setProvider(new MyCurrencyProvider());
tx.addOperation(op);
```
## InventoryOperation
处理物品的添加、移除和更新。
### 添加物品
```typescript
import { InventoryOperation } from '@esengine/transaction';
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
properties: { enchant: 'fire' },
}));
```
### 移除物品
```typescript
tx.addOperation(new InventoryOperation({
type: 'remove',
playerId: 'player1',
itemId: 'potion_hp',
quantity: 5,
}));
```
### 更新物品
```typescript
tx.addOperation(new InventoryOperation({
type: 'update',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1, // 可选,不传则保持原数量
properties: { enchant: 'lightning', level: 5 },
}));
```
### 操作数据
```typescript
interface InventoryOperationData {
type: 'add' | 'remove' | 'update'; // 操作类型
playerId: string; // 玩家 ID
itemId: string; // 物品 ID
quantity: number; // 数量
properties?: Record<string, unknown>; // 物品属性
reason?: string; // 原因/来源
}
```
### 操作结果
```typescript
interface InventoryOperationResult {
beforeItem?: ItemData; // 操作前物品
afterItem?: ItemData; // 操作后物品
}
interface ItemData {
itemId: string;
quantity: number;
properties?: Record<string, unknown>;
}
```
### 自定义数据提供者
```typescript
interface IInventoryProvider {
getItem(playerId: string, itemId: string): Promise<ItemData | null>;
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>;
hasCapacity?(playerId: string, count: number): Promise<boolean>;
}
class MyInventoryProvider implements IInventoryProvider {
async getItem(playerId: string, itemId: string): Promise<ItemData | null> {
return await db.getItem(playerId, itemId);
}
async setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void> {
if (item) {
await db.saveItem(playerId, itemId, item);
} else {
await db.deleteItem(playerId, itemId);
}
}
async hasCapacity(playerId: string, count: number): Promise<boolean> {
const current = await db.getItemCount(playerId);
const max = await db.getMaxCapacity(playerId);
return current + count <= max;
}
}
```
## TradeOperation
处理玩家之间的物品和货币交换。
### 基本用法
```typescript
import { TradeOperation } from '@esengine/transaction';
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
currencies: [{ currency: 'diamond', amount: 10 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
reason: 'player_trade',
}));
```
### 操作数据
```typescript
interface TradeOperationData {
tradeId: string; // 交易 ID
partyA: TradeParty; // 交易发起方
partyB: TradeParty; // 交易接收方
reason?: string; // 原因/备注
}
interface TradeParty {
playerId: string; // 玩家 ID
items?: TradeItem[]; // 给出的物品
currencies?: TradeCurrency[]; // 给出的货币
}
interface TradeItem {
itemId: string;
quantity: number;
}
interface TradeCurrency {
currency: string;
amount: number;
}
```
### 执行流程
TradeOperation 内部会生成以下子操作序列:
```
1. 移除 partyA 的物品
2. 添加 partyB 的物品(来自 partyA
3. 扣除 partyA 的货币
4. 增加 partyB 的货币(来自 partyA
5. 移除 partyB 的物品
6. 添加 partyA 的物品(来自 partyB
7. 扣除 partyB 的货币
8. 增加 partyA 的货币(来自 partyB
```
任何一步失败都会回滚之前的所有操作。
### 使用自定义提供者
```typescript
const op = new TradeOperation({ ... });
op.setProvider({
currencyProvider: new MyCurrencyProvider(),
inventoryProvider: new MyInventoryProvider(),
});
tx.addOperation(op);
```
## 创建工厂函数
每个操作类都提供工厂函数:
```typescript
import {
createCurrencyOperation,
createInventoryOperation,
createTradeOperation,
} from '@esengine/transaction';
tx.addOperation(createCurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
tx.addOperation(createInventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword',
quantity: 1,
}));
```

View File

@@ -0,0 +1,215 @@
---
title: "存储层"
description: "事务存储接口和实现MemoryStorage、RedisStorage、MongoStorage"
---
## 存储接口
所有存储实现都需要实现 `ITransactionStorage` 接口:
```typescript
interface ITransactionStorage {
// 分布式锁
acquireLock(key: string, ttl: number): Promise<string | null>;
releaseLock(key: string, token: string): Promise<boolean>;
// 事务日志
saveTransaction(tx: TransactionLog): Promise<void>;
getTransaction(id: string): Promise<TransactionLog | null>;
updateTransactionState(id: string, state: TransactionState): Promise<void>;
updateOperationState(txId: string, opIndex: number, state: string, error?: string): Promise<void>;
getPendingTransactions(serverId?: string): Promise<TransactionLog[]>;
deleteTransaction(id: string): Promise<void>;
// 数据操作
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
}
```
## MemoryStorage
内存存储,适用于开发和测试环境。
```typescript
import { MemoryStorage } from '@esengine/transaction';
const storage = new MemoryStorage({
maxTransactions: 1000, // 最大事务日志数量
});
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 无需外部依赖
- ✅ 快速,适合开发调试
- ❌ 数据仅保存在内存中
- ❌ 不支持真正的分布式锁
- ❌ 服务重启后数据丢失
### 测试辅助
```typescript
// 清空所有数据
storage.clear();
// 获取事务数量
console.log(storage.transactionCount);
```
## RedisStorage
Redis 存储,适用于生产环境的分布式系统。
```typescript
import Redis from 'ioredis';
import { RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({
client: redis,
prefix: 'tx:', // 键前缀
transactionTTL: 86400, // 事务日志过期时间(秒)
});
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 高性能分布式锁
- ✅ 快速读写
- ✅ 支持 TTL 自动过期
- ✅ 适合高并发场景
- ❌ 需要 Redis 服务器
### 分布式锁实现
使用 Redis `SET NX EX` 实现分布式锁:
```typescript
// 获取锁(原子操作)
SET tx:lock:player:123 <token> NX EX 10
// 释放锁Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
```
### 键结构
```
tx:lock:{key} - 分布式锁
tx:tx:{id} - 事务日志
tx:server:{id}:txs - 服务器事务索引
tx:data:{key} - 业务数据
```
## MongoStorage
MongoDB 存储,适用于需要持久化和复杂查询的场景。
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({
db,
transactionCollection: 'transactions', // 事务日志集合
dataCollection: 'transaction_data', // 业务数据集合
lockCollection: 'transaction_locks', // 锁集合
});
// 创建索引(首次运行时执行)
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 持久化存储
- ✅ 支持复杂查询
- ✅ 事务日志可追溯
- ✅ 适合需要审计的场景
- ❌ 相比 Redis 性能略低
- ❌ 需要 MongoDB 服务器
### 索引结构
```javascript
// transactions 集合
{ state: 1 }
{ 'metadata.serverId': 1 }
{ createdAt: 1 }
// transaction_locks 集合
{ expireAt: 1 } // TTL 索引
// transaction_data 集合
{ expireAt: 1 } // TTL 索引
```
### 分布式锁实现
使用 MongoDB 唯一索引实现分布式锁:
```typescript
// 获取锁
db.transaction_locks.insertOne({
_id: 'player:123',
token: '<token>',
expireAt: new Date(Date.now() + 10000)
});
// 如果键已存在,检查是否过期
db.transaction_locks.updateOne(
{ _id: 'player:123', expireAt: { $lt: new Date() } },
{ $set: { token: '<token>', expireAt: new Date(Date.now() + 10000) } }
);
```
## 存储选择指南
| 场景 | 推荐存储 | 理由 |
|------|----------|------|
| 开发/测试 | MemoryStorage | 无依赖,快速启动 |
| 单机生产 | RedisStorage | 高性能,简单 |
| 分布式系统 | RedisStorage | 真正的分布式锁 |
| 需要审计 | MongoStorage | 持久化日志 |
| 混合需求 | Redis + Mongo | Redis 做锁Mongo 做日志 |
## 自定义存储
实现 `ITransactionStorage` 接口创建自定义存储:
```typescript
import { ITransactionStorage, TransactionLog, TransactionState } from '@esengine/transaction';
class MyCustomStorage implements ITransactionStorage {
async acquireLock(key: string, ttl: number): Promise<string | null> {
// 实现分布式锁获取逻辑
}
async releaseLock(key: string, token: string): Promise<boolean> {
// 实现分布式锁释放逻辑
}
async saveTransaction(tx: TransactionLog): Promise<void> {
// 保存事务日志
}
// ... 实现其他方法
}
```