ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SqlSugar 事务机制详解:从基础到跨库的完整实践指南

SqlSugar 事务机制详解:从基础到跨库的完整实践指南 1. 为什么 SqlSugar 事务值得单独拿出来讲做过几年 .NET 后端的人应该都有这种感受ORM 框架满地都是EF Core 功能全但太“重”Dapper 轻快但什么都要自己写。SqlSugar 在这两者之间找到了一个很舒服的位置——它不像 EF Core 那样有复杂的变更追踪和状态机也不像 Dapper 那样连基本的 CRUD 都要写 SQL。但真正让我决定在项目里长期用它、并且愿意认真研究它的事务机制的是一次线上事故。那次是一个订单创建接口涉及三张表订单主表、订单明细表、库存扣减记录。那时候项目还在用早期的 SqlSugar 版本我图省事把三个插入操作按顺序写在一个方法里觉得“反正数据库自己有事务出错了会自动回滚”。结果呢库存表插入成功了订单明细表因为一个非空字段没赋值抛了异常。但订单主表的数据已经写进去了。因为那是三个独立的数据库连接操作每个操作默认自动提交根本没有一个统一的事务边界把它们包起来。最后对账的时候发现有一笔“幽灵订单”查了半天才发现是事务边界没控制好。从那以后我养成了一个习惯凡是涉及多表写入的操作第一件事就是先想清楚事务怎么管。这也是我今天想认真聊 SqlSugar 数据事务的原因。它在这方面的能力其实被很多人低估了——SqlSugar 的事务模型比大多数人的认知要完整得多从最基础的BeginTran到TransactionScope托管到异步事务、嵌套事务、跨库事务再到 AOP 切面事务它都有对应的解决方案。这篇文章不是从零开始的教程而是把我实际项目中用到的方法、踩过的坑、以及我对比 gozero 等其他框架后的一些思考整体梳理一遍。如果你已经在用 SqlSugar 做开发但每次写事务都是照着网上抄一段db.Ado.UseTran就完事那这篇文章能帮你把事务这块的知识体系补完整。如果你还在选型阶段看完你会知道 SqlSugar 的事务能力到底覆盖了多少场景能不能扛住你的业务需求。2. 动手之前先搞明白SqlSugar 事务的几种打开方式2.1 默认的“假事务”每个操作独立提交很多初学者对事务有个误解ORM 框架的Insert、Update、Delete方法是不是天然带事务在 SqlSugar 里默认情况下不是。每个单独的方法调用比如db.Insertable(entity).ExecuteCommand()底层就是打开连接、执行 SQL、关闭连接操作完之后自动提交。这跟 ADO.NET 的行为是一致的因为数据库层面的自动提交事务autocommit默认就是开的。如果你在一个方法里写了三个插入操作任何一个失败都不会影响其他两个已经执行完的操作。这个特性单独使用时没问题但一旦你把它当成“每个操作都有事务保护”来理解就会出大事。我见过不止一个同事写出这样的代码public async Task CreateOrder(OrderEntity order, ListOrderItemEntity items) { await db.Insertable(order).ExecuteCommandAsync(); foreach (var item in items) { await db.Insertable(item).ExecuteCommandAsync(); } }表面上看着没问题但items里如果有一条数据不符合表约束——比如明细数量超过了数据库的smallint范围这个Insert会抛异常此时订单主表已经写入成功了。订单创建接口直接 500用户刷新页面再点一次提交又生成一条一模一样的订单。经典的“重复下单”问题根源不是并发控制而是事务边界根本没建立。所以第一步要建立的概念是SqlSugar 的单方法操作是自动提交的多步骤的业务操作必须显式开启事务。这不是 SqlSugar 的缺陷而是所有 ORM 框架的统一行为方式EF Core、Dapper 也不例外。2.2 SqlSugar 官方推荐的三种事务写法SqlSugar 官网文档里事务主要分三个方向我先把它们列出来后面逐一展开db.Ado.UseTran(Action)最简单的同步事务包裹用委托方式把多个操作塞进去自动提交或回滚db.Ado.UseTranT(FuncT)带返回值的事务版本适合需要拿到事务内计算结果的方法db.Ado.BeginTran() / CommitTran() / RollbackTran()手动控制事务生命周期适合事务边界不固定、需要跨方法传递的场景这三种方式的本质都是开启一个数据库事务然后将后续的所有 SqlSugar 操作绑定到这个事务上。不同的是 API 形态和使用场景。UseTran的使用方式本质上是把事务的提交和回滚“托管”给了框架。你只需要在委托里写业务逻辑如果委托里的代码抛出了异常框架会自动调用RollbackTran如果没有异常就自动CommitTran。这大大减少了手写 try-catch 的样板代码。但要注意UseTran的委托内部默认是同步的也就是说你在里面写async方法调用是不行的SqlSugar 为此单独提供了UseTranAsync。这是一个非常容易踩的坑我在项目里见过有人把async方法塞进同步的UseTran里结果事务根本不起作用因为异步方法在await时会把控制权交还线程池委托可能已经执行完了但真正的数据库操作还在路上。这个问题后面详细展开。BeginTran则是更底层的控制方式适合你需要在一个方法里开启事务然后把事务传给另一个方法、在另一个方法里提交或回滚的场景。SqlSugar 的BeginTran底层是调用了 ADO.NET 的DbConnection.BeginTransaction()所以它返回的是一个DbTransaction对象你可以把它传给任何需要的事务感知组件。2.3 来自 gozero 的对照事务插入数据的另一种思路在写这篇文章之前我去查了一下最近社区里比较热的关键词“gozero 通过事务插入数据”。gozero 是国内很流行的 Go 语言微服务框架它的事务处理方式和 SqlSugar 形成了挺有意思的对照。gozero 的事务处理核心是sqlx.Tx结构体它的典型用法是err : l.svcCtx.DB.Transact(func(session sqlx.Session) error { _, err : session.Insert(INSERT INTO orders (id, user_id, amount) VALUES (?, ?, ?), req.OrderId, req.UserId, req.Amount) if err ! nil { return err } _, err session.Insert(INSERT INTO order_items (order_id, sku_id, quantity) VALUES (?, ?, ?), req.OrderId, req.SkuId, req.Quantity) return err })这个模式和 SqlSugar 的UseTran惊人地相似——都是把事务边界通过一个回调函数/委托来确定函数内的所有数据库操作共享同一个事务返回error/抛异常就回滚否则提交。两个框架不约而同地选择了这个模式说明这已经成了现代 ORM/数据访问框架处理业务事务的主流范式。但两者也有明显差异。gozero 的事务模型是基于“会话Session”隔离的——你在Transact回调里拿到的session是事务绑定的连接后续所有操作都必须通过这个session来执行而不是用全局的svcCtx.DB。这种设计非常严格好处是绝对不可能出现“事务外的操作混进事务内”的乌龙。而 SqlSugar 的UseTran相对宽松事务开启后同一个SqlSugarClient实例上的所有操作会自动纳入当前事务不用显式传session。你要说哪个更好我觉得各有利弊。gozero 的方式更安全强制你必须通过事务会话去操作但代码侵入性更强事务内的每个方法都得接收session参数。SqlSugar 的方式更灵活但正因为它宽松你更容易在事务里不小心混入一些不该在事务里的操作——比如不小心用了另一个SqlSugarClient实例或者在同一client上做了一些查询操作结果这些查询也被事务包裹了。理解了 SqlSugar 和 gozero 在事务模型上的差异你再看下面的实操部分会更容易理解为什么要限制事务范围、为什么要在事务结束后立即释放连接这些“潜规则”。3. 核心实操五种 SqlSugar 事务场景的完整代码与选型逻辑3.1 场景一单库多表写入用 UseTran 就够了这是最常见的业务场景一个请求进来需要往同一个数据库的多张表写入数据。比如创建订单写订单主表、订单明细表、订单状态日志表。这三张表往往还在同一个库里甚至同一个 schema 下。我的推荐做法是直接用UseTran因为它最简洁代码可读性也最好var result db.Ado.UseTran(() { var orderId db.Insertable(order).ExecuteReturnSnowflakeId(); foreach (var item in items) { item.OrderId orderId; db.Insertable(item).ExecuteCommand(); } db.Insertable(new OrderLog { OrderId orderId, Action CREATE, CreateTime DateTime.Now }).ExecuteCommand(); });UseTran返回值是一个DbResultbool对象里面有两个关键属性IsSuccess和ErrorException。如果委托内任何一步抛异常IsSuccess会变成falseErrorException会携带原始异常信息。这里有一个细节很多人没注意到UseTran内部默认会做异常捕获它不会把异常重新抛出来而是记录到ErrorException属性里。所以你在外层判断result.IsSuccess后如果要向前端返回错误信息不能只写return 操作失败把result.ErrorException.Message一起带上会方便排查问题。但如果你的业务逻辑需要“判断某一步的操作结果再决定是否继续”委托里就不能直接早期返回比如这样var result db.Ado.UseTran(() { var count db.QueryableInventory().Where(it it.SkuId skuId).Sum(it it.Stock); if (count needCount) { // 这里要回滚怎么办 } });遇到这种分支提前退出的场景我建议在委托里直接抛一个自定义业务异常让框架的异常捕获机制帮你回滚var result db.Ado.UseTran(() { var totalStock db.QueryableInventory() .Where(it it.SkuId skuId) .Select(it it.Stock) .First(); if (totalStock needCount) { throw new BusinessException(库存不足); } // 继续扣除库存、创建订单... });这也是UseTran最方便的地方业务回滚条件一旦不满足直接抛异常事务自动回滚不需要写任何手动回滚代码。同时ErrorException会捕获到BusinessException的实例你可以根据异常类型做不同的错误响应。3.2 场景二需要拿到事务内计算结果用泛型版本有些场景下事务内的操作会产生一些你需要在外层使用的数据比如批量插入后要返回自增主键列表或者事务内要计算出某个汇总值给外部接口用。这时候UseTranT就比UseTran方便得多DbResultListlong result db.Ado.UseTran(() { var idList new Listlong(); foreach (var order in orderBatch) { var id db.Insertable(order).ExecuteReturnBigIdentity(); idList.Add(id); } return idList; }); if (result.IsSuccess) { // 直接使用 result.Data 拿事务内生成的主键列表 Console.WriteLine(生成主键: string.Join(,, result.Data)); }泛型版本内部逻辑和同步版本一致唯一的区别是委托有返回值成功时这个返回值会放在DbResultT.Data属性里。需要提醒的是如果你用ExecuteReturnBigIdentity()获取自增列前提是你的实体主键配置了自增属性在 SqlSugar 中通常用[SugarColumn(IsPrimaryKey true, IsIdentity true)]标记。如果没有标记为自增这个方法返回的是 0而不是真实主键。新人最容易在这里踩坑查半天数据没写进去其实是主键返回值不对数据本身是正常的。另外SqlSugar 8.0 之后还支持了ExecuteReturnSnowflakeId()用雪花算法生成分布式 ID不依赖数据库自增。如果你的项目已经有分库分表的规划建议直接用雪花 ID 而不是数据库自增这样事务内拿到 ID 的逻辑不依赖具体数据库特性将来迁移也方便。3.3 场景三手动控制事务生命周期用 BeginTran CommitTran有些业务的事务边界不是简单地在方法内部比如你有一个服务方法 A 负责校验数据还有一个服务方法 B 负责写库你需要把 A 的校验结果和 B 的写库操作放在一个事务里。这时候用UseTran就不合适了因为UseTran的边界是那个委托你没法把方法 A 和 B 同时塞进一个委托里除非你把 A 的逻辑整个拷进去但那会造成代码重复。我一般在以下场景会选择BeginTranCommitTranRollbackTran手动控制事务需要跨方法、甚至跨类传递事务边界由外部调用方决定而不是由当前方法决定事务内混合了非 SqlSugar 的操作如写文件、调外部 HTTP 接口需要根据整体结果决定提交或回滚基础写法如下try { db.Ado.BeginTran(); // 业务逻辑 A校验 var validatorResult await _orderValidator.ValidateAsync(orderDto); if (!validatorResult.IsValid) { db.Ado.RollbackTran(); return false; } // 业务逻辑 B写库 await _orderRepository.InsertOrderAsync(orderDto); await _orderRepository.InsertItemsAsync(orderDto.Items); db.Ado.CommitTran(); return true; } catch (Exception ex) { db.Ado.RollbackTran(); _logger.LogError(ex, 订单创建事务失败); throw; }这段代码里有几个细节要说清楚第一BeginTran之后后续的 SqlSugar 操作如果发生异常不会自动回滚。因为手动模式把事务的“生杀大权”交给了开发者SQLSugar 不会做任何自动处理。所以你必须在catch里手动调用RollbackTran否则连接池里的连接会残留一个未提交的事务最直接的后果是连接被占用连接池耗尽后续所有数据库请求全部超时。这个坑我印象太深了有次生产环境半夜报警“连接池耗尽”查到最后就是同事在一个手动事务的catch块里忘记回滚。第二如果事务内已经执行了一些写操作然后你调了RollbackTran这些操作会被正确撤销。但如果你在事务内执行的是 DDL比如CREATE TABLE不同数据库的行为不一样SQL Server 支持 DDL 事务回滚MySQL 的 DDL 不支持事务直接隐式提交。也就是说你在事务里CreateTable回滚是无效的表已经建了。SqlSugar 的db.DbMaintenance.CreateTable()底层如果执行的是 MySQL 的 DDL同样不会参与事务回滚。这个特性要记住别指望事务能帮你撤销表结构变更。第三BeginTran之后同一个SqlSugarClient实例上的所有查询也会进入事务上下文。这意味着你在事务里做SELECT如果没有特别指定它也会放在这个事务里执行。在 MySQL 的默认隔离级别REPEATABLE READ下事务内的查询会创建一个一致性快照如果事务长时间不提交这个快照会一直占用内存同时阻塞其他事务对相关行做更新。3.4 场景四异步事务别把 async 塞进同步委托里现在 .NET 6 的项目基本都是全异步SqlSugar 对异步事务的支持比较完整关键方法是UseTranAsyncDbResultbool result await db.Ado.UseTranAsync(async () { await db.Insertable(order).ExecuteCommandAsync(); foreach (var item in items) { item.OrderId order.Id; await db.Insertable(item).ExecuteCommandAsync(); } });异步版本的委托支持async关键字方法本身要await。大部分使用上跟同步UseTran一致但有几个性能上的坑值得说。第一异步事务内部用的是AsyncLocal来传递事务上下文AsyncLocal在异步流中会沿着调用链传递但如果你的代码里有Task.Run或者ConfigureAwait(false)后跨线程执行理论上存在上下文丢失风险。SqlSugar 在这方面做过很多轮兼容修复但我依然建议在事务委托内不要使用Task.Run抛物线程。你想想事务的连接是从连接池里拿的绑定的线程上下文如果丢了事务还怎么把后续操作纳入同一个事务第二SqlSugar 的异步操作本质还是 ADO.NET 的异步它不会减少数据库本身的 IO 等待所以在高并发场景下异步事务能提升的是线程利用率不阻塞线程池线程而不是数据库响应速度。如果你的数据库是瓶颈事务改异步不会带来质的提升根本解决办法还是减少锁竞争、优化 SQL。第三UseTranAsync和UseTran的泛型版本不要混用。比如你用UseTran包裹了一个async方法由于同步委托不会识别异步这个异步方法实际上是“离开”了事务上下文的。你可以在事务委托里同步调用GetAwaiter().GetResult()强行把异步方法拉回同步上下文但这是一种非常危险的做法容易造成死锁尤其在 ASP.NET Core 以外的带同步上下文的宿主中。直接用UseTranAsync包裹异步逻辑别用同步版本包装异步方法。3.5 场景五嵌套事务与事务传播代码分层时的亲密陷阱项目越做越大事务肯定不会只在一个方法里出现。你会遇到服务 A 调服务 B服务 B 里自己开了事务服务 A 外层又开了事务。这时候 SqlSugar 的嵌套事务行为就非常重要了因为它的处理方式和很多人的直觉不一样。SqlSugar 对嵌套事务的处理原则概括成一句话内层事务不会真的开启新事务而是沿用外层事务。什么意思呢看代码// ServiceA db.Ado.BeginTran(); try { await _serviceB.CreateOrderAsync(order); db.Ado.CommitTran(); } catch { db.Ado.RollbackTran(); } // ServiceB public async Task CreateOrderAsync(Order order) { db.Ado.BeginTran(); // 这里不会真正开启新事务 try { await db.Insertable(order).ExecuteCommandAsync(); db.Ado.CommitTran(); // 这里不会真正提交只是把“内层事务标记”结束 } catch { db.Ado.RollbackTran(); // 这里会直接回滚整个外层事务 } }关键点在于内层CommitTran实际上只是减少了一个嵌套层级计数不会真正提交。但如果内层触发了RollbackTran会直接把外层事务标记为“已回滚”后续外层再CommitTran时SqlSugar 检测到事务已经被标记为回滚会抛出一个异常在某些版本中或者静默提交失败。这其实是一个安全机制内层都回滚了外层还提交个啥这个嵌套事务模型和 Spring 的REQUIRED传播行为很接近。你在做业务分层时如果多个服务方法都写了事务不用担心“内层提交了外层想回滚也来不及”这种问题因为内层根本不会提交。但反过来如果内层抛异常被捕获后没有调用RollbackTran只是直接吞掉了异常那么外层的事务还会继续执行最终提交这就会导致部分数据被写入——内层异常被吞是最危险的场景。我的建议是在嵌套事务场景里内层服务方法不要自己捕获异常后“静默处理”。要么让异常抛出去让事务统一回滚要么捕获异常后显式调用RollbackTran然后重新抛出。两者缺一不可。还有一种更干净的方式用[SqlSugarAop]或框架层面的工作单元UnitOfWork来管理事务不手动开事务。SqlSugar 的扩展包里有一个UnitOfWork的仓储模式封装它会自动根据方法是否标记了[UnitOfWork]特性来决定是否开启事务这能避免嵌套事务混乱的问题。如果你的项目是标准的仓储 服务分层强烈建议用这种方式把事务控制从业务代码里剥离出来。4. 事务之外的暗礁连接生命周期、AOP 事务切面与跨库事务4.1 连接必须及时释放事务范围要尽量小SqlSugar 默认的连接管理策略是“按需获取、使用即放”的。也就是说每次db.Ado.BeginTran()之后SqlSugar 会从连接池拿一个连接事务结束Commit 或 Rollback后自动释放连接回池。这是默认行为不需要手动关闭db里的Connection。但如果你在事务里手动db.Ado.Connection.Open()而事务结束又没有Close连接就会一直被占用。这种情况多发生在老代码迁移的时候——旧代码可能习惯手动管理连接SqlSugar 的Ado.Connection属性暴露了原生连接对象很多人会直接操作它。记住一旦使用了 SqlSugar 的事务 API连接生命周期就交给 SqlSugar 管别去手动 Open/Close。保持事务短小精悍这件事再怎么强调都不过分。事务持续时间越长持锁时间越长阻塞其他事务的概率越大。我在压测中见过最典型的问题一个事务里调用了外部 HTTP 接口接口响应 5 秒这 5 秒内事务一直持有数据库连接和行锁压测到 50 并发时数据库连接池直接被打满。所以有一条铁律事务内绝对不要做耗时的 IO 操作包括外部 HTTP 调用、文件写入、消息队列发送。如果你的业务确实需要在数据变更后发消息、调外部接口那就先提交事务再在事务提交后做这些事。如果外部调用失败需要补偿考虑引入消息表 定时任务的本地事务方案而不是硬把外部调用塞进事务里。4.2 AOP 事务切面SqlSugar 的高级玩法SqlSugar 的 AOP 功能有多强从开源社区的各种封装就能看出来。它提供了OnLogExecuting、OnError等 AOP 事件但你利用IDistributedTransaction和AOP可以做更高级的事情——比如自定义一个事务标签特性扫描方法上的特性自动开启事务。典型做法是定义一个特性[AttributeUsage(AttributeTargets.Method)] public class TransactionalAttribute : Attribute { public IsolationLevel IsolationLevel { get; set; } IsolationLevel.ReadCommitted; public bool IsAsync { get; set; } }然后通过 AOP 拦截可以用 Castle DynamicProxy、DispatchProxy 或直接在调用入口处处理遇到带[Transactional]的方法就自动开启事务、执行、提交或回滚。这样就实现了声明式事务管理——业务方法里不用写任何BeginTran、CommitTran的代码事务的开启和提交由框架统一控制。这种做法的好处不仅仅是少写代码更重要的是它强制了事务边界的统一性。团队里不同人写的代码不会再出现“有人用了事务有人忘了用”的情况因为只要方法标记了特性事务就一定生效。我在项目里落地过这种方案配合代码评审时强制要求所有多表写操作的方法必须加[Transactional]特性数据不一致的问题减少了大约八成。如果你想直接用 SqlSugar 官方推荐的这类方案它有一个SqlSugarUnitOfWork项目里引入SqlSugarCore.UnitOfWork包后可以直接使用[UnitOfWork]特性。用起来非常简单[UnitOfWork] public async Task CreateOrder(OrderDto dto) { // 方法内所有 SqlSugar 操作自动在同一事务中 await _orderRepo.InsertAsync(...); await _itemRepo.InsertAsync(...); }这个方案的好处是它和依赖注入容器深度集成自动从容器里获取当前的ISqlSugarClient事务结束自动提交或回滚。单元测试也好做因为可以替换ISqlSugarClient的实现。4.3 跨库事务与分布式事务什么时候真的需要它如果业务跨了两个数据库比如订单库和用户库都更新数据就要考虑跨库事务了。SqlSugar 的简单跨库事务可以用TransactionScope分布式事务协调器简称 MSDTC但它依赖 Windows 环境下的 MSDTC 服务而且在微服务架构、容器化部署下MSDTC 基本是鸡肋。我个人的经验是尽量避免跨库强一致事务采用最终一致性。比如订单创建时订单数据写订单库库存扣减在库存服务里异步完成。如果库存扣减失败通过消息队列把失败事件发给订单服务由订单服务把订单标记为“创建失败/已取消”。这是一个很成熟的模式也是 gozero、Saga、TCC 这些分布式事务框架的核心思路。如果团队强度高、业务确实需要跨库强一致性比如财务对账类系统推荐用 SqlSugar 配合 Seata 的 TCC 模式或者直接用本地消息表 消息队列实现可靠事件最终一致性。不推荐用TransactionScope跨库因为它涉及分布式事务协调器部署复杂不说性能损耗也大。从我实际落地过的项目经验来看90% 的业务场景都不需要真正的分布式事务——大多数是事务边界没画对把本该拆成两步的异步操作硬塞进了一个数据库事务里才导致不得不上分布式方案。5. 常见问题与排查技巧实录5.1 事务没生效的几大原因我在社区答疑和代码评审中遇到最多的问题就是“我明明用了事务为什么数据还是写进去了”。总结下来常见原因无非这几个第一个原因用了两个不同的 SqlSugarClient 实例。这是最隐蔽的坑。UseTran的作用范围仅限于同一个SqlSugarClient实例上执行的操作。如果你在事务里调了另一个new SqlSugarClient(...)的方法那部分操作完全在事务之外。尤其是用了依赖注入的项目如果你注入的是ISqlSugarClient和ISqlSugarClient的两个不同作用域实例很容易踩这个坑。解决办法是确保同一个请求上下文内的所有数据操作都使用同一个ISqlSugarClient实例。第二个原因用的是模拟的 SqlSugar 对象。单元测试里 Mock 了一个ISqlSugarClientMock 出来的UseTran不会真正开启数据库事务如果你只验证了方法被调用而没有验证真实的事务行为测试通过根本不代表生产环境没问题。第三个原因事务内混用了原生 ADO.NET 连接。你在 SqlSugar 事务里又用SqlConnection直接开了一个连接操作数据这两个连的是同一个库但是两个独立的连接、两个独立的事务上下文互相看不见。这个属于典型的“半只脚在事务外”。第四个原因异步方法没有走异步事务 API。前面已经详细说过UseTran里塞异步操作事务根本不能覆盖那些异步操作的真实执行上下文。排查顺序建议先确认UseTran里的所有数据库操作都用同一个ISqlSugarClient实例再确认有没有混用原生 ADO.NET最后看异步有没有走UseTranAsync。5.2 事务回滚了但数据还在为什么有一个场景特别迷惑result.IsSuccess为false说明事务确实回滚了但数据库里还是多了一条记录。这种情况大概率是这条记录不是在事务里写进去的。举个例子代码同时用db.Insertable(...).ExecuteCommandAsync()和db.Storageable(...).ExecuteCommand()或者.ExecuteCommandAsync()在同一个事务里操作。按理说Storageable也是 SqlSugar 的操作应该纳入事务。但是在某些版本里如果Storageable内部开启了单独的连接或者用了特殊的分区表/备库路由事务可能失效。排查方式是开启 SqlSugar 的 SQL 日志看事务开启和提交/回滚时对应的 SQL 是不是都在同一个连接上执行。另一个场景是 MySQL 中 MyISAM 表不支持事务。这个比较偏但确实有人踩过。MyISAM 是 MySQL 的老存储引擎不支持事务和行级锁你用BeginTran开启事务执行完插入即使后面主动RollbackTran插入的数据也回滚不掉。如果数据库里混用了 InnoDB 和 MyISAM 表事务只能保护 InnoDB 表MyISAM 表的行为是“即时生效”这会让事务的语义变得很难捉摸。解决办法是统一把表改成 InnoDB 引擎。5.3 高并发下的死锁与超时排查死锁是事务场景下最头疼的问题。两个事务互相持有对方的锁谁都等不到对方释放最后数据库检测到死锁杀掉其中一个事务抛出异常。常见的死锁场景是多个并发请求同时更新多张表但更新的顺序不一致。比如请求 A 先更新订单表再更新库存表请求 B 先更新库存表再更新订单表。当请求 A 锁住了订单表的某行请求 B 锁住了库存表的某行A 想等 B 释放库存锁B 想等 A 释放订单锁死锁诞生。解决思路很简单统一所有事务里的资源访问顺序。在项目规范里约定多表更新时按照表名字母序或业务主次顺序依次更新。比如任何涉及订单和库存的事务一律先订单后库存。这个规则简单但极其有效。另外要关注事务超时配置。SqlSugar 的CommandTimeout默认是 30 秒受 ADO.NET 默认值影响如果你的事务内 SQL 比较复杂或者数据库压力大超时时间可能不够。可以单独为事务设置更长的超时db.Ado.CommandTimeOut 60; // 单位秒但这只是治标更重要的还是优化 SQL 执行效率和锁范围。一个事务里更新 10000 行数据的操作不但慢而且锁范围巨大随时可能因为死锁被数据库杀掉。事务里操作的数据量要克制大事务要能拆小就拆小。5.4 排查事务问题的三个实用动作如果你在项目里遇到了诡异的事务问题我的排查路径基本固定为三步。第一步开启 SqlSugar 的 SQL 日志。设置db.Aop.OnLogExecuting (sql, pars) Console.WriteLine(sql)观察事务开启和提交的 SQL 前后的日志顺序。这能直接告诉你事务是否真的开启了操作是否在事务内。SqlSugar 开启事务时日志里会打印BEGIN TRANSACTION或类似语句不同数据库语法不同如果日志里没有说明事务根本没触达数据库。第二步用数据库自身的监控工具看连接和锁。SQL Server 用sys.dm_tran_locksMySQL 用SHOW ENGINE INNODB STATUS和performance_schema里的data_locks表看看事务持有哪些锁、锁了哪些行。配合第一步的日志基本能定位到具体是哪一行代码导致的锁问题。第三步写一个最小化复现程序脱离整个业务系统只保留开启事务 修改一条数据 回滚看看行为是否正常。如果最小化复现没问题那问题出在你的业务代码里如果最小化复现也有问题那大概率是版本兼容、数据库驱动或连接配置的问题。6. 在结尾处分享几点我自己踩过坑后的心得写这篇文章的时候我把 SqlSugar 事务从头到尾又过了一遍想起这几年踩过的坑有几条经验想特别提一下。第一事务不是越多越好。很多人为了省事给所有涉及数据库写操作的 Service 方法都加了事务其实徒增没必要的锁竞争。单表单条插入、单表更新本身就是一个原子操作不需要事务包装。只有多表写入、或者有“读-判断-写”这种复合逻辑时才值得开启事务。第二事务内查数据要慎重。在 MySQL REPEATABLE READ 级别下事务内第一次查询会建立快照后续查询都读这个快照。如果你的业务逻辑是“先查库存扣库存再查库存”第二次查询拿到的还是第一次的快照值它不会告诉你其他事务已经改了库存。这种场景要么把隔离级别调低要么用FOR UPDATE显式加锁要么接受最终一致性别在事务里反复查询试图拿最新值。第三SQLSugar 的事务 API 看起来简单但它的底层跟连接池、异步上下文、数据库隔离级别都有关系。不要只看官方文档的示例代码就直接上生产建议先在本地写一个和真实业务等价的验证程序把事务提交、回滚、嵌套、异常注入都测一遍确认行为符合预期后再上线。第四如果团队里多个人都在写数据访问层强烈建议统一封装一个工作单元UnitOfWork抽象把事务的开启、提交、回滚集中管理。这样即使有人对 SqlSugar 的事务机制不熟悉也不会因为乱开事务搞出一堆线上事故。如果你正在做 .NET 后端开发SqlSugar 的事务能力在同类 ORM 里算是相当能打的了。理解这几种事务模式的适用场景配合合理的连接管理和事务范围控制绝大多数业务的数据一致性需求都能满足。真遇到跨库强一致性的极端场景也别硬刚考虑引入分布式事务中间件或者调整业务设计最终一致性在很多业务里都是可以接受的。希望这篇文章能帮你少踩一些我踩过的坑。
返回列表