11. 事务、工作单元与并发
单个写操作不用管事务;一个方法里有多步写操作就必须加 [UnitOfWork],否则前一步成功、后一步失败会留下脏数据。
1. 三种事务方式的选择
① [UnitOfWork] 特性 ★ 推荐,AOP 自动提交/回滚,代码最干净
② 手动 BeginTran/Commit 需要精细控制提交时机、或在一个方法里分多段提交
③ SqlSugarUnitOfWork Furion 的 IUnitOfWork 实现,通常不用直接调用(被 ① 使用)2. [UnitOfWork] 用法
// 真实代码:Admin.NET.Core/Service/User/SysUserService.cs
[UnitOfWork]
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除用户")]
public virtual async Task DeleteUser(DeleteUserInput input)
{
var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
user.ValidateIsSuperAdminAccountType();
user.ValidateIsUserId(_userManager.UserId);
await OfflineAndExpireToken(user); // 1 下线
await _sysUserRep.DeleteAsync(user); // 2 删主表
await _sysUserRoleService.DeleteUserRoleByUserId(input.Id); // 3 删角色
await _sysUserExtOrgService.DeleteUserExtOrgByUserId(input.Id); // 4 删扩展机构
await _sysUserLdapService.DeleteUserLdapByUserId(input.Id); // 5 删域账号
await _sysUserMenuService.DeleteUserMenuList(input.Id); // 6 删收藏菜单
await _eventPublisher.PublishAsync(UserEventTypeEnum.Delete, input); // 7 发事件
}同一个 [UnitOfWork] 方法内调用的其他服务方法(如 _sysUserRoleService.DeleteUserRoleByUserId)共享同一个事务,这是因为所有 SqlSugarRepository<T> 都指向同一个 ISqlSugarClient 单例。
// 需要事务的其他真实用例
[UnitOfWork] [ApiDescriptionSettings(Name = "Add"), HttpPost] public async Task<long> AddUser(AddUserInput input)
[UnitOfWork] [ApiDescriptionSettings(Name = "Update"), HttpPost] public async Task UpdateUser(UpdateUserInput input)
[UnitOfWork] [DisplayName("授权用户角色")] public async Task GrantRole(UserRoleInput input)
[UnitOfWork] [ApiDescriptionSettings(Name = "Update"), HttpPost] public async Task UpdateConfig(UpdateConfigInput input)3. SqlSugarUnitOfWork 实现(了解即可)
// Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs
public sealed class SqlSugarUnitOfWork : IUnitOfWork
{
private readonly ISqlSugarClient _sqlSugarClient;
public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient) => _sqlSugarClient = sqlSugarClient;
public async Task BeginTransactionAsync(FilterContext context, UnitOfWorkAttribute unitOfWork)
=> await _sqlSugarClient.AsTenant().BeginTranAsync();
public async Task CommitTransactionAsync(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)
=> await _sqlSugarClient.AsTenant().CommitTranAsync();
public async Task RollbackTransactionAsync(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)
=> await _sqlSugarClient.AsTenant().RollbackTranAsync();
public Task OnCompletedAsync(FilterContext context, FilterContext resultContext)
{
_sqlSugarClient.Dispose();
return Task.CompletedTask;
}
}注册位置:SqlSugarSetup.AddSqlSugar() 里的 services.AddUnitOfWork<SqlSugarUnitOfWork>();
4. 手动事务
// 真实代码:Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs
[DisplayName("增加代码生成")]
[ApiDescriptionSettings(Name = "Add"), HttpPost]
public async Task AddCodeGen(AddCodeGenInput input)
{
try
{
ValidateTemplateParameters(input.Adapt<UpdateCodeGenInput>());
await _db.Ado.BeginTranAsync();
var codegen = input.Adapt<SysCodeGen>();
await _db.Insertable(codegen).ExecuteCommandAsync();
codegen.TableList.ForEach(e => e.CodeGenId = codegen.Id);
await _db.Insertable(codegen.TableList).ExecuteCommandAsync();
codegen.TableList.ForEach(e => { /* 整理字段序号 */ });
await _db.Storageable(codegen.TableList.SelectMany(e => e.ColumnList).ToList()).ExecuteCommandAsync();
await _db.Ado.CommitTranAsync();
}
catch (Exception)
{
await _db.Ado.RollbackTranAsync();
throw;
}
}也可以用 SqlSugar 的 UseTranAsync(自动包裹 try/catch):
var result = await _db.Ado.UseTranAsync(async () =>
{
await _db.Insertable(a).ExecuteCommandAsync();
await _db.Insertable(b).ExecuteCommandAsync();
});
if (!result.IsSuccess) throw Oops.Oh(result.ErrorMessage);手动事务必须
catch → RollbackTranAsync → throw,否则异常抛出后连接不会自动回滚。
5. 乐观锁:UpdateWithVerAsync
仓储内置(Admin.NET.Core/SqlSugar/SqlSugarRepository.cs):实体只要有名为 Ver 的 int 或 int? 属性,就会自动做版本号校验 + 自增。
public class SysStock : EntityTenantBaseData
{
[SugarColumn(ColumnDescription = "商品")]
public string Name { get; set; }
[SugarColumn(ColumnDescription = "库存")]
public int Qty { get; set; }
/// <summary>版本号(乐观锁,字段名必须是 Ver,类型 int/int?)</summary>
[SugarColumn(ColumnDescription = "版本号", DefaultValue = "0")]
public int Ver { get; set; }
}var entity = await _rep.GetByIdAsync(id);
entity.Qty -= 1;
// 生成的 SQL:UPDATE ... SET Ver = Ver + 1 WHERE Id = @Id AND Ver = @原Ver
var ok = await _rep.UpdateWithVerAsync(entity); // 默认字段 "Ver"
// var ok = await _rep.UpdateWithVerAsync(entity, "VerNo"); // 自定义字段名也支持
if (!ok) throw Oops.Oh("数据已被他人修改,请刷新后重试");行为说明:
| 场景 | 结果 |
|---|---|
实体无 Ver 属性 | 退化为普通更新,返回 受影响行数 > 0 |
有 Ver 且匹配 | Ver+1 并更新,返回 true |
有 Ver 但不匹配(并发) | 更新 0 行,返回 false |
有 Ver 但值为 null(int?) | 抛 ArgumentException |
6. 软删除:FakeDelete
Admin.NET.Core/Extension/RepositoryExtension.cs 提供同步/异步、按 Id/按表达式/按实体、单条/批量全套重载:
await _rep.FakeDeleteAsync(id); // 按主键
await _rep.FakeDeleteAsync(new List<long?> { 1, 2 }); // 批量主键
await _rep.FakeDeleteAsync(u => u.Status == 0); // 按表达式
await _rep.FakeDeleteAsync(entity); // 按实体(返回影响行数)
await _rep.FakeDeleteAsync(entityList); // 批量实体
await _db.FakeDeleteAsync<SysPos>(id); // 直接在 ISqlSugarClient 上用内部实现(ISqlSugarClient 版):
return db.Updateable(entity).AS()
.ReSetValue(x => { x.IsDelete = true; })
.IgnoreColumns(ignoreAllNullColumns: true)
.EnableDiffLogEvent()
.UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId, x.UpdateUserName })
.ExecuteCommandAsync();只更新 5 个列;
DeleteTime/UpdateTime/UpdateUserId/UpdateUserName由DataExecutingAOP 自动填值。
7. 差异日志:UpdateWithDiffLog / InsertWithDiffLog
开启 Database.json 的 EnableDiffLog: true 后,EnableDiffLogEvent() 的写操作会把前后值写到 SysLogDiff(落在日志库)。
await _rep.UpdateWithDiffLogAsync(entity); // 更新 + 差异日志(默认忽略 null 列)
await _rep.UpdateWithDiffLogAsync(entity, false); // 不忽略 null 列
await _rep.InsertWithDiffLogAsync(entity); // 新增 + 差异日志
await _db.UpdateWithDiffLogAsync(entity); // ISqlSugarClient 版差异日志的写入逻辑在 SqlSugarSetup.SetDbDiffLog():写 AfterData/BeforeData/BusinessData/DiffType/Sql/Parameters/Elapsed,并对 SysLogDiff 表本身做了递归保护。
8. 批量写入
// ① Storageable:有则更新、无则插入(按主键或 WhereColumns 指定比对列)
await _sysPosRep.Context.Storageable(posList).WhereColumns(u => u.Code).ExecuteCommandAsync();
await _db.Storageable(list).ExecuteCommandAsync();
// ② BulkCopy:纯批量插入(最快,不判重)
await _db.Fastest<SysDictData>().PageSize(300).BulkCopyAsync(list);
// ③ BulkMerge:批量插入或更新
await _db.Fastest<SysDictType>().PageSize(300).BulkMergeAsync(list);
// ④ 常规批量插入
await _rep.AsInsertable(list).ExecuteCommandAsync();| 方式 | 判重 | 速度 | 适用 |
|---|---|---|---|
Insertable(list) | ✗ | 中 | 几百条 |
Storageable(list) | ✓ | 中 | 导入/同步(有则更新) |
Fastest<T>().BulkCopy | ✗ | 最快 | 万级纯新增 |
Fastest<T>().BulkMerge | ✓ | 最快 | 万级 upsert |
种子数据初始化用的就是 StorageableByObject(seedData).ToStorage() + AsUpdateable + AsInsertable(见 SqlSugarSetup.InitTenantData)。
9. 步骤清单
- 单条写操作 → 什么都不用加
- 多步写操作 → 方法加
[UnitOfWork](最省心) - 需要手动控制提交点 / 分阶段提交 →
_db.Ado.BeginTranAsync()+try/catch+RollbackTranAsync+throw - 高并发扣减/状态流转 → 实体加
Ver字段 +UpdateWithVerAsync,失败提示"请刷新重试" - 业务数据删除一律
FakeDeleteAsync,配置/关联表才用DeleteAsync - 批量导入 →
Storageable(...).WhereColumns(...);万级数据用Fastest<T>().BulkMergeAsync - 需要审计 → 打开
EnableDiffLog并用UpdateWithDiffLogAsync/InsertWithDiffLogAsync
⚠️ 注意事项 / 常见坑
- [UnitOfWork] 只对请求线程内同一个
ISqlSugarClient生效,跨库(主库 + 日志库)写不在同一事务里。 [UnitOfWork]不能加在[NonAction]的内部方法上指望它生效——AOP 基于 Action Filter,内部方法请用手动事务。SqlSugarUnitOfWork.OnCompletedAsync里调用了_sqlSugarClient.Dispose();不要在事务未结束前再复用该 client。Ver字段必须是int或int?,写成long会被GetVerProperty判为不存在,静默退化成普通更新(乐观锁失效!)。FakeDelete只更新 5 个列,实体上其他字段的修改不会被保存。- 开启
EnableDiffLog会显著增加写入量,生产环境建议只在核心表上使用。 BulkCopy/BulkMerge在 Sqlite 下支持有限,换库前先验证。- 事务里不要做耗时 IO(发邮件、调远程接口)——长事务会拖垮连接池。事件发布建议放在事务提交后。
小结
多步写 → [UnitOfWork];要精细控制 → BeginTranAsync/CommitTranAsync/RollbackTranAsync;并发冲突 → Ver 字段 + UpdateWithVerAsync;业务删除 → FakeDeleteAsync;需要审计 → UpdateWithDiffLogAsync;大批量 → Storageable / Fastest<T>().BulkMerge。