事务是数据库系统中的重要概念,本文讲解作者从业 CRUD 十余载的事务多种使用方式总结。

  • 以下所有内容都是针对单机事务而言,不涉及分布式事务相关的东西!
  • 关于事务原理的讲解不针对具体的某个数据库实现,所以某些地方可能和你的实践经验不符。

认识事务

为什么需要数据库事务?

转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:

1.将A账户的金额减少100元

2.将B账户的金额增加100元。

在这个过程中可能会出现以下问题:

1.转账操作的第一步执行成功,A账户上的钱减少了100元,但是第二步执行失败或者未执行便发生系统崩溃,导致B账户并没有相应增加100元。

2.转账操作刚完成就发生系统崩溃,系统重启恢复时丢失了崩溃前的转账记录。

3.同时又另一个用户转账给B账户,由于同时对B账户进行操作,导致B账户金额出现异常。

为了便于解决这些问题,需要引入数据库事务的概念。

以上内容引用自:https://www.cnblogs.com/takumicx/p/9998844.html


认识 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:SqlHepler

原始 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/zhugenqiang/archive/2008/07/27/1252761.html

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只能定义单一事务行为(比如不能嵌套),事务5实现的行为机制如下:

六种传播方式(propagation),意味着跨方法的事务非常方便,并且支持同步异步:

  • Requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,默认的选择。
  • Supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • Mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • NotSupported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • Never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • Nested:以嵌套事务方式执行。

参考 FreeSql 的使用方式如下:

第一步:配置 Startup.cs 注入

//Startup.cs
public 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

.NET 数据库事务的各种玩法进化的更多相关文章

  1. mysql数据库中表记录的玩法

    一.增加表记录(相当于插入表记录) 1. 插入完整数据(顺序插入) 语法一: INSERT INTO 表名(字段1,字段2,字段3…字段n) VALUES(值1,值2,值3…值n); 语法二: INS ...

  2. 一种通过MQ使缓存和数据库同步的玩法

    其他相关玩法 可以搜索 mysql 和 redis 结合使用

  3. WEB安全新玩法 [1] 业务安全动态加固平台

    近年来,信息安全体系建设趋于完善,以注入攻击.跨站攻击等为代表的传统 Web 应用层攻击很大程度上得到了缓解.但是,Web 应用的业务功能日益丰富.在线交易活动愈加频繁,新的安全问题也随之呈现:基于 ...

  4. 轻量级高性能ORM框架:Dapper高级玩法

    Dapper高级玩法1: 数据库中带下划线的表字段自动匹配无下划线的Model字段. Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; 备 ...

  5. 全面解密QQ红包技术方案:架构、技术实现、移动端优化、创新玩法等

    本文来自腾讯QQ技术团队工程师许灵锋.周海发的技术分享. 一.引言 自 2015 年春节以来,QQ 春节红包经历了企业红包(2015 年).刷一刷红包(2016 年)和 AR 红包(2017 年)几个 ...

  6. MySQL高可用新玩法之MGR+Consul

    前面的文章有提到过利用consul+mha实现mysql的高可用,以及利用consul+sentinel实现redis的高可用,具体的请查看:http://www.cnblogs.com/gomysq ...

  7. 朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh)

    十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别.最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器 ...

  8. 【腾讯云的1001种玩法】几种在腾讯云建立WordPress的方法(Linux)(二)

    版权声明:本文由张宁原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/126547001488207964 来源:腾云阁 ht ...

  9. windows下mongodb基础玩法系列二CURD操作(创建、更新、读取和删除)

    windows下mongodb基础玩法系列 windows下mongodb基础玩法系列一介绍与安装 windows下mongodb基础玩法系列二CURD操作(创建.更新.读取和删除) windows下 ...

随机推荐

  1. IDEA解决MAVEN下载插件慢问题

    原文链接:https://blog.csdn.net/qq_25983579/article/details/104398915 使用阿里的maven镜像 右键项目选中maven选项,然后选择“ope ...

  2. 渲染导航菜单的同时给每个菜单绑定不同的router跳转

    这个问题一开始的时候,我总想着router跳转只有两种方式 一种@click,一种router-link 然后我想着@click,绑定一个事件,事件下面无法确定我当前是哪个菜单,解决不了. 然后< ...

  3. PHP prev() 函数

    实例 输出数组中的当前元素.下一个元素和上一个元素的值: <?php$people = array("Peter", "Joe", "Glenn ...

  4. PHP unpack() 函数

    实例 从二进制字符串对数据进行解包: <?php$data = "PHP";print_r(unpack("C*",$data));?>高佣联盟 w ...

  5. PHP getName() 函数

    实例 返回 XML 元素及其子元素的名称: <?php$xml=<<<XML高佣联盟 www.cgewang.com<?xml version="1.0&quo ...

  6. C/C++编程笔记:C语言入门知识点(三),请收藏C语言最全笔记!

    今天我们继续来学习C语言的入门知识点,第一课:C/C++编程笔记:C语言入门知识点(二),请收藏C语言最全笔记! 21. 输入 & 输出 当我们提到输入时,这意味着要向程序填充一些数据.输入可 ...

  7. Spark中直接操作HDFS

    Spark作为一个基于内存的大数据计算框架,可以和hadoop生态的资源调度器和分布式文件存储系统无缝融合.Spark可以直接操作存储在HDFS上面的数据: 通过Hadoop方式操作已经存在的文件目录 ...

  8. MyBatis-Plus使用(4)-集成SpringBoot

    我这里使用的MyBatis-Plus是当前最新的3.2.0版本, 1. 引入需要的jar,基础jar包括: <dependencies> <dependency> <gr ...

  9. JDK8的Optional用法

    参考资料:https://www.baeldung.com/java-optional https://mp.weixin.qq.com/s/P2kb4fswb4MHfb0Vut_kZg 1. 描述 ...

  10. Springboot开启事务的支持

    主要分为两步 步骤一.在main方法加上@EnableTransactionManagement注解: @SpringBootApplication @EnableTransactionManagem ...