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

基于TypeORM与NodeJS实现悲观读写数据库锁解决余额更新竞态问题

悲观锁不生效问题原因及修复方案

核心问题排查

  • 异步操作未加await导致事务提前提交:代码中所有查询、更新操作均为异步Promise任务,未等待执行完成就调用了queryRunner.commitTransaction(),事务在加锁逻辑执行前就已结束,锁机制自然无法生效。
  • 同事务内查询不受悲观锁限制:悲观锁的作用是阻塞其他事务对同一行数据的操作,你在同一个事务内并行发起多次加锁查询,本身就不会互相等待,模拟并发场景需要在独立的事务/独立请求中触发操作。
  • 更新语句语法错误:update查询中你给表设置的别名是balanceX,但where条件写的是balances.id,会导致更新匹配失败。
  • 应用层计算余额存在冗余风险:即使加锁成功,直接在应用层读取余额计算后再写回,不如数据库原子操作稳妥。

修复后代码示例

require("dotenv").config();
const environment = process.env;

module.exports = async function UserAccount(connection, Entities, addAmount) {
    console.log("==== STARTING =====");
    const queryRunner = connection.createQueryRunner();
    await queryRunner.connect();
    try {
        await queryRunner.startTransaction();
        // 加悲观写锁查询,等待其他事务锁释放后才会返回结果
        const balanceRecord = await queryRunner.manager
            .getRepository(Entities.Balances)
            .createQueryBuilder("balances")
            .useTransaction(true)
            .select()
            .setLock("pessimistic_write")
            .where("balances.id = :id", { id: 1 })
            .getOne();

        console.log("读取到的余额为:", balanceRecord.balance);
        // 方式1:应用层计算后更新(加锁后可以保证安全)
        const newBalance = Number(balanceRecord.balance) + addAmount;
        await queryRunner.manager
            .getRepository(Entities.Balances)
            .createQueryBuilder()
            .update()
            .set({ balance: newBalance })
            .where("id = :id", { id: 1 })
            .execute();
        
        // 方式2:更稳妥的数据库原子操作,不需要应用层计算,甚至可以不用单独加读锁
        /* await queryRunner.manager
            .getRepository(Entities.Balances)
            .createQueryBuilder()
            .update()
            .set({ balance: () => `balance + ${addAmount}` })
            .where("id = :id", { id: 1 })
            .execute(); */

        await queryRunner.commitTransaction();
        console.log("更新后余额为:", newBalance);
    } catch (err) {
        await queryRunner.rollbackTransaction();
        throw err;
    } finally {
        await queryRunner.release();
    }
};

并发测试方法

要验证锁是否生效,可同时触发多次该函数调用,比如:

// 模拟10个并发请求,每个请求给余额加1
Promise.all(new Array(10).fill(1).map(amount => UserAccount(connection, Entities, amount)))

如果锁生效,最终余额会比初始值多10,不会出现少加的情况。

可选优化方案

如果业务并发量不是极高,也可以使用乐观锁方案:

  1. 给balances表增加version int类型字段,默认值为1
  2. 更新时携带version条件:update balances set balance = balance + 1, version = version +1 where id =1 and version = 读取到的version
  3. 如果更新影响行数为0,说明数据已被其他请求修改,重试操作即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 10:48:01