前言
在 NestJS 微服务项目中,事务管理一直是个痛点。当业务逻辑跨越多个 Service 方法时,传统的 Prisma 事务写法会让代码变得臃肿,而且事务上下文难以传递。
本文分享一种基于装饰器的事务管理方案,实现一行代码自动开启事务,并且支持跨方法、跨 Service 的事务上下文传递。
传统事务写法的痛点
在 Prisma 中开启事务通常这样写:
this.prisma.$transaction(tx => {
// 通过 tx 传递使用事务
tx.user.update(...);
// 嵌套事务无法直接传递
tx.$transaction(...); // ❌ 不支持
});
当业务跨方法时,问题更明显:
class TestService {
@Inject(PrismaService)
private prisma: PrismaService;
updateUser() {
this.prisma.$transaction(tx => {
// 用户相关操作
tx.user.update(...);
tx.user.create(...);
// 调用其他方法,事务无法传递
this.updateOrder(); // ❌ 需要重新开启事务
});
}
updateOrder() {
// 这里必须重新开启事务,无法复用外层事务
this.prisma.$transaction(tx => {
tx.order.update(...);
});
}
}
核心问题:
- 事务上下文无法跨方法传递
- 重复的
$transaction调用,代码冗余 - 业务代码与事务管理耦合严重
期望的效果
我希望实现这样的写法:
class TestService {
@Inject(PrismaService)
private prisma: PrismaService;
@Transactional() // ✅ 一行装饰器开启事务
async updateUser() {
// 直接使用 prisma,无需手动开启事务
await this.prisma.user.update(...);
await this.prisma.user.create(...);
// 跨方法调用,事务自动传递
await this.updateOrder(); // ✅ 共享同一事务
}
async updateOrder() {
// 直接使用 prisma,自动复用外层事务
await this.prisma.order.update(...);
}
}
实现方案
技术选型
市面上已有 nestjs-cls + @nestjs-cls/transactional-adapter-prisma 方案,但为了深入理解原理,我选择自己实现。
核心技术:
AsyncLocalStorage:Node.js 原生 API,实现异步上下文隔离- 装饰器 + 元数据:标记需要事务的方法
Proxy:动态代理方法调用
架构设计
┌─────────────────────────────────────────────────────────────┐
│ 调用流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ @Transactional() │
│ ↓ │
│ TransactionalInjector(模块初始化时代理方法) │
│ ↓ │
│ runInTransaction(开启事务,存储到 AsyncLocalStorage) │
│ ↓ │
│ 业务方法执行 │
│ ↓ │
│ PrismaService.current(从上下文获取事务实例) │
│ ↓ │
│ 事务提交/回滚 │
│ │
└─────────────────────────────────────────────────────────────┘
核心实现
1. 事务上下文管理
使用 AsyncLocalStorage 存储事务实例,这是整个方案的核心:
// transaction/transaction-context.ts
import { AsyncLocalStorage } from 'node:async_hooks';
import { PrismaClient, Prisma } from '@prisma/client';
type PrismaTx = Prisma.TransactionClient; // Prisma 事务客户端类型
// 事务配置选项
export type TransactionOptions = {
maxWait?: number; // 最大等待时间
timeout?: number; // 超时时间
isolationLevel?: Prisma.TransactionIsolationLevel; // 隔离级别
};
// 创建异步上下文存储,保存事务实例
const storage = new AsyncLocalStorage<{ tx: PrismaTx }>();
/**
* 在事务上下文中执行代码
* @param prisma PrismaClient 实例
* @param fn 要执行的业务逻辑
* @param options 事务配置
*/
export async function runInTransaction<T>(
prisma: PrismaClient,
fn: () => Promise<T>,
options?: TransactionOptions,
): Promise<T> {
return prisma.$transaction(async (tx) => {
// 将事务实例存入上下文,fn 中可通过 getCurrentTx 获取
return storage.run({ tx }, fn);
}, options);
}
/**
* 获取当前事务上下文中的事务实例
* @returns 事务实例,如果不在事务中则返回 null
*/
export function getCurrentTx(): PrismaTx | null {
const store = storage.getStore();
return store?.tx ?? null;
}
原理说明:
AsyncLocalStorage类似 Java 的ThreadLocal,但适用于 Node.js 的异步环境storage.run({ tx }, fn)会将{ tx }绑定到fn的整个异步调用链- 在
fn内部的任何异步调用中,都能通过storage.getStore()获取到事务实例
2. 装饰器定义
使用 NestJS 的元数据机制标记需要事务的方法:
// transaction/transactional.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { TransactionOptions } from './transaction-context';
export const TRANSACTIONAL_KEY = 'transactional';
/**
* @Transactional 装饰器
* 标记方法需要在事务中执行
*
* @example
* @Transactional() // 默认配置
* @Transactional({ timeout: 5000 }) // 自定义超时
*/
export const Transactional = (options: TransactionOptions = {}) =>
SetMetadata(TRANSACTIONAL_KEY, options);
3. 事务注入器(核心)
这是最关键的部分。传统方案使用 Interceptor 拦截 HTTP 请求,但事务不应该依赖 HTTP 上下文。这里采用 Module Init + Proxy 方式,在模块初始化时扫描并代理所有带装饰器的方法。
这种实现方式学习自开源项目 AiToEarn
// transaction/transactional.injector.ts
import type { Injectable } from '@nestjs/common/interfaces';
import type { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Injectable as InjectableDec, Logger, OnModuleInit } from '@nestjs/common';
import { MetadataScanner, ModulesContainer } from '@nestjs/core';
import { runInTransaction, TransactionOptions } from './transaction-context';
import { TRANSACTIONAL_KEY } from './transactional.decorator';
import { PrismaService } from '../prisma.service';
/**
* 事务注入器
* 在模块初始化时扫描所有带 @Transactional 装饰器的方法,
* 并为这些方法注入事务处理逻辑
*/
@InjectableDec()
export class TransactionalInjector implements OnModuleInit {
private readonly logger = new Logger(TransactionalInjector.name);
private readonly metadataScanner: MetadataScanner = new MetadataScanner();
constructor(
private readonly prisma: PrismaService,
private readonly modulesContainer: ModulesContainer,
) {}
async onModuleInit() {
// 扫描所有 Provider,注入事务逻辑
for (const provider of this.getProviders()) {
this.injectToProvider(provider);
}
}
/**
* 获取所有 Provider
*/
private* getProviders(): Generator<InstanceWrapper<Injectable>> {
for (const module of this.modulesContainer.values()) {
for (const provider of module.providers.values()) {
if (provider && provider.metatype?.prototype) {
yield provider as InstanceWrapper<Injectable>;
}
}
}
}
/**
* 为 Provider 注入事务逻辑
*/
private injectToProvider(wrapper: InstanceWrapper<Injectable>): void {
const { metatype } = wrapper;
if (!metatype) return;
const prototype = metatype.prototype;
const methodNames = this.metadataScanner.getAllMethodNames(prototype);
for (const methodName of methodNames) {
const method = prototype[methodName];
if (this.isDecorated(method)) {
const options = this.getDecoratorOptions(method);
const wrappedMethod = this.wrapMethod(method, methodName, prototype.constructor.name, options);
this.reDecorate(method, wrappedMethod);
prototype[methodName] = wrappedMethod;
this.logger.log(`Injected transaction to ${prototype.constructor.name}.${methodName}`);
}
}
}
private isDecorated(target: object): boolean {
return Reflect.hasMetadata(TRANSACTIONAL_KEY, target);
}
private getDecoratorOptions(target: object): TransactionOptions {
return Reflect.getMetadata(TRANSACTIONAL_KEY, target);
}
/**
* 保留原有装饰器
*/
private reDecorate(source: object, destination: object): void {
const keys = Reflect.getMetadataKeys(source);
for (const key of keys) {
const meta = Reflect.getMetadata(key, source);
Reflect.defineMetadata(key, meta, destination);
}
}
/**
* 使用 Proxy 包装方法,注入事务逻辑
*/
private wrapMethod(
originalMethod: (...args: unknown[]) => unknown,
methodName: string,
className: string,
options: TransactionOptions,
): (...args: unknown[]) => unknown {
return new Proxy(originalMethod, {
apply: async (target, thisArg, args: unknown[]) => {
this.logger.debug(`Executing transactional method: ${className}.${methodName}`);
return runInTransaction(this.prisma, async () => {
return Reflect.apply(target, thisArg, args) as Promise<unknown>;
}, options);
},
});
}
}
实现亮点:
ModulesContainer获取 NestJS 容器中的所有模块MetadataScanner扫描类的所有方法Proxy代理方法调用,自动包裹事务逻辑- 支持定时任务、消息队列等非 HTTP 场景
4. 修改 PrismaService
让 this.prisma 自动感知事务上下文:
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { getCurrentTx } from './transaction/transaction-context';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
/**
* 获取当前 PrismaClient 实例
* 如果在事务中,返回事务实例;否则返回普通实例
*/
get current() {
const tx = getCurrentTx();
if (!tx) return this;
// 使用 Proxy 让调用自动转发到事务实例
return new Proxy(this, {
get(target, prop: string | symbol, receiver) {
// 如果事务实例有该属性/方法,优先使用
if (prop in tx) {
const value = (tx as any)[prop];
if (typeof value === 'function') {
return value.bind(tx);
}
return value;
}
// 否则回退到原始 PrismaClient(如 $connect、$on 等)
return Reflect.get(target, prop, receiver);
},
}) as unknown as Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'>;
}
}
使用方式:
// 在 Service 中
this.prisma.user.findMany(); // 自动使用事务实例(如果存在)
注意:直接使用
this.prisma调用即可,currentgetter 会自动返回正确的事务实例或普通实例。
5. 模块注册
// prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { TransactionalInjector } from './transaction/transactional.injector';
@Global()
@Module({
providers: [PrismaService, TransactionalInjector],
exports: [PrismaService],
})
export class PrismaModule {}
使用示例
@Injectable()
export class UserService {
constructor(
private readonly prisma: PrismaService,
private readonly orderService: OrderService,
) {}
@Transactional({ timeout: 5000 }) // 5 秒超时
async purchaseProduct(userId: number, productId: number, amount: number) {
// 1. 扣减用户积分
await this.prisma.user.update({
where: { id: userId },
data: { integral: { decrement: amount } },
});
// 2. 创建订单(跨 Service,共享事务)
await this.orderService.createOrder(userId, productId, amount);
// 3. 扣减库存
await this.prisma.product.update({
where: { id: productId },
data: { stock: { decrement: 1 } },
});
// 如果任何一步失败,整个事务自动回滚
}
}
@Injectable()
export class OrderService {
constructor(private readonly prisma: PrismaService) {}
async createOrder(userId: number, productId: number, amount: number) {
// 无需 @Transactional,自动复用外层事务
return this.prisma.order.create({
data: { userId, productId, amount },
});
}
}
验证事务生效
通过设置极短的超时时间验证:
@Transactional({
maxWait: 10, // 最大等待 10ms
timeout: 10, // 执行超时 10ms
})
async test() {
await this.prisma.user.findMany();
}
看到 Prisma 事务超时错误,说明事务已正确开启。
方案对比
| 特性 | 传统 $transaction | Interceptor 方案 | Injector 方案(本文) |
|---|---|---|---|
| 跨方法传递 | ❌ | ❌ | ✅ |
| 非 HTTP 场景 | ❌ | ❌ | ✅ |
| 代码侵入性 | 高 | 中 | 低 |
| 学习成本 | 低 | 中 | 中 |
| 灵活性 | 低 | 中 | 高 |
总结
本文实现了基于装饰器的 Prisma 事务管理方案,核心思路:
- AsyncLocalStorage 实现事务上下文的异步传递
- 装饰器 + 元数据 标记需要事务的方法
- Module Init + Proxy 在模块初始化时代理方法,注入事务逻辑
- PrismaService.current 自动感知事务上下文
这样实现了:
- ✅ 一行装饰器开启事务
- ✅ 跨方法、跨 Service 事务传递
- ✅ 支持定时任务、消息队列等非 HTTP 场景
- ✅ 业务代码与事务管理解耦