TypeORM使用setLock()实现PostgreSQL行锁的正确方案及测试问题咨询
问题核心原因
- 你对
useTransaction(true)的使用不符合预期:该配置仅作用于当前QueryBuilder对应的单条SQL,执行完成后会立即自动提交事务。你加行锁的SELECT语句执行完就触发了COMMIT,后续的UPDATE完全不在事务范围内,行锁在UPDATE执行前就已经释放,和你观察到的「COMMIT在UPDATE之前执行」的现象完全吻合,所以锁逻辑完全不生效。 - 你的两个服务实例的测试逻辑本身没有问题,但因为锁逻辑本身写错了,所以看不到预期的阻塞效果。
正确实现方案
必须保证「加锁查询」和「更新操作」在同一个数据库事务内运行,行锁才会在事务全程生效,直到事务提交/回滚后才释放。修正后生成的SQL顺序应该是START TRANSACTION -> SELECT ... FOR UPDATE -> UPDATE ... -> COMMIT,符合行锁生效要求。
方案1:手动控制事务(无额外依赖,逻辑直观)
注入DataSource实例,用其transaction方法包裹整个业务逻辑:
import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './user.entity'; @Injectable() export class UserService { constructor( @InjectRepository(User) private userRepo: Repository<User>, private dataSource: DataSource, ) {} async testFun(): Promise<any> { // 整个逻辑包裹在同一个事务中 return this.dataSource.transaction(async (transactionalEntityManager) => { // 用事务管理器创建查询,保证所有操作在同一个事务里 const user = await transactionalEntityManager .createQueryBuilder(User, "user") .setLock("pessimistic_write") .where("id = :id", { id: 2 }) .getOne(); // 测试用延迟代码可以放在这里 // function sleep(ms) { // return new Promise(resolve => setTimeout(resolve, ms)); // } // await sleep(20000); const updateResult = await transactionalEntityManager .createQueryBuilder() .update(User) .set({ fullname: "frm3000" }) .where("id = :id", { id: 2 }) .execute(); return updateResult; }); } }
方案2:声明式事务装饰器(代码更简洁)
先安装typeorm-transactional包,在项目启动类添加@EnableTransactionManagement()注解后,直接用装饰器修饰方法即可自动包裹事务:
import { Transactional } from 'typeorm-transactional'; import { Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './user.entity'; @Injectable() export class UserService { constructor( @InjectRepository(User) private userRepo: Repository<User>, ) {} @Transactional() // 整个方法会自动运行在同一个事务中 async testFun(): Promise<any> { const user = await this.userRepo .createQueryBuilder("user") .setLock("pessimistic_write") .where("id = :id", { id: 2 }) .getOne(); // 测试用延迟代码放这里 // await sleep(20000); const updateResult = await this.userRepo .createQueryBuilder('test') .update() .set({ fullname: "frm3000" }) .where("id = :id", { id: 2 }) .execute(); return updateResult; } }
正确测试方法
- 按上述方案修正代码,保留3001端口服务在加锁查询后的20秒延迟逻辑
- 先触发3001端口的
testFun接口,10秒内再触发3000端口的testFun接口 - 预期结果:3000端口的接口会阻塞直到3001的20秒延迟走完、事务提交后才会执行完成,最终数据库的fullname值为3000端口设置的
frm3000(因为后执行更新)。如果锁不生效,3000接口会立刻返回,最终值为3001设置的frm3001。
内容的提问来源于stack exchange,提问作者Joseph
相关产品推荐
相关产品推荐

