Asp.net core 学习笔记 ( ef core transaction scope & change level )

更新 2021-12-31

今天遇到一个 bug, 2 个 services,  A 和 B

A 调用 B, 算是一个嵌套. A 无论如何都会执行, 哪怕 B fail 了.

B 在 save change 的时候 fail 了, 我就执行 rollback to savepoint. 然后交回给 A, A 继续执行然后 save changes. 结果又 fail 了.

才发现, 是因为 B 的 state 依然在. 没有被 rollback 掉.

原来我之前搞错了. 之前说, 当 ef core rollback to savepoint 也会把 state 给 rollback.. 天真...

DbContextTransaction should also rollback dbcontext

ef 认为 1 个 context 代表一个 unit of work. 

开启一个 context 之后做一堆的修修改改. 最终 save changes

所有的 changes record 就清空了. 这个 unit of work 就算是完成一个小任务了.

至于刚才的 save changes 有没有永久保存数据库, 那个是 transaction 负责的. 要看 commit/rollback.

所以 transaction 是 transaction, context 是 context. 没有直接关系. 请分开看待. 这也能比较好理解, 多个 context share 1 transaction 是怎么回事儿了. 

回到上面的例子, 2 个思路, 第 1 个 A,B 公用一个 context 是否合理?, 第 2 个, save change 失败后要如何处理 state ?

for 我的案子的话, 分开会好一些. save change 失败一般上都不太会管 state, 大部分情况就是 throw error 出去. 

 

更新: 2021-07-03

.net 5 之后 ef core 有了 save point 的功能 

https://docs.microsoft.com/en-gb/ef/core/saving/transactions#savepoints

sql server 是有这个概念的, 可以在 rollback 的时候选一个 save point. 这个也是模拟 nested transaction scope 的做法. 

之前我是用 system.transaction 来处理 nested transaction 的 (虽然 ef 并没有推荐). 而现在我觉得如果可以的话尽量用 ef 的 transaction 会更好.

好处 1 就是比较贴近 sql server 的方式,

好处 2 就是 system transaction 有些局限, 比如它不可能 rollback entity 的 state, 这导致了它只能要嘛全部 commit 要嘛全不 commit. 虽然说可以搞 new scope 但是如果要求是 child rollback, parent 要 commit, parent commit child 才可以 commit 就做不到了

好处 3 就是 ef 推荐.

坏处就是比较写起来比较麻烦, 需要判断是不是 root transaction, 要做 save point 等. 但是这些可以通过 wrap transaction 封装一下就可以解决的了. 

说说 save point. 它很厉害哦, 当 rollback to savepoint 的时候, entity state 是会改变的. 比如本来是 added, rollback 以后是 unchange 

附上一个 example : 

[HttpPost("CreateUserByPassword")]
public async Task<ActionResult> CreateUserByPasswordAsync([FromBody] CreateUser1Dto dto)
{
    using var transaction = await _db.Database.BeginTransactionAsync();
    try
    {
        await CreateCountryAsync();
    }
    catch
    {


    }
    try
    {
        var user = new User
        {
            UserName = "keatkeat90",
            Type = UserType.Internal
        };
        var result = await _userManager.CreateAsync(user, "keatkeat90");
        await transaction.CommitAsync();
    }
    catch (Exception ex)
    {

    }

    return Ok();
}

public async Task CreateCountryAsync()
{
    var isRootTransaction = _db.Database.CurrentTransaction == null;
    IDbContextTransaction transaction = isRootTransaction ? await _db.Database.BeginTransactionAsync() : _db.Database.CurrentTransaction!;
    var country = new Country
    {
        CountryName = "Philippines"
    };
    if (!isRootTransaction)
    {
        await transaction.CreateSavepointAsync("CreateCountrySavePoint");
    }
    try
    {
        _db.Countries.Add(country);
        var state = _db.Entry(country).State; // Added
        await _db.SaveChangesAsync();
        throw new Exception("error");
        if (isRootTransaction)
        {
            await transaction.CommitAsync();
        }
    }
    catch (Exception ex)
    {
        if (isRootTransaction)
        {
            await transaction.RollbackAsync();
        }
        else
        {
            await transaction.RollbackToSavepointAsync("CreateCountrySavePoint");
            var state = _db.Entry(country).State; // UnChanged
        }
        throw ex;
    }
    finally
    {
        if (isRootTransaction)
        {
            transaction.Dispose();
        }
    }
}
View Code

 

 

 

更新: 2021-06-21

nested TransactionScope != nested transactions

https://github.com/dotnet/efcore/issues/6233#issuecomment-242693262

好比如 sql server 并不支持 nested transactions 是一样的道理 (oracle 就真的支持),它只是有类似的方案而已. 

 

更新: 2020-02-21 

补上一些忧虑

