基于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,不会出现少加的情况。
可选优化方案
如果业务并发量不是极高,也可以使用乐观锁方案:
- 给balances表增加
versionint类型字段,默认值为1 - 更新时携带version条件:
update balances set balance = balance + 1, version = version +1 where id =1 and version = 读取到的version - 如果更新影响行数为0,说明数据已被其他请求修改,重试操作即可
内容的提问来源于stack exchange,提问作者Sky Tech
相关产品推荐
相关产品推荐

