如何开个人网站,大连网站设计选仟亿科技,万网怎么建立网站,搜狐做app的网站在业务开发中#xff0c;使用数据库事务是必不可少的。而开发中往往会使用各种 ORM 执行数据库操作#xff0c;简化代码复杂度#xff0c;不过#xff0c;由于各种 ORM 的封装特性#xff0c;开发者的使用方式也不一样#xff0c;开发者想要了解 ORM 对事务做了什么处理是…在业务开发中使用数据库事务是必不可少的。而开发中往往会使用各种 ORM 执行数据库操作简化代码复杂度不过由于各种 ORM 的封装特性开发者的使用方式也不一样开发者想要了解 ORM 对事务做了什么处理是比较难的。因此本文介绍数据库事务基础、Ado.net 事务、如何封装 DbContext 读者掌握以后可以加深对 C# 使用事务的理解使用各种 ORM 时也会更应手。 生成数据库数据 为了演示各种事务操作我们想要先创建 demo 数据打开 filldb 官网根据操作提示生成模拟数据。
filldb 地址 Step 1: Upload Database schema FillDB 是一款免费工具可快速生成大量 MySql 格式的自定义数据用于测试软件和使用随机数据填充数据库。 然后按照 authors、posts 的顺序点击 Generate 生成数据库数据。 因为 posts 有 authors 的外键因此生成数据的顺序是 authors、posts。 最后点击 Export database 导出 SQL 即可。 然后在数据库中导入数据。 为了连接 Mysql 数据库这里使用 MySqlConnector 驱动请在创建控制台项目之后通过 nuget 引入此包。 MySqlConnector 的主要部件和 API 如下 使用同步方法可能会对托管线程池产生不利影响如果没有正确调优还会导致速度减慢或锁定。 Mysql 连接字符串配置示例 const string connectionString Serverlocalhost;Port3306;User IDmysqltest;PasswordPassword123;Databasemysqldb; 或使用 MySqlConnectionStringBuilder 构建连接字符串 var connectionBuilder new MySqlConnectionStringBuilder(){Server localhost,Port 3306,UserID mysqltest,Password Password123,Database mysqldb
};
var connectionString connectionBuilder.ConnectionString; 详细连接字符串配置可以在 MySQL Connection String Options for .NET/C# - MySqlConnector 中找到。 为了让 MysqlConnetor 可以记录日志需要手动配置日志程序。 完整的 nuget 包如下 ItemGroupPackageReference IncludeMicrosoft.Extensions.Logging Version8.0.0 /PackageReference IncludeMicrosoft.Extensions.Logging.Console Version8.0.0 /PackageReference IncludeMySqlConnector Version2.3.1 /PackageReference IncludeMySqlConnector.Logging.Microsoft.Extensions.Logging Version2.1.0 //ItemGroup 配置连接字符串、配置日志、创建数据库连接完整代码示例如下 var loggerFactory LoggerFactory.Create(builder builder.AddConsole());
var logger loggerFactory.CreateLoggerProgram();
var dataSourceBuilder new MySqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseLoggerFactory(loggerFactory);
await using var dataSource dataSourceBuilder.Build();using var connection dataSource.CreateConnection(); 经过以上配置之后我们拥有了模拟数据库以及基础代码下面我们来正式学习 MysqlConnetor 和数据库事务相关的知识。 Mysql 数据库事务基础 百度百科数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列这些操作要么全部执行,要么全部不执行是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。 数据库事务有四个特性 原子性原子性是指包含事务的操作要么全部执行成功要么全部失败回滚。 一致性一致性指事务在执行前后状态是一致的。 隔离性一个事务所进行的修改在最终提交之前对其他事务是不可见的。 持久性数据一旦提交其所作的修改将永久地保存到数据库中。 相信大家对数据库事务都不陌生因此这里就不扯淡了下面来讲解不同数据库事务的特征。 数据库的并发一致性问题 虽然数据库事务可以帮助我们执行数据库操作、回滚操作但是数据库事务并发执行时事务之间可能会相互干扰比如脏读、幻读等现象我们使用数据库事务时要根据严格程度和性能之间相互平衡选择事务隔离级别。 当多个事务并发执行时可能会出现以下问题 脏读 事务 A 更新了数据但还没有提交这时事务 B 读取到事务 A 更新后的数据然后事务 A 回滚了事务 B 读取到的数据就成为脏数据了。 不可重复读 事务 A 对数据进行多次读取事务 B 在事务 A 多次读取的过程中执行了更新操作并提交了导致事务 A 多次读取到的数据并不一致。 不可重复读特征是相同的数据在事务 A 的不同阶段读取的数据不一样。 幻读 事务 A 在读取数据后事务 B 向事务A读取的数据中插入了几条数据事务 A 再次读取数据时发现多了几条数据和之前读取的数据不一致。 幻读前后数据量不一样。 丢失修改 事务 A 和事务 B 都对同一个数据进行修改事务 A 先修改事务 B 随后修改事务 B 的修改覆盖了事务 A 的修改。 不可重复度和幻读看起来比较像它们主要的区别是在不可重复读中发现数据不一致主要是数据被更新了。在幻读中发现数据不一致主要是数据增多或者减少了。 数据库事务的隔离级别 数据库事务的隔离级别有以下四种按隔离级别从低到高 未提交读一个事务在提交前它的修改对其他事务也是可见的。 提交读一个事务提交之后它的修改才能被其他事务看到。 可重复读在同一个事务中多次读取到的数据是一致的。 串行化需要加锁实现会强制事务串行执行。 Ado.net 中使用 System.Data.IsolationLevel 枚举表示以上几种数据库事务隔离级别 public enum IsolationLevel{// 未指定Unspecified -1,// 不能覆盖来自更高度隔离的事务的挂起的更改。Chaos 16,// 未提交读脏读是可能的这意味着不会发出共享锁也不会使用独占锁。ReadUncommitted 256,// 提交读在读取数据时持有共享锁以避免脏读但是数据可以在事务结束之前更改从而导致不可重复读取或幻像数据。ReadCommitted 4096,// 可重复读锁被放置在查询中使用的所有数据上防止其他用户更新数据。防止不可重复读取但仍然可以使用幻像行。RepeatableRead 65536,// 串行化将在 DataSet 上放置一个范围锁以防止其他用户在事务完成之前更新数据集或将行插入数据集。Serializable 1048576,// 通过存储一个应用程序可以读取而另一个应用程序正在修改相同数据的数据版本来减少阻塞。// 指示即使重新查询也无法从一个事务中看到在其他事务中所做的更改。Snapshot 16777216} 数据库的隔离级别分别可以解决数据库的脏读、不可重复读、幻读等问题。 其实也不必纠结这些问题可以按照读写锁的情况来理解。 编程中由于多个线程并发操作两个字典 Dictionarystring, string a;
Dictionarystring, string b; 第一个问题时并发操作一个字典时会出现线程并发异常。 所以我们想要使用并发字典 ConcurrentDictionarystring, string a;
ConcurrentDictionarystring, string b; 可是当 T1 线程修改 a 完成接着修改 b 时线程 T2 把字典 a 修改了。这就导致了数据不一致。 使用读写锁优化将 a、b 两个数据包在一起 ConcurrentDictionarystring, string a;ConcurrentDictionarystring, string b;private static ReaderWriterLockSlim _lock new ReaderWriterLockSlim();// 读private void Read(){try{_lock.EnterReadLock(); // 读}catch { }finally{_lock.ExitReadLock(); // 释放读取锁}}// 写public void Write(int key, int value){try{_lock.EnterUpgradeableReadLock();_lock.EnterWriteLock();// 写_lock.ExitWriteLock();}catch { }finally{_lock.ExitUpgradeableReadLock();}} 读写锁的原理很简单读和写是两个冲突的操作。当没有线程 写 时多个线程可以并发 读此时不会有任何问题。当有一个线程 写 时既不允许有其它线程同时在 写 也不允许其它线程同时在 读。也就是说读 是可以并发的但是写是独占的。 串行化 当然对于数据库事务就复杂了很多。如果要按照读写锁的形式去做那么其隔离级别相当于 串行化整个表都被锁住不允许事务并发执行此时不会有 脏读、不可重复读、 幻读 这些情况。 可是这样对于数据库来说压力是很大的会严重拖垮数据库的性能以及严重降低了业务程序的并发量。
当事务 A 只需要修改 id1,2,3 的数据时使用 串行化 级别会锁住整个表。这样似乎有点太浪费了。 可重复读 那么我们只需要锁住事务 A 正在修改的那几行记录不就行了吗那么我们把数据库事务下降一个级别使用 可重复读。 使用 可重复读 事务级别其被锁住的数据依然保持安全也就是不会被其它事务所修改。所以不会出现 脏读、不可重复读。但是因为不是锁住整个表因此其它事务是可以插入数据的这就导致了会出现 幻读。当然可重复读 出现的问题一般来说只需要保证事务中只处理自己想要的数据即可。 可重复读 导致的 幻读 问题比如 A 事务在 笔记本 分类下给联想笔记本型号都打 9 折优惠可是此时 B 事务从 笔记本 分类下增加了几个理想笔记本型号。结果事务 A 最后一查询把 B 事务插入的数据查询出来了。那么事务 A 查询的数据就包含了打折和未打折的数据了。 InnoDB 使用 MVCC 来实现高并发性并实现了所有 4 个SQL标准隔离级别。InnoDB 默认为 REPEATABLE READ (可重复读)隔离级别并且通过间隙锁(next-key locking)策略来防止在这个隔离级别上的幻读。InnoDB 不只锁定在查询中涉及的行还会对索引结构中的间隙进行锁定以防止幻行被插入。 提交读 使用示例 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE pet SET NAME A;
SELECT SLEEP(5);
SELECT * from pet;
COMMIT;SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE pet SET NAME B;
SELECT SLEEP(5);
SELECT * from pet;
COMMIT; A 事务和 B 事务运行时大家都对 name 做了修改但是事务只能看到自己做出的修改也就是说B 事务未提交之前A、B 都修改了数据但是是隔离的。 A 事务修改了 name A B 事务修改了 name B 未提交之前A、B 事务读到的分别是 A、B这没问题不会干扰。 但是如果 A 先提交了事务那么数据库的 name 值就为 A此时 B 事务还没有提交B 查询到的 name A这就是不可重复读。 提交读 只能保证事务未提交前的数据隔离。当另一个事务提交后会导致当前事务看到的数据前后不一样。 未提交读 这就离谱了。啥也不能保证。 对于数据库事务的理解大家倒序建议就比较容易理解了。 BeginTransaction() 和 TransactionScope 的区别 在 C# Ado.net 中主要有两种事务使用方式 // 方式 1
using var tran await connection.BeginTransactionAsync();// 方式 2
using (TransactionScope transactionScope new TransactionScope())
{} BeginTransaction() 由 IDbConnection 连接对象开启只能作用于当前 IDbConnection 。通过调用数据库连接对象的 BeginTransaction() 方法显式地启动了一个数据库事务因此与同步方法异步方法不冲突。 TransactionScope 内部封装了一些 API在TransactionScope设置的范围内不需要显式地调用 Commit() 或 Rollback() 方法可以跨 IDbConnection 使用在异步方法下使用需要做额外配置。 主要区别在于 BeginTransaction() 是显式地管理事务而 TransactionScope 则是在编程模型上提供了更为方便的自动事务管理机制。 在 System.Transactions 命名空间中存在很多与事务相关的代码封装。读者可以自行了解
System.Transactions Namespace | Microsoft Learn 下面来详细说明两种事务开启方式的使用区别。 BeginTransaction() 先说 BeginTransaction() 其返回的是 DbTransaction 类型。 BeginTransaction() 开启事务比较简单不过需要手动给 IDbCommand 设置事务属性。 await connection.OpenAsync();// 先开启事务再创建命令using var tran await connection.BeginTransactionAsync();using var command new MySqlCommand(){Connection connection,// 注意这里Transaction tran};try{command.CommandText ... ...;await command.ExecuteNonQueryAsync();if(...){await tran.CommitAsync();}else{await tran.RollbackAsync();}}catch (Exception ex){await tran.RollbackAsync();logger.LogError(ex, Tran error);} BeginTransaction() 定义如下 ValueTaskMySqlTransaction BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken default) DbTransaction 还可以设置保存点。 using var tran await connection.BeginTransactionAsync();try{command.CommandText ... ...;await command.ExecuteNonQueryAsync();// 保存点await tran.SaveAsync(stepa);// 释放保存点、回滚到该保存点if(...){await tran.ReleaseAsync(stepa);}} BeginTransaction() 的使用比较简单也不太容易出错。 可以不手动撤销 很多时候我们会在 catch{} 回滚事务如下代码所示。 try{... ...await tran.CommitAsync();}catch (Exception ex){logger.LogError(ex, Tran error);await tran.RollbackAsync();} 实际上是当一个事务在 IDbConnection 中或者在此 IDbCommand 中没有主动提交时当对象生命周期结束或主动断开连接时、被回收到连接池时事务会自动回滚。只要没有主动提交则之前的操作皆无效。
比如我们执行下面的 SQL 时posts 表会被插入一条新的数据id 为 101。 -- 开启事务
BEGIN; -- 或者使用 START TRANSACTION;
INSERT INTO demo.posts (id, author_id, title, description, content, date)
VALUES (101, 1, 测试, 测试, 测试, 2023-12-08);
COMMIT ; 而执行以下代码时因为没有调用 CommitAsync() 方法提交事务因此程序结束后插入数据库的数据并不会起效。 using var connection dataSource.CreateConnection();await connection.OpenAsync();using var tran await connection.BeginTransactionAsync();using var command new MySqlCommand(){Connection connection,Transaction tran};try{command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (102, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();// await tran.CommitAsync();}catch (Exception ex){logger.LogError(ex, Tran error);} TransactionScope 如以下代码所示虽然代码执行不会报错但是其不受事务所控制也就是说虽然没有提交但是数据库实实在在的插入了一条新的数据。 这是因为事务完全没有起效因为只有在 TransactionScope 中打开的数据库连接才会起效。 using var connection dataSource.CreateConnection();await connection.OpenAsync();using (TransactionScope transactionScope new TransactionScope()){var command connection.CreateCommand();try{command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (103, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();//transactionScope.Complete();}catch (Exception ex){logger.LogError(ex, Tran error);}} 修正之后 using (TransactionScope transactionScope new TransactionScope()){using var connection dataSource.CreateConnection();await connection.OpenAsync();var command connection.CreateCommand();try{command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (104, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();//transactionScope.Complete();}catch (Exception ex){logger.LogError(ex, Tran error);}} 但是上面的代码还是会报错。这是因为 TransactionScope 默认不支持异步方法而该代码使用了异步导致释放时没有使用相同的线程。 System.InvalidOperationException:“A TransactionScope must be disposed on the same thread that it was created.” 当然TransactionScope 是支持异步的我们只需要启用配置即可。 using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){using var connection dataSource.CreateConnection();await connection.OpenAsync();var command connection.CreateCommand();try{command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (104, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();//transactionScope.Complete();}catch (Exception ex){logger.LogError(ex, Tran error);}} 如下代码所示当执行代码之后因为我们没有主动提交事务因此数据库中不会真的插入数据。 using (TransactionScope transactionScope // 使其支持异步new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){using var connection dataSource.CreateConnection();await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (105, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();//transactionScope.Complete();} 有了经验之后我们发现如果我们不调用 Complete() 方法那么数据库中不会真的插入数据。 可是问题来了因为是在 TransactionScope 中创建 IDbConnection 并打开连接也就是说 TransactionScope 作用域范围大于 IDbConnection 那么 IDbConnection 释放之后再提交 TransactionScope 是否可以 using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){using (var connection dataSource.CreateConnection()){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (105, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();}transactionScope.Complete();} 答案是一切正常。 简化代码如下所示 using (TransactionScope transactionScope ...){using (var connection dataSource.CreateConnection()){await connection.OpenAsync();await command.ExecuteNonQueryAsync();}transactionScope.Complete();} 虽然 IDbConnection 在 using 中transactionScope.Complete() 在 using 之外但是事务依然可以起效。如果调用 .Complete()则事务提交。如果不调用 .Complete() 则事务不会提交。 回到本小节第一个代码示例中事务不起效的问题。我们已经知道了是因为 IDbConnection 没有在 TransactionScope 内创建所以导致事务不能作用。 但是对于 ASP.NET Core 程序、Context 形式的 ORM、仓储形式的 ORM 等由于其封装在上下文内不太可能在开发者使用 TransactionScope 时再手动打开 IDbConnection.Open() 。不过这些 ORM 框架大多数都做了封装而本文末尾也介绍了几种封装方式。 总结 通过 BeginTransaction() 创建的事务不会因为异步等出现问题因为其是明确在一个 IDbCommand 、IDbConnection 中起效。 using var tran await connection.BeginTransactionAsync();using var command new MySqlCommand(){Connection connection,// 注意这里Transaction tran}; 所以说通过 .BeginTransactionAsync() 使用事务是最简单、最不容易出错的而且其明确在哪个 IDbCommand 中使用事情出现问题时排除起来也相对简单。 而对于 TransactionScope 来说笔者花费了比较多的篇幅去实验和解释TransactionScope 是使用事务作用域实现隐式事务的使用起来有一定难度也容易出错。 DML 是否可以使用事务 开始的时候笔者并没有想到这个事情在跟同事偶然吹水时提到了这个事情。 Mysql 的事务对删除表、创建表这些 DML 命令其事务是无效的起效的是表数据相关的操作即 insert、update、delete 语句。 如下 SQL 所示虽然回滚了事务但是最后还是创建了视图。 -- 开启事务
use demo;
BEGIN;
create view v_posts AS SELECT * FROM posts;
ROLLBACK;
-- COMMIT ; 顺序多操作 先从 TransactionScope 说起情况如下代码所示 TransactionScope 中包含、创建了两个 IDbConnection 并且两个 IDbConnection 都插入了数据。 也就是说使用 TransactionScope 同时管理多个 IDbConnection 。 using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){using (var connection dataSource.CreateConnection()){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (108, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();}using (var connection dataSource.CreateConnection()){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (109, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();}//transactionScope.Complete();} 这样是可以的TransactionScope 管理在期内的所有 IDbConnection让他们在当前的事务中保持一致。 但是 BeginTransaction() 是使用 IDbConnection.BeginTransaction() 创建的不能跨 IDbConnection 使用。 比如以下代码会报错 using var connection1 dataSource.CreateConnection();using var connection2 dataSource.CreateConnection();await connection1.OpenAsync();await connection2.OpenAsync();try{var tran1 connection1.BeginTransaction();var command1 connection1.CreateCommand();command1.Transaction tran1;var command2 connection2.CreateCommand();command2.Transaction tran1;command1.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (108, 1, 测试, 测试, 测试, 2023-12-08);;await command1.ExecuteNonQueryAsync();command2.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (108, 1, 测试, 测试, 测试, 2023-12-08);;await command2.ExecuteNonQueryAsync();tran1.Commit();}catch (Exception ex){logger.LogError(ex, Tran error);} 所以这里又有一个区别。 嵌套事务 .BeginTransaction() 不支持嵌套事务代码如下所示 static async Task Main(string[] args){using var connection dataSource.CreateConnection();await connection.OpenAsync();var tran connection.BeginTransaction();try{var command connection.CreateCommand();command.Transaction tran;command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (110, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();// 嵌套事务try{await InsertAsync(connection);}catch (Exception ex){logger.LogError(ex, Tran error.);await tran.RollbackAsync();return;}await tran.RollbackAsync();}catch (Exception ex){logger.LogError(ex, Tran error);}}// 嵌套的子事务private static async Task InsertAsync(MySqlConnection connection){var tran connection.BeginTransaction();var command connection.CreateCommand();command.Transaction tran;command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (112, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();await tran.CommitAsync();} 当一个 IDbConnection 调用两次 .BeginTransaction() 时代码会报错。 System.InvalidOperationException: Transactions may not be nested. 所以我们只能寄望于 TransactionScope。 使用 TransactionScope 做嵌套事务可以做到灵活的逻辑定制每个嵌套子事务都有自己的逻辑。 每个子事务只需要正常编写自己的 TransactionScope 即可即使子事务的 TransactionScope 已提交如果最外层的 TransactionScope 事务没有提交则所有的事务都不会提交。 如下代码所示 static async Task Main(string[] args){using var connection dataSource.CreateConnection();using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (110, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();// 嵌套事务try{await InsertAsync(connection);}catch (Exception ex){logger.LogError(ex, Tran error.);return;}// transactionScope.Complete();}}// 嵌套的子事务private static async Task InsertAsync(MySqlConnection connection){using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (112, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();transactionScope.Complete();}} 虽然 InsertAsync() 中的事务已经提交但是由于其受到外层 TransactionScope 事务的影响因此当外层事务不提交时子事务也不会提交。 当然即使不是同一个 IDbConnection 也是可以的。 static async Task Main(string[] args){using var connection dataSource.CreateConnection();using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (110, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();// 嵌套事务try{await InsertAsync();}catch (Exception ex){logger.LogError(ex, Tran error.);return;}// transactionScope.Complete();}}// 嵌套的子事务private static async Task InsertAsync(){using var connection dataSource.CreateConnection();using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){await connection.OpenAsync();var command connection.CreateCommand();command.CommandText INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (112, 1, 测试, 测试, 测试, 2023-12-08);;await command.ExecuteNonQueryAsync();transactionScope.Complete();}} 所以每个方法的代码只需要关注自己的逻辑即可。对于模块分离、职责分离的代码很有用。 事务范围 前面我们提到了 TransactionScope 的嵌套事务。 TransactionScope 对于嵌套事务的处理有一个 TransactionScopeOption 枚举配置。 public enum TransactionScopeOption{// 该范围需要一个事务。 如果已经存在环境事务则使用该环境事务。 否则在进入范围之前创建新的事务。 这是默认值。Required 0,// 总是为该范围创建新事务。RequiresNew 1,// 如果使用 Suppress 实例化范围则无论是否存在环境事务该范围都不会参与事务。使用此值实例化的范围始终将 null 作为其环境事务。Suppress 2} 使用示例 using(TransactionScope scope1 new TransactionScope())
{// 默认支持嵌套using(TransactionScope scope2 new TransactionScope(TransactionScopeOption.Required)){//...}// 不受 scope1 的影响using(TransactionScope scope3 new TransactionScope(TransactionScopeOption.RequiresNew)){//... }// 如果使用 Suppress 实例化范围则无论是否存在环境事务该范围都不会参与事务。using(TransactionScope scope4 new TransactionScope(TransactionScopeOption.Suppress)){//... }
} 对于嵌套事务作用域范围读者可以从这篇文章中了解更多Implementing an Implicit Transaction using Transaction Scope | Microsoft Learn 封装 DbContext 前面提到过IDbConnection 需要在 TransactionScope 中打开连接TransactionScope 才能管控其连接的事务。 不过有一些数据库驱动已经支持了 TransactionScope 即使不在其内打开链接也可以。比如 EFCore 框架EFCore 自动管理 IDbConnection 的生命周期因此我们往往不会手动管理连接因此事务事务时我们不太可能这样做 MyContext _context;using (TransactionScope transactionScope ...)
{_context.Connection.Open()
} 在使用数据库事务之前往往连接早就已经打开了。 MyContext _context;
_context.SelectAsync()....
_context.User.SectAsync()....
using (TransactionScope transactionScope ...)
{
} 所以我们需要封装一个上下文类型能够在连接打开后自动使用上下文的事务。 TransactionScope 封装一个数据库上下文执行命令时如果发现其在事务范围内则主动使用上下文事务。 public class DbContext{private readonly DbConnection _connection;public DbContext(DbConnection connection){_connection connection;}public async Task ExecuteAsync(string sql){var command _connection.CreateCommand();// 获取当前事务var tran Transaction.Current;if (tran ! null){// 注意这里。_connection.EnlistTransaction(tran);}command.CommandText sql;await command.ExecuteNonQueryAsync();}} 使用示例 using var connection dataSource.CreateConnection();
// 在之外打开await connection.OpenAsync();var context new DbContext(connection);using (TransactionScope transactionScope new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)){var sql INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (111, 1, 测试, 测试, 测试, 2023-12-08);;await context.ExecuteAsync(sql);} BeginTransaction() 使用上下文的形式封装 BeginTransaction() 开启的事务比较简单只需要手动维护 DbTransaction 即可。 public class DbContext{private readonly DbConnection _connection;private DbTransaction? _tran;public DbContext(MySqlConnection connection){_connection connection;}public async Task OpenTran(){if (_tran ! null) throw new Exception(请勿重复开启事务);_tran await _connection.BeginTransactionAsync();}public async Task ExecuteAsync(string sql){var command _connection.CreateCommand();command.CommandText sql;if (_tran ! null){command.Transaction _tran;}await command.ExecuteNonQueryAsync();}public async Task EndTran(){if (_tran null) throw new Exception(未开启事务);await _tran.CommitAsync();_tran.Dispose();_tran null;}} 使用方法 using var connection dataSource.CreateConnection();await connection.OpenAsync();DbContext context new DbContext(connection);await context.OpenTran();var sql INSERT INTO demo.posts (id, author_id, title, description, content, date) VALUES (111, 1, 测试, 测试, 测试, 2023-12-08);;await context.ExecuteAsync(sql); 当然由于不同的 ORM 封装的数据库事务方法不一样因此 ORM 的差异比较大。 文章转载自痴者工良
原文链接https://www.cnblogs.com/whuanle/p/17897778.html