事务是数据库系统中的重要概念,本文讲解作者从业 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. Seaborn实现多变量分析

    import seaborn as sns import numpy as np import pandas as pd import matplotlib.pyplot as plt sns.set ...

  2. Django学习路5_更新和删除数据库表中元素

    查找所有的元素 Student.objects.all() 查找单个元素 Student.objects.get(主键=值) 主键 pk = xxx 更新数据库数据后进行保存 stu.save() 删 ...

  3. P5979 [PA2014]Druzyny dp 分治 线段树 分类讨论 启发式合并

    LINK:Druzyny 这题研究了一下午 终于搞懂了. \(n^2\)的dp很容易得到. 考虑优化.又有大于的限制又有小于的限制这个非常难处理. 不过可以得到在限制人数上界的情况下能转移到的最远端点 ...

  4. 4.26 ABC F I hate Matrix Construction 二进制拆位 构造 最大匹配

    LINK:I hate Matrix Construction 心情如题目名称. 主要说明一下构造的正确性. 准确来说这道题困扰我很久. 容易发现可以拆位构造. 这样题目中的条件也比较容易使用. 最后 ...

  5. windows:shellcode 远程线程hook/注入(五)

    前面几篇文章介绍了通过APC注入.进程注入.windows窗口处理函数回调.kernercallback回调执行shellcode,今天继续介绍通过heap Spray(翻译成中文叫堆喷射)执行she ...

  6. [转]Nginx限流配置

    原文:https://www.cnblogs.com/biglittleant/p/8979915.html 作者:biglittleant 1. 限流算法 1.1 令牌桶算法 算法思想是: 令牌以固 ...

  7. ASP.NET中使用Cache类来缓存页面的信息

    实现 如果将数据保存在全局应用程序对象Application中,值将会在程序运行时一直存在,而我们只需要缓存一段时间. ASP.NET提供了一个Cache对象来执行对象数据的缓存. Cache对象是S ...

  8. 6月28日考试 题解(GCD约分+动态规划+树状数组二维偏序)

    前言:考的一般般吧……T3暴力没打上来挺可惜的,到手的75分没了. ---------------------------------- T1 [JZOJ4745]看电影 Description 听说 ...

  9. JSON 和 POJO 互转,List<T> 和 JSON 互转

    JSON 和 POJO import com.alibaba.fastjson.JSONObject; import org.slf4j.Logger; import org.slf4j.Logger ...

  10. ios textView跟随键盘的移动

    实现效果: textview 能够跟随键盘的移动而移动 效果图如下: 下边贴上主要的代码: 1.创建textview @interface ViewController ()<UITextVie ...