在高并发环境下,即使两个请求逻辑看起来「正确」,也可能因为同时操作相同的数据,导致写入被覆盖或丢失。本文以 NestJS + TypeORM 为例,模拟并发转账操作,展示未加锁情况下的“写覆盖”问题,并讲解如何通过悲观锁与乐观锁来保障数据一致性。
@Entity(custom_account')
export class CustromEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ default: 0 })
balance: number;
// 可选:用于乐观锁演示
// @VersionColumn()
// version: number;
}
我们定义两个接口:一个不加锁(unsafeTransfernoLock),一个加悲观锁(transferWithPessimisticLock):
async unsafeTransfernoLock(fromId: number, toId: number, amount: number): Promise<void> {
console.log(`[START] unsafeTransfernoLock`);
const from = await this.custromRepository.findOneByOrFail({ id: fromId });
console.log(`[读取 FROM] id=${from.id}, balance=${from.balance}`);
if (from.balance < amount) throw new Error('Insufficient funds');
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟处理延迟
from.balance -= amount;
await this.custromRepository.save(from);
console.log(`[扣款后 FROM] balance=${from.balance}`);
const to = await this.custromRepository.findOneByOrFail({ id: toId });
console.log(`[读取 TO] id=${to.id}, balance=${to.balance}`);
to.balance += amount;
await this.custromRepository.save(to);
console.log(`[加款后 TO] balance=${to.balance}`);
console.log(`[END] unsafeTransfernoLock`);
}
Promise.all([
fetch('http://localhost:3000/tools/custrom/lock/a'),
fetch('http://localhost:3000/tools/custrom/lock/a')
]);
[请求 A 和 B 同时开始]
读取 FROM balance=100
读取 FROM balance=100
...
请求 A 扣款写入 balance=50
请求 B 扣款写入 balance=50(覆盖了 A)
最终结果:TO = 200,FROM = 50(转账了两次但只扣了一次)
(图片)
async transferWithPessimisticLock(fromId: number, toId: number, amount: number): Promise<void> {
await this.custromRepository.manager.transaction(async manager => {
const from = await manager.findOne(CustromEntity, {
where: { id: fromId },
lock: { mode: 'pessimistic_write' },
});
if (from.balance < amount) throw new Error('Insufficient funds');
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟处理延迟
from.balance -= amount;
await manager.save(from);
const to = await manager.findOne(CustromEntity, {
where: { id: toId },
lock: { mode: 'pessimistic_write' },
});
to.balance += amount;
await manager.save(to);
});
}
当你同时请求 /lock/b
两次时,第一个请求完成前,第二个会被数据库锁住,直到锁释放。这避免了并发写入覆盖的问题。
(效果图)
@VersionColumn()
version: number;
使用 @VersionColumn()
后,TypeORM 会在 save()
时校验记录版本号是否匹配。如果在你读取数据和写入之间,别人已经修改过这条记录,就会抛出错误,避免覆盖写入。
适合场景:
锁类型 | 特点 | 推荐场景 |
---|---|---|
❌ 无锁 | 快,但极易出现并发冲突 | 仅用于只读操作或测试场景 |
🔒 悲观锁 | 强一致性,阻塞并发,适合核心业务 | 金融、库存、事务强一致场景 |
🔁 乐观锁 | 性能好,检测冲突后失败,可结合重试机制 | 写冲突少的业务,例如订单更新 |
📦 modules/
┣ 📂 custrom/
┃ ┣ 📜 custrom.entity.ts
┃ ┣ 📜 custrom.service.ts
┃ ┣ 📜 custrom.controller.ts
┃ ┣ 📜 constant.ts
┃ ┗ 📜 ...
希望这篇文章能帮你深入理解并发下的数据一致性问题,并选出适合你业务场景的锁机制解决方案 🙌
文章标题:TypeORM 实战:并发下的数据一致性问题与锁机制详解
文章作者:Cling.
文章链接:[复制]
最后修改时间:2025年 08月 02日 15时08分
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。 本文采用CC BY-NC-SA 4.0进行许可。
Copyright © 2023-2025
豫ICP备2022014268号-1
「每想拥抱你一次,天空飘落一片雪,至此雪花拥抱撒哈拉!」
本站已经艰难运行了661天