ABP UnitOfWork中RepeatableRead级别下数据库记录锁定及增量异常问题
问题描述
我有一张名为VoucherNumber的数据库表,包含int类型的TotalNumberRecord列,初始仅一条记录且TotalNumberRecord值为0。在VoucherNumberAppService.cs中我编写了两个方法:
public async void TestIncrementA(string id) { using var unitOfWork = _unitOfWorkManager.Begin(isolationLevel: System.Data.IsolationLevel.RepeatableRead); var query = await _voucherNumberService.GetQueryableAsync(); query = query.Where(p => p.Id == id); var sections = await AsyncExecuter.FirstOrDefaultAsync(query); sections.TotalNumberRecord++; await Task.Delay(5000); await _voucherNumberService.UpdateAsync(sections); await unitOfWork.CompleteAsync(); }
public async void TestIncrementB(string id) { using var unitOfWork = _unitOfWorkManager.Begin(isolationLevel: System.Data.IsolationLevel.RepeatableRead); var query = await _voucherNumberService.GetQueryableAsync(); query = query.Where(p => p.Id == id); var sections = await AsyncExecuter.FirstOrDefaultAsync(query); sections.TotalNumberRecord++; await _voucherNumberService.UpdateAsync(sections); await unitOfWork.CompleteAsync(); }
两个方法均用于将TotalNumberRecord值加1,仅TestIncrementA方法包含5秒延迟。当我执行TestIncrementA(id);TestIncrementB(id);后,预期TotalNumberRecord值为2,但实际仅为1,请问我忽略了什么?
问题原因与解决方案
核心原因
async void误用:async void是为事件处理器设计的语法,调用时不会等待方法执行完成。TestIncrementA(id);TestIncrementB(id);会同时启动两个异步操作,完全没有执行顺序的保证。- 并发脏写:两个方法都先读取了数据库中
TotalNumberRecord的初始值0,各自在内存中加1变成1,最后更新时都把1写入数据库,互相覆盖了修改。RepeatableRead隔离级别只能防止脏读和不可重复读,无法阻止这种基于旧值更新的脏写场景。 - 延迟放大冲突:
TestIncrementA的5秒延迟让两个方法的读操作几乎同时完成,都拿到初始值0,最终导致覆盖问题暴露得更明显。
解决方案
- 修正异步方法签名:将
async void改为async Task,调用时通过await保证顺序执行,或者用并发方式配合锁机制:public async Task TestIncrementA(string id) { /* 原逻辑保留 */ } public async Task TestIncrementB(string id) { /* 原逻辑保留 */ } // 调用时保证顺序 await TestIncrementA(id); await TestIncrementB(id); - 使用数据库原子更新:跳过先读再改的逻辑,直接在数据库层面执行增量更新,从根源避免并发冲突:
// 示例:使用ORM的批量更新API直接操作数据库 await _voucherNumberService.ExecuteUpdateAsync( predicate: p => p.Id == id, updateExpression: p => p.SetProperty(x => x.TotalNumberRecord, x => x.TotalNumberRecord + 1) ); - 添加并发控制机制:如果必须先读再改,可选择两种方式:
- 升级隔离级别为
Serializable:会给数据加范围锁,阻止其他事务读取该数据,但会大幅降低并发性能; - 乐观锁:给表添加
RowVersion(或Timestamp)列,更新时带上版本号,当版本不匹配时抛出冲突异常,再重试更新逻辑。
- 升级隔离级别为
内容的提问来源于stack exchange,提问作者Anh Đinh
相关产品推荐
相关产品推荐

