feat(transaction): 添加游戏事务系统 | add game transaction system (#381)
- TransactionManager/TransactionContext 事务管理 - MemoryStorage/RedisStorage/MongoStorage 存储实现 - CurrencyOperation/InventoryOperation/TradeOperation 内置操作 - SagaOrchestrator 分布式 Saga 编排 - withTransactions() Room 集成 - 完整中英文文档
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
261
docs/src/content/docs/en/modules/transaction/core.md
Normal file
261
docs/src/content/docs/en/modules/transaction/core.md
Normal 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}`);
|
||||
}
|
||||
```
|
||||
355
docs/src/content/docs/en/modules/transaction/distributed.md
Normal file
355
docs/src/content/docs/en/modules/transaction/distributed.md
Normal 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 });
|
||||
}
|
||||
```
|
||||
238
docs/src/content/docs/en/modules/transaction/index.md
Normal file
238
docs/src/content/docs/en/modules/transaction/index.md
Normal 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) });
|
||||
}
|
||||
```
|
||||
313
docs/src/content/docs/en/modules/transaction/operations.md
Normal file
313
docs/src/content/docs/en/modules/transaction/operations.md
Normal 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,
|
||||
}));
|
||||
```
|
||||
215
docs/src/content/docs/en/modules/transaction/storage.md
Normal file
215
docs/src/content/docs/en/modules/transaction/storage.md
Normal 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
|
||||
}
|
||||
```
|
||||
@@ -35,6 +35,7 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中
|
||||
| 模块 | 包名 | 描述 |
|
||||
|------|------|------|
|
||||
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
|
||||
| [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 |
|
||||
|
||||
## 安装
|
||||
|
||||
|
||||
261
docs/src/content/docs/modules/transaction/core.md
Normal file
261
docs/src/content/docs/modules/transaction/core.md
Normal 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}`);
|
||||
}
|
||||
```
|
||||
355
docs/src/content/docs/modules/transaction/distributed.md
Normal file
355
docs/src/content/docs/modules/transaction/distributed.md
Normal 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 });
|
||||
}
|
||||
```
|
||||
238
docs/src/content/docs/modules/transaction/index.md
Normal file
238
docs/src/content/docs/modules/transaction/index.md
Normal 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) });
|
||||
}
|
||||
```
|
||||
313
docs/src/content/docs/modules/transaction/operations.md
Normal file
313
docs/src/content/docs/modules/transaction/operations.md
Normal 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,
|
||||
}));
|
||||
```
|
||||
215
docs/src/content/docs/modules/transaction/storage.md
Normal file
215
docs/src/content/docs/modules/transaction/storage.md
Normal 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> {
|
||||
// 保存事务日志
|
||||
}
|
||||
|
||||
// ... 实现其他方法
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user