public async Task Inside()
{
    using var scope = SqlCommonMethod.CreateTransactionScope();
    await SqlCommonMethod.SetIsolationLevel(Db, IsolationLevel.Serializable); // 开启高级别锁
    var countries = await Db.Countries.ToListAsync(); // counties 表被高级锁了
    await SqlCommonMethod.SetIsolationLevel(Db, IsolationLevel.ReadCommitted); // 设置回普通锁, 不然接下来都会一直是高级锁
    await Inside2();
    scope.Complete(); // 这里的 complete 结束并不会解锁, 会一直等到最外面的 scope 被释放, 符合逻辑
}

public async Task Inside2()
{
    using var scope = SqlCommonMethod.CreateTransactionScope();
    var countries = await Db.States.ToListAsync(); // // states 表被普通锁了
    scope.Complete();
}

#region Simple test 
[HttpPost("simple-test")]
public async Task<IActionResult> SimpleTest()
{
    using (var scope = SqlCommonMethod.CreateTransactionScope())
    {
        await Inside();
        scope.Complete(); // 这里依然不会解锁, 要等到 scope 释放
    } 
    // 直到 using 结束才会解锁 table 哦
    return Ok("ok");
}

总结 : 每次设置级别后最好是设置回去. 不然全场都会使用高级锁. 

 

ef core 有 unit of work 的概念,当我们 save change 时会自动使用 transaction 确保更新的一致性. 隔离级别是默认的 read committed 不允许脏读. 

但是呢, 有时候我们希望拥有更好的隔离级别, 比如 repeatable read, serializable 

那么就需要调用 database.beginTransaction 了. 

一旦需要自己控制 trans 麻烦就跟着来了。

比如在多个服务嵌套调用时, 如何共享 trans 呢 ? 

每个服务的 trans 级别也有可能是不同的.

如果我们单纯使用 beginTransaction 那么需要在每个服务写判断,是否有 current transaction 做出不同的处理.   

在早期, 我们会用 transaction scope 作为业务级别的事务. 

transaction scope 非常强大, 可以跨库, 分布式, 甚至可以链接 file system 

比如一个事务内做了数据库修改,也创建了 file, 如果事务最终失败,连 file 也可以 rollback 删除掉. 

但自从 ef 出现后, 这个就变得大材小用了些. ef 也不推荐我们使用了 refer https://docs.microsoft.com/zh-cn/ef/ef6/saving/transactions?redirectedfrom=MSDN

 

 

ef core 在 2.1 的时候支持了 transactionscope 但是不支持分布式, 好像是不兼容 linux 所以去掉了.

但是在我说的这种情况下,使用它依然会让代码变得更好.

调用起来是这样的

            using (var scope = new TransactionScope(
                scopeOption: TransactionScopeOption.Required,
                transactionOptions: new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead },
                asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled
            ))
            {
                try
                {
                 
                }
                catch (Exception ex)
                {
                    return BadRequest(ex.Message);
                }
                scope.Complete();
            }

默认隔离级别是 serializable. 

如果想在 service 里面嵌套, 那么重要设定 scopeOption 是 required 就可以了. 

它还有另外 2 个选择, 1 个是 new 意思是另外开启一个独立的 trans, 再一个是 suppend 就是完全没有 trans 无关.

有了这些就很灵活了,在不同 service 中我们可以去实现独立或无关的事务处理. 

使用过程中需要注意几件事情

 

 

嵌套 scope 需要使用同一种级别

这个挺麻烦的,通常不可能全部一个级别吧... 

目前没有看到方法可以修改的,一个可能的办法是直接调用 sql 语句 

set transaction isolation level read committed
set transaction isolation level repeatable read;
set transaction isolation level serializable

去设定它. 

这里顺便说说 sql server 对于这一块的处理. 

https://www.cnblogs.com/keatkeat/p/11830113.html

 

 

另一个要注意的是, 一定要设置 async enabled 如果 scope 内需要 async 的话

asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled

 

refer : 

https://www.cnblogs.com/csdbfans/p/transactionscope.html

https://blog.csdn.net/qin_yu_2010/article/details/86150247

https://docs.microsoft.com/en-us/ef/core/saving/transactions

https://weblogs.thinktecture.com/pawel/2018/06/entity-framework-core-use-transactionscope-with-caution.html

https://www.cnblogs.com/taiyonghai/p/6047849.html

https://www.21cto.com/article/1075

https://www.codeproject.com/Articles/690136/All-About-TransactionScope#hBusinessTrans

https://codewala.net/2018/05/06/transactionscope-a-simple-way-to-handle-transactions-in-net/

https://docs.microsoft.com/zh-cn/dotnet/framework/data/transactions/implementing-an-implicit-transaction-using-transaction-scope

 

posted @ 2019-11-10 16:19  兴杰  阅读(2114)  评论(0编辑  收藏  举报