You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
  }
}
正确测试方法
  1. 按上述方案修正代码,保留3001端口服务在加锁查询后的20秒延迟逻辑
  2. 先触发3001端口的testFun接口,10秒内再触发3000端口的testFun接口
  3. 预期结果:3000端口的接口会阻塞直到3001的20秒延迟走完、事务提交后才会执行完成,最终数据库的fullname值为3000端口设置的frm3000(因为后执行更新)。如果锁不生效,3000接口会立刻返回,最终值为3001设置的frm3001。

内容的提问来源于stack exchange,提问作者Joseph

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 00:27:04