事务是数据库系统中的重要概念,本文解说作者从业 CRUD 十余载的事务多种应用形式总结。
- 以下所有内容都是针对单机事务而言,不波及分布式事务相干的货色!
- 对于事务原理的解说不针对具体的某个数据库实现,所以某些中央可能和你的实践经验不符。
意识事务
为什么须要数据库事务?
转账是生存中常见的操作,比方从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的繁多操作,然而在数据库系统中,至多会分成两个步骤来实现:
1.将A账户的金额缩小100元
2.将B账户的金额减少100元。
在这个过程中可能会呈现以下问题:
1.转账操作的第一步执行胜利,A账户上的钱缩小了100元,然而第二步执行失败或者未执行便产生零碎解体,导致B账户并没有相应减少100元。
2.转账操作刚实现就产生零碎解体,零碎重启复原时失落了解体前的转账记录。
3.同时又另一个用户转账给B账户,因为同时对B账户进行操作,导致B账户金额出现异常。
为了便于解决这些问题,须要引入数据库事务的概念。
以上内容援用自:https://www.cnblogs.com/takum...
意识 ADO.NET
ADO.NET是.NET框架中的重要组件,次要用于实现C#应用程序拜访数据库。
ADO.NET的组成:
System.Data.Common → 各种数据拜访类的基类和接口
System.Data.SqlClient → 对Sql Server进行操作的数据拜访类
a) SqlConnection → 数据库连接器
b) SqlCommand → 数据库命名对象
d) SqlDataReader → 数据读取器
f) SqlParameter → 为存储过程定义参数
g) SqlTransaction → 数据库事物
事务1:ADO.NET
最原始的事务应用形式,毛病:
- 代码又臭又长
- 逻辑难管制,一不小心就忘了提交或回滚,随即而来的是数据库锁得不到开释、或者连接池不够用
- 跨办法传递 Tran 对象太麻烦
举荐:★☆☆☆☆
SqlConnection conn = new SqlConnection(connString);SqlCommand cmd = new SqlCommand();cmd.Connection = conn;try{ conn.Open(); cmd.Transaction = conn.BeginTransaction();//开启事务 int result = 0; foreach (string sql in sqlList) { cmd.CommandText = sql; result += cmd.ExecuteNonQuery(); } cmd.Transaction.Commit();//提交事务 return result;}catch (Exception ex){ //写入日志... if (cmd.Transaction != null) cmd.Transaction.Rollback();//回滚事务 throw new Exception("调用事务更新办法时出现异常:" + ex.Message);}finally{ if (cmd.Transaction != null) cmd.Transaction = null;//革除事务 conn.Close();}
事务2:SqlHelper
原始 ADO.NET 事务代码又臭又长,是时候封装一个 SqlHelper 来操作 ADO.NET 了。比方:
SqlHelper.ExecuteNonQuery(...);SqlHelper.ExecuteScaler(...);
这样封装之后对单次命令执行的确办法不了少,用着用着又发现,事务怎么解决?重截一个 IDbTransaction 参数传入吗?比方:
SqlHelper.ExecuteNonQuery(tran, ...);SqlHelper.ExecuteScaler(tran, ...);
举荐:★☆☆☆☆
如同也还行,勉强能承受。
随着在我的项目一直的实际,总有一天不能再忍耐这种 tran 传递的形式,因为它太容易漏传,特地是跨办法传来传去的时候,真的太难了。
事务3:利用线程id
在晚期 .NET 还没有异步办法的时候,对事务2的缺点进行了简略封装,防止事务 tran 对象传来传去的问题。
其原理是利用线程id,在事务开启之时保留到 staic Dictionary<int, IDbTransaction> 之中,在 SqlHelper.ExecuteXxx 办法执行之前获取以后线程的事务对象,执行命令。
这样免去了事务传递的恶梦,最终出现的事务代码如下:
SqlHelper.Transaction(() =>{ SqlHelper.ExecuteNonQuery(...); //不再须要显式传递 tran SqlHelper.ExecuteScaler(...);});
这种事务应用起来非常简单,不须要思考事务提交/开释问题,被默认利用在了 FreeSql 中,毛病:不反对异步。
举荐:★★★☆☆
同线程事务应用简略,同时又产生了设计限度:
- 默认是提交,遇异样则回滚;
- 事务对象在线程挂载,每个线程只可开启一个事务连贯,嵌套应用的是同一个事务;
- 事务体内代码不能够切换线程,因而不可应用任何异步办法,包含FreeSql提供的数据库异步办法(能够应用任何 Curd 同步办法);
事务4:工作单元
显式将 ITransaction 对象传来传去,说间接点像少女没穿衣服街上乱跑一样,不平安。而且到时候想给少女带点货(状态),赤身露体没穿衣服咋带货(没口袋)。
这个时候对 ITransaction 做一层包装就显得有必要了,在IUnitOfWork 中能够定义更多的状态属性。
举荐:★★★★☆
定义 IUnitOfWork 接口如下:
public interface IUnitOfWork : IDisposable{ IDbTransaction GetOrBeginTransaction(); //创立或获取对应的 IDbTransaction IsolationLevel? IsolationLevel { get; set; } void Commit(); void Rollback();}
事务5:AOP 事务
技术一直在倒退,先来一堆实践:
以下内容援用自:https://www.cnblogs.com/zhuge...
AOP(Aspect-Oriented Programming,面向方面编程),能够说是OOP(Object-Oriented Programing,面向对象编程)的补充和欠缺。OOP引入封装、继承和多态性等概念来建设一种对象层次结构,用以模仿公共行为的一个汇合。当咱们须要为扩散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP容许你定义从上到下的关系,但并不适宜定义从左到右的关系。例如日志性能。日志代码往往程度地分布在所有对象档次中,而与它所分布到的对象的外围性能毫无关系。对于其余类型的代码,如安全性、异样解决和通明的持续性也是如此。这种分布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的反复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象外部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简略地说,就是将那些与业务无关,却为业务模块所独特调用的逻辑或责任封装起来,便于缩小零碎的反复代码,升高模块间的耦合度,并有利于将来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的办法,就好像一把利刃,将这些空心圆柱体剖开,以取得其外部的音讯。而剖开的切面,也就是所谓的“方面”了。而后它又以巧夺天功的妙手将这些剖开的切面还原,不留痕迹。
应用“横切”技术,AOP把软件系统分为两个局部:外围关注点和横切关注点。业务解决的次要流程是外围关注点,与之关系不大的局部是横切关注点。横切关注点的一个特点是,他们常常产生在外围关注点的多处,而各处都根本类似。比方权限认证、日志、事务处理。Aop 的作用在于拆散零碎中的各种关注点,将外围关注点和横切关注点拆散开来。正如Avanade公司的高级计划构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供反对的通用服务进行拆散。”
实现AOP的技术,次要分为两大类:一是采纳动静代理技术,利用截取音讯的形式,对该音讯进行装璜,以取代原有对象行为的执行;二是采纳动态织入的形式,引入特定的语法创立“方面”,从而使得编译器能够在编译期间织入无关“方面”的代码。
最终出现的应用代码如下:
[Transactional]public void SaveOrder(){ SqlHelper.ExecuteNonQuery(...); SqlHelper.ExecuteScaler(...);}
举荐:★★★★☆
利用 [Transactional] 个性标记 SaveOrder 开启事务,他其实是执行相似这样的操作:
public void SaveOrder(){ var (var tran = SqlHelper.BeginTransaction()) { try { SqlHelper.ExecuteNonQuery(tran, ...); SqlHelper.ExecuteScaler(tran, ...); tran.Commit(); } catch { tran.Roolback(); throw; } }}
解决了即不必显着传递 tran 对象,也解决了异步逻辑难管制的问题。
目前该事务形式在 Asp.NETCore 中利用比拟宽泛,实现起来相当简略,利用动静代理技术,替换 Ioc 中注入的内容,动静拦挡 [Transactional] 个性标记的办法。
应用 Ioc 后就不能再应用 SqlHelper 技术了,此时应该应用 Repository。
组合技术:Ioc + Repository + UnitOfWork
理解原理比拟重要,本节讲得比拟形象,如果想深刻理解原理,请参考 FreeSql 的应用实现代码如下:
自定义仓储基类
public class UnitOfWorkRepository<TEntity, TKey> : BaseRepository<TEntity, TKey>{ public UnitOfWorkRepository(IFreeSql fsql, IUnitOfWork uow) : base(fsql, null, null) { this.UnitOfWork = uow; }}public class UnitOfWorkRepository<TEntity> : BaseRepository<TEntity, int>{ public UnitOfWorkRepository(IFreeSql fsql, IUnitOfWork uow) : base(fsql, null, null) { this.UnitOfWork = uow; }}
注入仓储、单例 IFreeSql、AddScoped(IUnitOfWork)
public static IServiceCollection AddFreeRepository(this IServiceCollection services, params Assembly[] assemblies){ services.AddScoped(typeof(IReadOnlyRepository<>), typeof(UnitOfWorkRepository<>)); services.AddScoped(typeof(IBasicRepository<>), typeof(UnitOfWorkRepository<>)); services.AddScoped(typeof(BaseRepository<>), typeof(UnitOfWorkRepository<>)); services.AddScoped(typeof(IReadOnlyRepository<,>), typeof(UnitOfWorkRepository<,>)); services.AddScoped(typeof(IBasicRepository<,>), typeof(UnitOfWorkRepository<,>)); services.AddScoped(typeof(BaseRepository<,>), typeof(UnitOfWorkRepository<,>)); if (assemblies?.Any() == true) foreach (var asse in assemblies) foreach (var repo in asse.GetTypes().Where(a => a.IsAbstract == false && typeof(UnitOfWorkRepository).IsAssignableFrom(a))) services.AddScoped(repo); return services;}
事务6:UnitOfWorkManager
举荐:★★★★★
(事务5)申明式事务管理在底层是建设在 AOP 的根底之上的。其本质是对办法前后进行拦挡,而后在指标办法开始之前创立或者退出一个事务,在执行完指标办法之后依据执行状况提交或者回滚事务。
申明式事务最大的长处就是不须要通过编程的形式治理事务,这样就不须要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相干的事务规定申明(或通过等价的基于标注的形式),便能够将事务规定利用到业务逻辑中。因为事务管理自身就是一个典型的横切逻辑,正是 AOP 的用武之地。
通常状况下,笔者强烈建议在开发中应用申明式事务,不仅因为其简略,更次要是因为这样使得纯业务代码不被净化,极大不便前期的代码保护。
和编程式事务相比,申明式事务惟一有余中央是,后者的最细粒度只能作用到办法级别,无奈做到像编程式事务那样能够作用到代码块级别。然而即使有这样的需要,也存在很多变通的办法,比方,能够将须要进行事务管理的代码块独立为办法等等。
事务6 UnitOfWorkManager 参考隔壁弱小的 java spring 事务管理机制,而事务5只能定义繁多事务行为(比方不能嵌套),事务6 UnitOfWorkManager 实现的行为机制如下:
六种传播方式(propagation),意味着跨办法的事务十分不便,并且反对同步异步:
- Requierd:如果以后没有事务,就新建一个事务,如果已存在一个事务中,退出到这个事务中,默认的抉择。
- Supports:反对以后事务,如果没有以后事务,就以非事务办法执行。
- Mandatory:应用以后事务,如果没有以后事务,就抛出异样。
- NotSupported:以非事务形式执行操作,如果以后存在事务,就把以后事务挂起。
- Never:以非事务形式执行操作,如果以后事务存在则抛出异样。
- Nested:以嵌套事务形式执行。
参考 FreeSql 的应用形式如下:
第一步:配置 Startup.cs 注入
//Startup.cspublic void ConfigureServices(IServiceCollection services){ services.AddSingleton<IFreeSql>(fsql); services.AddScoped<UnitOfWorkManager>(); services.AddFreeRepository(null, typeof(Startup).Assembly);}
UnitOfWorkManager 成员 | 阐明 |
---|---|
IUnitOfWork Current | 返回以后的工作单元 |
void Binding(repository) | 将仓储的事务交给它治理 |
IUnitOfWork Begin(propagation, isolationLevel) | 创立工作单元 |
第二步:定义事务个性
[AttributeUsage(AttributeTargets.Method)]public class TransactionalAttribute : Attribute{ /// <summary> /// 事务传播方式 /// </summary> public Propagation Propagation { get; set; } = Propagation.Requierd; /// <summary> /// 事务隔离级别 /// </summary> public IsolationLevel? IsolationLevel { get; set; }}
第三步:引入动静代理库
在 Before 从容器中获取 UnitOfWorkManager,调用它的 var uow = Begin(attr.Propagation, attr.IsolationLevel) 办法
在 After 调用 Before 中的 uow.Commit 或者 Rollback 办法,最初调用 uow.Dispose
第四步:在 Controller 或者 Service 或者 Repository 中应用事务个性
public class SongService{ BaseRepository<Song> _repoSong; BaseRepository<Detail> _repoDetail; SongRepository _repoSong2; public SongService(BaseRepository<Song> repoSong, BaseRepository<Detail> repoDetail, SongRepository repoSong2) { _repoSong = repoSong; _repoDetail = repoDetail; _repoSong2 = repoSong2; } [Transactional] public virtual void Test1() { //这里 _repoSong、_repoDetail、_repoSong2 所有操作都是一个工作单元 this.Test2(); } [Transactional(Propagation = Propagation.Nested)] public virtual void Test2() //嵌套事务,新的(不应用 Test1 的事务) { //这里 _repoSong、_repoDetail、_repoSong2 所有操作都是一个工作单元 }}
问题:是不是进办法就开事务呢?
不肯定是实在事务,有可能是虚的,就是一个假的 unitofwork(不带事务)。
也有可能是延用上一次的事务。
也有可能是新开事务,具体要看流传模式。
结束语
技术一直的演变提高,从 1.0 -> 10.0 须要慢长的过程。
同时呐喊大家不要自觉应用微服务,演变的过程周期漫长对我的项目的危险太高。
早上五点半醒来,写下本文对事务了解的一点总结。谢谢!!
以上各种事务机制在 FreeSql 中都有实现,FreeSql 是功能强大的对象关系映射技术(O/RM),反对 .NETCore 2.1+ 或 .NETFramework 4.0+ 或 Xamarin。反对 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/达梦/人大金仓/神舟通用/Access;单元测试数量 5000+,以 MIT 开源协定托管于 github:https://github.com/dotnetcore/FreeSql