Skip to content

UnitOfWork 在 Dispose 后仍能创建新事务,导致孤儿事务和连接泄漏 #2226

@Tangtang1997

Description

@Tangtang1997

问题描述及重现代码:

UnitOfWork 赋值给 BaseRepository.UnitOfWork 后执行 Commit + Dispose,Repository 上的引用不会自动清除。此后通过同一个 Repository 执行操作(如 UpdateAsync),内部会调用已 Dispose 的 UoW 的 GetOrBeginTransaction()该方法会从连接池借出一个全新连接并开启新事务。这个新事务永远不会被提交、回滚或释放,造成孤儿事务和永久连接泄漏。

var repo = fsql.GetRepository<MyEntity>();

// 第一步:创建 UoW,绑定到 Repository,提交,释放
using (var uow = fsql.CreateUnitOfWork())
{
    repo.UnitOfWork = uow;
    await repo.InsertAsync(new MyEntity { Id = 1, Name = "test" });
    uow.Commit();
}
// 此时 uow 已 Dispose,但 repo.UnitOfWork 仍然指向它

// 第二步:继续使用同一个 Repository(Scoped 注入场景下很常见)
repo.Attach(new MyEntity { Id = 1 });
await repo.UpdateAsync(new MyEntity { Id = 1, Name = "updated" });
//  内部静默地借出了新连接并执行了 BEGIN TRANSACTION
// 该事务不会被 Commit 或 Rollback

第二步执行后查询数据库:

  • PostgreSQLSELECT * FROM pg_stat_activity WHERE state = 'idle in transaction'; 会看到一个孤儿连接

原因分析

三个问题叠加导致:

1. Dispose() 没有设置 Enable = falseUnitOfWork.cs

public void Dispose()
{
    if (Interlocked.Increment(ref _disposeCounter) != 1) return;
    try { this.Rollback(); }
    finally { GC.SuppressFinalize(this); }
    //  缺少 Enable = false
}

2. GetOrBeginTransaction() 没有检查 _disposeCounterUnitOfWork.cs

public DbTransaction GetOrBeginTransaction(bool isCreate = true)
{
    if (_tran != null) return _tran;
    if (isCreate == false) return null;
    if (!Enable) return null;               // ← 只检查 Enable,此时仍为 true
    // 缺少 if (_disposeCounter > 0) return null;

    _conn = _fsql.Ado.MasterPool.Get();       // 从连接池借出新连接
    _tran = _conn.Value.BeginTransaction();    // 开启新事务
    DebugBeingUsed.TryAdd(this.Id, this);      // 静态字典强引用,阻止 GC 回收
    return _tran;
}

Commit() 之后 _tran_conn 被置空,但 Enable 仍为 true,所以 GetOrBeginTransaction() 认为可以继续创建新事务。

3. RepositoryDbContext.SaveChangesSuccess() 不调用 Commit()RepositoryDbContext.cs

基类 DbContext.SaveChangesSuccess() 会调用 UnitOfWork?.Commit(),但 RepositoryDbContext 覆写了这个方法,只处理 EntityChangeReport,不调用 Commit。因此第二步创建的新事务没有任何代码会去提交它。

此外,DebugBeingUsedstatic ConcurrentDictionary,持有 UoW 的强引用,导致 UoW 及其持有的连接对象永远不会被 GC 回收。

数据库版本

PostgreSQL - 16

安装的Nuget包

3.5.305

.net framework/. net core? 及具体版本

.NET 8

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions