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

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,请问我忽略了什么?


问题原因与解决方案

核心原因

  1. async void误用:async void是为事件处理器设计的语法,调用时不会等待方法执行完成。TestIncrementA(id);TestIncrementB(id);会同时启动两个异步操作,完全没有执行顺序的保证。
  2. 并发脏写:两个方法都先读取了数据库中TotalNumberRecord的初始值0,各自在内存中加1变成1,最后更新时都把1写入数据库,互相覆盖了修改。RepeatableRead隔离级别只能防止脏读和不可重复读,无法阻止这种基于旧值更新的脏写场景。
  3. 延迟放大冲突:TestIncrementA的5秒延迟让两个方法的读操作几乎同时完成,都拿到初始值0,最终导致覆盖问题暴露得更明显。

解决方案

  1. 修正异步方法签名:将async void改为async Task,调用时通过await保证顺序执行,或者用并发方式配合锁机制:
    public async Task TestIncrementA(string id) { /* 原逻辑保留 */ }
    public async Task TestIncrementB(string id) { /* 原逻辑保留 */ }
    // 调用时保证顺序
    await TestIncrementA(id);
    await TestIncrementB(id);
    
  2. 使用数据库原子更新:跳过先读再改的逻辑,直接在数据库层面执行增量更新,从根源避免并发冲突:
    // 示例:使用ORM的批量更新API直接操作数据库
    await _voucherNumberService.ExecuteUpdateAsync(
        predicate: p => p.Id == id,
        updateExpression: p => p.SetProperty(x => x.TotalNumberRecord, x => x.TotalNumberRecord + 1)
    );
    
  3. 添加并发控制机制:如果必须先读再改,可选择两种方式:
    • 升级隔离级别为Serializable:会给数据加范围锁,阻止其他事务读取该数据,但会大幅降低并发性能;
    • 乐观锁:给表添加RowVersion(或Timestamp)列,更新时带上版本号,当版本不匹配时抛出冲突异常,再重试更新逻辑。

内容的提问来源于stack exchange,提问作者Anh Đinh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 01:05:27