在業(yè)務(wù)開發(fā)中,使用數(shù)據(jù)庫事務(wù)是必不可少的。而開發(fā)中往往會使用各種 ORM 執(zhí)行數(shù)據(jù)庫操作,簡化代碼復(fù)雜度,不過,由于各種 ORM 的封裝特性,開發(fā)者的使用方式也不一樣,開發(fā)者想要了解 ORM 對事務(wù)做了什么處理是比較難的。因此,本文介紹數(shù)據(jù)庫事務(wù)基礎(chǔ)、Ado.net 事務(wù)、如何封裝 DbContext ,讀者掌握以后,可以加深對 C# 使用事務(wù)的理解,使用各種 ORM 時也會更應(yīng)手。
生成數(shù)據(jù)庫數(shù)據(jù)
為了演示各種事務(wù)操作,我們想要先創(chuàng)建 demo 數(shù)據(jù),打開 filldb 官網(wǎng),根據(jù)操作提示生成模擬數(shù)據(jù)。
filldb 地址:?Step 1: Upload Database schema
FillDB 是一款免費工具,可快速生成大量 MySql 格式的自定義數(shù)據(jù),用于測試軟件和使用隨機數(shù)據(jù)填充數(shù)據(jù)庫。
然后按照 authors、posts 的順序,點擊?Generate
?,生成數(shù)據(jù)庫數(shù)據(jù)。
因為 posts 有 authors 的外鍵,因此生成數(shù)據(jù)的順序是 authors、posts。
最后點擊 Export database 導(dǎo)出 SQL 即可。
然后在數(shù)據(jù)庫中導(dǎo)入數(shù)據(jù)。
為了連接 Mysql 數(shù)據(jù)庫,這里使用 MySqlConnector 驅(qū)動,請在創(chuàng)建控制臺項目之后,通過 nuget 引入此包。
MySqlConnector 的主要部件和 API 如下:
使用同步方法可能會對托管線程池產(chǎn)生不利影響,如果沒有正確調(diào)優(yōu),還會導(dǎo)致速度減慢或鎖定。
Mysql 連接字符串配置示例:
const string connectionString = "Server=localhost;Port=3306;User ID=mysqltest;Password=Password123;Database=mysqldb";
或使用 MySqlConnectionStringBuilder 構(gòu)建連接字符串:
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 包如下:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="MySqlConnector" Version="2.3.1" />
<PackageReference Include="MySqlConnector.Logging.Microsoft.Extensions.Logging" Version="2.1.0" />
</ItemGroup>
配置連接字符串、配置日志、創(chuàng)建數(shù)據(jù)庫連接,完整代碼示例如下:
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<Program>();
var dataSourceBuilder = new MySqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseLoggerFactory(loggerFactory);
await using var dataSource = dataSourceBuilder.Build();
using var connection = dataSource.CreateConnection();
經(jīng)過以上配置之后,我們擁有了模擬數(shù)據(jù)庫以及基礎(chǔ)代碼,下面我們來正式學(xué)習(xí) MysqlConnetor 和數(shù)據(jù)庫事務(wù)相關(guān)的知識。
Mysql 數(shù)據(jù)庫事務(wù)基礎(chǔ)
百度百科:數(shù)據(jù)庫事務(wù)( transaction)是訪問并可能操作各種數(shù)據(jù)項的一個數(shù)據(jù)庫操作序列,這些操作要么全部執(zhí)行,要么全部不執(zhí)行,是一個不可分割的工作單位。事務(wù)由事務(wù)開始與事務(wù)結(jié)束之間執(zhí)行的全部數(shù)據(jù)庫操作組成。
數(shù)據(jù)庫事務(wù)有四個特性:
-
原子性:原子性是指包含事務(wù)的操作要么全部執(zhí)行成功,要么全部失敗回滾。
-
一致性:一致性指事務(wù)在執(zhí)行前后狀態(tài)是一致的。
-
隔離性:一個事務(wù)所進行的修改在最終提交之前,對其他事務(wù)是不可見的。
-
持久性:數(shù)據(jù)一旦提交,其所作的修改將永久地保存到數(shù)據(jù)庫中。
相信大家對數(shù)據(jù)庫事務(wù)都不陌生,因此這里就不扯淡了,下面來講解不同數(shù)據(jù)庫事務(wù)的特征。
數(shù)據(jù)庫的并發(fā)一致性問題
雖然數(shù)據(jù)庫事務(wù)可以幫助我們執(zhí)行數(shù)據(jù)庫操作、回滾操作,但是數(shù)據(jù)庫事務(wù)并發(fā)執(zhí)行時,事務(wù)之間可能會相互干擾,比如臟讀、幻讀等現(xiàn)象,我們使用數(shù)據(jù)庫事務(wù)時,要根據(jù)嚴格程度和性能之間相互平衡選擇事務(wù)隔離級別。
當(dāng)多個事務(wù)并發(fā)執(zhí)行時,可能會出現(xiàn)以下問題:
臟讀
? 事務(wù) A 更新了數(shù)據(jù),但還沒有提交,這時事務(wù) B 讀取到事務(wù) A 更新后的數(shù)據(jù),然后事務(wù) A 回滾了,事務(wù) B 讀取到的數(shù)據(jù)就成為臟數(shù)據(jù)了。
不可重復(fù)讀
? 事務(wù) A 對數(shù)據(jù)進行多次讀取,事務(wù) B 在事務(wù) A 多次讀取的過程中執(zhí)行了更新操作并提交了,導(dǎo)致事務(wù) A 多次讀取到的數(shù)據(jù)并不一致。
不可重復(fù)讀,特征是相同的數(shù)據(jù),在事務(wù) A 的不同階段讀取的數(shù)據(jù)不一樣。
幻讀
事務(wù) A 在讀取數(shù)據(jù)后,事務(wù) B 向事務(wù)A讀取的數(shù)據(jù)中插入了幾條數(shù)據(jù),事務(wù) A 再次讀取數(shù)據(jù)時發(fā)現(xiàn)多了幾條數(shù)據(jù),和之前讀取的數(shù)據(jù)不一致。
幻讀,前后數(shù)據(jù)量不一樣。
丟失修改
事務(wù) A 和事務(wù) B 都對同一個數(shù)據(jù)進行修改,事務(wù) A 先修改,事務(wù) B 隨后修改,事務(wù) B 的修改覆蓋了事務(wù) A 的修改。
不可重復(fù)度和幻讀看起來比較像,它們主要的區(qū)別是:在不可重復(fù)讀中,發(fā)現(xiàn)數(shù)據(jù)不一致主要是數(shù)據(jù)被更新了。在幻讀中,發(fā)現(xiàn)數(shù)據(jù)不一致主要是數(shù)據(jù)增多或者減少了。
數(shù)據(jù)庫事務(wù)的隔離級別
數(shù)據(jù)庫事務(wù)的隔離級別有以下四種,按隔離級別從低到高:
-
未提交讀:一個事務(wù)在提交前,它的修改對其他事務(wù)也是可見的。
-
提交讀:一個事務(wù)提交之后,它的修改才能被其他事務(wù)看到。
-
可重復(fù)讀:在同一個事務(wù)中多次讀取到的數(shù)據(jù)是一致的。
-
串行化:需要加鎖實現(xiàn),會強制事務(wù)串行執(zhí)行。
Ado.net 中使用?System.Data.IsolationLevel
?枚舉表示以上幾種數(shù)據(jù)庫事務(wù)隔離級別:
public enum IsolationLevel
{
// 未指定
Unspecified = -1,
// 不能覆蓋來自更高度隔離的事務(wù)的掛起的更改。
Chaos = 16,
// 未提交讀,臟讀是可能的,這意味著不會發(fā)出共享鎖,也不會使用獨占鎖。
ReadUncommitted = 256,
// 提交讀,在讀取數(shù)據(jù)時持有共享鎖,以避免臟讀,但是數(shù)據(jù)可以在事務(wù)結(jié)束之前更改,從而導(dǎo)致不可重復(fù)讀取或幻像數(shù)據(jù)。
ReadCommitted = 4096,
// 可重復(fù)讀,鎖被放置在查詢中使用的所有數(shù)據(jù)上,防止其他用戶更新數(shù)據(jù)。防止不可重復(fù)讀取,但仍然可以使用幻像行。
RepeatableRead = 65536,
// 串行化,將在 DataSet 上放置一個范圍鎖,以防止其他用戶在事務(wù)完成之前更新數(shù)據(jù)集或?qū)⑿胁迦霐?shù)據(jù)集。
Serializable = 1048576,
// 通過存儲一個應(yīng)用程序可以讀取而另一個應(yīng)用程序正在修改相同數(shù)據(jù)的數(shù)據(jù)版本來減少阻塞。
// 指示即使重新查詢,也無法從一個事務(wù)中看到在其他事務(wù)中所做的更改。
Snapshot = 16777216
}
數(shù)據(jù)庫的隔離級別分別可以解決數(shù)據(jù)庫的臟讀、不可重復(fù)讀、幻讀等問題。
其實也不必糾結(jié)這些問題,可以按照讀寫鎖的情況來理解。
編程中由于多個線程并發(fā)操作兩個字典:
Dictionary<string, string> a;
Dictionary<string, string> b;
第一個問題時,并發(fā)操作一個字典時,會出現(xiàn)線程并發(fā)異常。
所以,我們想要使用并發(fā)字典:
ConcurrentDictionary<string, string> a;
ConcurrentDictionary<string, string> b;
可是,當(dāng) T1 線程修改 a 完成,接著修改 b 時,線程 T2 把字典 a 修改了。這就導(dǎo)致了數(shù)據(jù)不一致。
使用讀寫鎖優(yōu)化,將 a、b 兩個數(shù)據(jù)包在一起:
ConcurrentDictionary<string, string> a;
ConcurrentDictionary<string, 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();
}
}
讀寫鎖的原理很簡單,讀和寫是兩個沖突的操作。當(dāng)沒有線程?寫
?時,多個線程可以并發(fā)?讀
,此時不會有任何問題。當(dāng)有一個線程?寫
?時,既不允許有其它線程同時在?寫
?,也不允許其它線程同時在?讀
。也就是說,讀
?是可以并發(fā)的,但是寫是獨占的。
串行化
:
當(dāng)然對于數(shù)據(jù)庫事務(wù)就復(fù)雜了很多。如果要按照讀寫鎖的形式去做,那么其隔離級別相當(dāng)于?串行化
,整個表都被鎖住,不允許事務(wù)并發(fā)執(zhí)行,此時不會有?臟讀
、不可重復(fù)讀
、?幻讀
?這些情況。
可是,這樣對于數(shù)據(jù)庫來說壓力是很大的,會嚴重拖垮數(shù)據(jù)庫的性能,以及嚴重降低了業(yè)務(wù)程序的并發(fā)量。
當(dāng)事務(wù) A 只需要修改?id=1,2,3
?的數(shù)據(jù)時,使用?串行化
?級別,會鎖住整個表。這樣似乎有點太浪費了。
可重復(fù)讀
:
那么,我們只需要鎖住事務(wù) A 正在修改的那幾行記錄不就行了嗎?那么我們把數(shù)據(jù)庫事務(wù)下降一個級別,使用?可重復(fù)讀
。
使用?可重復(fù)讀
?事務(wù)級別,其被鎖住的數(shù)據(jù),依然保持安全,也就是不會被其它事務(wù)所修改。所以,不會出現(xiàn)?臟讀
、不可重復(fù)讀
。但是因為不是鎖住整個表,因此其它事務(wù)是可以插入數(shù)據(jù)的,這就導(dǎo)致了會出現(xiàn)?幻讀
。當(dāng)然,可重復(fù)讀
?出現(xiàn)的問題,一般來說只需要保證事務(wù)中只處理自己想要的數(shù)據(jù)即可。
可重復(fù)讀
?導(dǎo)致的?幻讀
?問題,比如 A 事務(wù)在?筆記本
?分類下給聯(lián)想筆記本型號都打 9 折優(yōu)惠,可是此時 B 事務(wù)從?筆記本
?分類下,增加了幾個理想筆記本型號。結(jié)果,事務(wù) A 最后一查詢,把 B 事務(wù)插入的數(shù)據(jù)查詢出來了。那么事務(wù) A 查詢的數(shù)據(jù)就包含了打折和未打折的數(shù)據(jù)了。
InnoDB 使用 MVCC 來實現(xiàn)高并發(fā)性,并實現(xiàn)了所有 4 個SQL標準隔離級別。InnoDB 默認為 REPEATABLE READ (可重復(fù)讀)隔離級別,并且通過間隙鎖(next-key locking)策略來防止在這個隔離級別上的幻讀。InnoDB 不只鎖定在查詢中涉及的行,還會對索引結(jié)構(gòu)中的間隙進行鎖定,以防止幻行被插入。
提交讀
:
使用示例:
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 事務(wù)和 B 事務(wù)運行時,大家都對 name 做了修改,但是事務(wù)只能看到自己做出的修改,也就是說,B 事務(wù)未提交之前,A、B 都修改了數(shù)據(jù),但是是隔離的。
A 事務(wù)修改了?name = A
?,B 事務(wù)修改了?name = B
?,未提交之前,A、B 事務(wù)讀到的分別是 A、B,這沒問題,不會干擾。
但是如果 A 先提交了事務(wù),那么數(shù)據(jù)庫的 name 值就為 A,此時 B 事務(wù)還沒有提交,B 查詢到的?name = A
,這就是不可重復(fù)讀。
提交讀
?只能保證事務(wù)未提交前的數(shù)據(jù)隔離。當(dāng)另一個事務(wù)提交后,會導(dǎo)致當(dāng)前事務(wù)看到的數(shù)據(jù)前后不一樣。
未提交讀
:
這就離譜了。啥也不能保證。
對于數(shù)據(jù)庫事務(wù)的理解,大家倒序建議就比較容易理解了。
BeginTransaction()?和 TransactionScope 的區(qū)別
在 C# Ado.net 中,主要有兩種事務(wù)使用方式:
// 方式 1:
using var tran = await connection.BeginTransactionAsync();
// 方式 2:
using (TransactionScope transactionScope = new TransactionScope())
{
}
BeginTransaction()
?由 IDbConnection 連接對象開啟,只能作用于當(dāng)前 IDbConnection 。通過調(diào)用數(shù)據(jù)庫連接對象的?BeginTransaction()
?方法,顯式地啟動了一個數(shù)據(jù)庫事務(wù),因此與同步方法異步方法不沖突。
TransactionScope
?內(nèi)部封裝了一些 API,在TransactionScope
設(shè)置的范圍內(nèi),不需要顯式地調(diào)用?Commit()
?或?Rollback()
?方法,可以跨 IDbConnection 使用,在異步方法下使用需要做額外配置。
主要區(qū)別在于?BeginTransaction()
?是顯式地管理事務(wù),而?TransactionScope
?則是在編程模型上提供了更為方便的自動事務(wù)管理機制。
在 System.Transactions 命名空間中存在很多與事務(wù)相關(guān)的代碼封裝。讀者可以自行了解:
System.Transactions Namespace | Microsoft Learn
下面來詳細說明兩種事務(wù)開啟方式的使用區(qū)別。
BeginTransaction()
先說?BeginTransaction()
?,其返回的是 DbTransaction 類型。
BeginTransaction()
?開啟事務(wù)比較簡單,不過需要手動給 IDbCommand 設(shè)置事務(wù)屬性。
await connection.OpenAsync();
// 先開啟事務(wù),再創(chuàng)建命令
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()
?定義如下:
ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel,
CancellationToken cancellationToken = default)
DbTransaction 還可以設(shè)置保存點。
using var tran = await connection.BeginTransactionAsync();
try
{
command.CommandText = "... ...";
await command.ExecuteNonQueryAsync();
// 保存點
await tran.SaveAsync("stepa");
// 釋放保存點、回滾到該保存點
if(...)
{
await tran.ReleaseAsync("stepa");
}
}
BeginTransaction()
?的使用比較簡單,也不太容易出錯。
可以不手動撤銷
很多時候我們會在?catch{}
?回滾事務(wù),如下代碼所示。
try
{
... ...
await tran.CommitAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Tran error");
await tran.RollbackAsync();
}
實際上是當(dāng)一個事務(wù)在 IDbConnection 中或者在此 IDbCommand 中沒有主動提交時,當(dāng)對象生命周期結(jié)束或主動斷開連接時、被回收到連接池時,事務(wù)會自動回滾。只要沒有主動提交,則之前的操作皆無效。
比如,我們執(zhí)行下面的 SQL 時,posts 表會被插入一條新的數(shù)據(jù),id 為 101。
-- 開啟事務(wù)
BEGIN; -- 或者使用 START TRANSACTION;
INSERT INTO demo.posts (id, author_id, title, description, content, date)
VALUES (101, 1, '測試', '測試', '測試', '2023-12-08');
COMMIT ;
而執(zhí)行以下代碼時,因為沒有調(diào)用?CommitAsync()
?方法提交事務(wù),因此程序結(jié)束后,插入數(shù)據(jù)庫的數(shù)據(jù)并不會起效。
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
如以下代碼所示,雖然代碼執(zhí)行不會報錯,但是其不受事務(wù)所控制,也就是說,雖然沒有提交,但是數(shù)據(jù)庫實實在在的插入了一條新的數(shù)據(jù)。
這是因為事務(wù)完全沒有起效,因為只有在 TransactionScope 中打開的數(shù)據(jù)庫連接,才會起效。
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 默認不支持異步方法,而該代碼使用了異步,導(dǎo)致釋放時沒有使用相同的線程。
System.InvalidOperationException:“A TransactionScope must be disposed on the same thread that it was created.”
當(dāng)然,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");
}
}
如下代碼所示,當(dāng)執(zhí)行代碼之后,因為我們沒有主動提交事務(wù),因此,數(shù)據(jù)庫中不會真的插入數(shù)據(jù)。
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();
}
有了經(jīng)驗之后,我們發(fā)現(xiàn),如果我們不調(diào)用?Complete()
?方法,那么數(shù)據(jù)庫中不會真的插入數(shù)據(jù)。
可是問題來了,因為是在 TransactionScope 中創(chuàng)建 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 之外,但是事務(wù)依然可以起效。如果調(diào)用?.Complete()
,則事務(wù)提交。如果不調(diào)用?.Complete()
?則事務(wù)不會提交。
回到本小節(jié)第一個代碼示例中,事務(wù)不起效的問題。我們已經(jīng)知道了是因為 IDbConnection 沒有在 TransactionScope 內(nèi)創(chuàng)建,所以導(dǎo)致事務(wù)不能作用。
但是,對于 ASP.NET Core 程序、Context 形式的 ORM、倉儲形式的 ORM 等,由于其封裝在上下文內(nèi),不太可能在開發(fā)者使用 TransactionScope 時,再手動打開?IDbConnection.Open()
?。不過這些 ORM 框架大多數(shù)都做了封裝,而本文末尾也介紹了幾種封裝方式。
總結(jié)
通過?BeginTransaction()
?創(chuàng)建的事務(wù),不會因為異步等出現(xiàn)問題,因為其是明確在一個 IDbCommand 、IDbConnection 中起效。
using var tran = await connection.BeginTransactionAsync();
using var command = new MySqlCommand()
{
Connection = connection,
// 注意這里
Transaction = tran
};
所以說,通過?.BeginTransactionAsync()
?使用事務(wù),是最簡單、最不容易出錯的,而且其明確在哪個 IDbCommand 中使用事情,出現(xiàn)問題時,排除起來也相對簡單。
而對于 TransactionScope 來說,筆者花費了比較多的篇幅去實驗和解釋,TransactionScope 是使用事務(wù)作用域?qū)崿F(xiàn)隱式事務(wù)的,使用起來有一定難度,也容易出錯。
DML 是否可以使用事務(wù)
開始的時候,筆者并沒有想到這個事情,在跟同事偶然吹水時,提到了這個事情。
Mysql 的事務(wù)對刪除表、創(chuàng)建表這些 DML 命令,其事務(wù)是無效的,起效的是表數(shù)據(jù)相關(guān)的操作,即?insert、update、delete?語句。
如下 SQL 所示,雖然回滾了事務(wù),但是最后還是創(chuàng)建了視圖。
-- 開啟事務(wù)
use demo;
BEGIN;
create view v_posts AS SELECT * FROM posts;
ROLLBACK;
-- COMMIT ;
順序多操作
先從 TransactionScope 說起,情況如下代碼所示:
TransactionScope 中包含、創(chuàng)建了兩個 IDbConnection ,并且兩個 IDbConnection 都插入了數(shù)據(jù)。
也就是說使用 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 管理在期內(nèi)的所有 IDbConnection,讓他們在當(dāng)前的事務(wù)中保持一致。
但是?BeginTransaction()
?是使用?IDbConnection.BeginTransaction()
?創(chuàng)建的,不能跨 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");
}
所以,這里又有一個區(qū)別。
嵌套事務(wù)
.BeginTransaction()
?不支持嵌套事務(wù),代碼如下所示:
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();
// 嵌套事務(wù)
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");
}
}
// 嵌套的子事務(wù)
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();
}
當(dāng)一個 IDbConnection 調(diào)用兩次?.BeginTransaction()
?時,代碼會報錯。
System.InvalidOperationException: Transactions may not be nested.
所以,我們只能寄望于 TransactionScope。
使用 TransactionScope 做嵌套事務(wù),可以做到靈活的邏輯定制,每個嵌套子事務(wù)都有自己的邏輯。
每個子事務(wù)只需要正常編寫自己的 TransactionScope 即可,即使子事務(wù)的 TransactionScope 已提交,如果最外層的 TransactionScope 事務(wù)沒有提交,則所有的事務(wù)都不會提交。
如下代碼所示:
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();
// 嵌套事務(wù)
try
{
await InsertAsync(connection);
}
catch (Exception ex)
{
logger.LogError(ex, "Tran error.");
return;
}
// transactionScope.Complete();
}
}
// 嵌套的子事務(wù)
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()
?中的事務(wù)已經(jīng)提交,但是由于其受到外層 TransactionScope 事務(wù)的影響,因此當(dāng)外層事務(wù)不提交時,子事務(wù)也不會提交。
當(dāng)然,即使不是同一個 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();
// 嵌套事務(wù)
try
{
await InsertAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Tran error.");
return;
}
// transactionScope.Complete();
}
}
// 嵌套的子事務(wù)
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();
}
}
所以,每個方法的代碼,只需要關(guān)注自己的邏輯即可。對于模塊分離、職責(zé)分離的代碼很有用。
事務(wù)范圍
前面我們提到了 TransactionScope 的嵌套事務(wù)。
TransactionScope 對于嵌套事務(wù)的處理,有一個 TransactionScopeOption 枚舉配置。
public enum TransactionScopeOption
{
// 該范圍需要一個事務(wù)。 如果已經(jīng)存在環(huán)境事務(wù),則使用該環(huán)境事務(wù)。 否則,在進入范圍之前創(chuàng)建新的事務(wù)。 這是默認值。
Required = 0,
// 總是為該范圍創(chuàng)建新事務(wù)。
RequiresNew = 1,
// 如果使用 Suppress 實例化范圍,則無論是否存在環(huán)境事務(wù),該范圍都不會參與事務(wù)。使用此值實例化的范圍始終將 null 作為其環(huán)境事務(wù)。
Suppress = 2
}
使用示例:
using(TransactionScope scope1 = new TransactionScope())
{
// 默認支持嵌套
using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
{
//...
}
// 不受 scope1 的影響
using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
{
//...
}
// 如果使用 Suppress 實例化范圍,則無論是否存在環(huán)境事務(wù),該范圍都不會參與事務(wù)。
using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
{
//...
}
}
對于嵌套事務(wù)作用域范圍,讀者可以從這篇文章中了解更多:Implementing an Implicit Transaction using Transaction Scope | Microsoft Learn
封裝 DbContext
前面提到過,IDbConnection 需要在 TransactionScope 中打開連接,TransactionScope 才能管控其連接的事務(wù)。
不過,有一些數(shù)據(jù)庫驅(qū)動已經(jīng)支持了 TransactionScope ,即使不在其內(nèi)打開鏈接也可以。比如 EFCore 框架,EFCore 自動管理 IDbConnection 的生命周期,因此我們往往不會手動管理連接,因此事務(wù)事務(wù)時,我們不太可能這樣做:
MyContext _context;
using (TransactionScope transactionScope = ...)
{
_context.Connection.Open()
}
在使用數(shù)據(jù)庫事務(wù)之前,往往連接早就已經(jīng)打開了。
MyContext _context;
_context.SelectAsync()....
_context.User.SectAsync()....
using (TransactionScope transactionScope = ...)
{
}
所以,我們需要封裝一個上下文類型,能夠在連接打開后,自動使用上下文的事務(wù)。
TransactionScope
封裝一個數(shù)據(jù)庫上下文,執(zhí)行命令時,如果發(fā)現(xiàn)其在事務(wù)范圍內(nèi),則主動使用上下文事務(wù)。
public class DbContext
{
private readonly DbConnection _connection;
public DbContext(DbConnection connection)
{
_connection = connection;
}
public async Task ExecuteAsync(string sql)
{
var command = _connection.CreateCommand();
// 獲取當(dāng)前事務(wù)
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()
?開啟的事務(wù)比較簡單,只需要手動維護 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("請勿重復(fù)開啟事務(wù)");
_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("未開啟事務(wù)");
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);
當(dāng)然,由于不同的 ORM 封裝的數(shù)據(jù)庫事務(wù)方法不一樣,因此 ORM 的差異比較大。
文章轉(zhuǎn)載自:癡者工良文章來源:http://www.zghlxwxcb.cn/news/detail-757574.html
原文鏈接:https://www.cnblogs.com/whuanle/p/17897778.html文章來源地址http://www.zghlxwxcb.cn/news/detail-757574.html
到了這里,關(guān)于C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!