国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù)

這篇具有很好參考價值的文章主要介紹了C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

在業(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ù)庫。

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

然后按照 authors、posts 的順序,點擊?Generate?,生成數(shù)據(jù)庫數(shù)據(jù)。

因為 posts 有 authors 的外鍵,因此生成數(shù)據(jù)的順序是 authors、posts。

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

最后點擊 Export database 導(dǎo)出 SQL 即可。

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

然后在數(shù)據(jù)庫中導(dǎo)入數(shù)據(jù)。

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

為了連接 Mysql 數(shù)據(jù)庫,這里使用 MySqlConnector 驅(qū)動,請在創(chuàng)建控制臺項目之后,通過 nuget 引入此包。

MySqlConnector 的主要部件和 API 如下:

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

使用同步方法可能會對托管線程池產(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ù)讀、幻讀等問題。

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

其實也不必糾結(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.”

C# 從代碼入門 Mysql 數(shù)據(jù)庫事務(wù),數(shù)據(jù)庫,c#,mysql

當(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)載自:癡者工良

原文鏈接: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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 【Mysql數(shù)據(jù)庫 第13章】MySQL的事務(wù)、事務(wù)的隔離級別、事務(wù)的保存點

    ??Spring中的創(chuàng)建對象的三種方式、第三方資源配置管理詳細描述及使用(XML版完結(jié)篇) ??Spring中的bean的配置、作用范圍、生命周期詳細描述及使用(XML版上篇) ??

    2023年04月20日
    瀏覽(21)
  • 簡單認識MySQL數(shù)據(jù)庫事務(wù)

    簡單認識MySQL數(shù)據(jù)庫事務(wù)

    MySQL 事務(wù)主要用于處理操作量大,復(fù)雜度高的數(shù)據(jù)。比如說,在戰(zhàn)艦登錄系統(tǒng)中, 要刪除一艘戰(zhàn)艦,即需要刪除戰(zhàn)艦的基本資料,又需要刪除和該戰(zhàn)艦相關(guān)的信息,如艦長, 登記船員等等。這樣,這些數(shù)據(jù)庫操作語句就構(gòu)成一個事務(wù)! ●事務(wù)是一種機制、一個操作序列,包

    2024年02月16日
    瀏覽(24)
  • 【MySQL數(shù)據(jù)庫 | 第十五篇】事務(wù)

    【MySQL數(shù)據(jù)庫 | 第十五篇】事務(wù)

    ? ? 目錄 ? ?前言: ?介紹事務(wù): ?控制事務(wù): ?事務(wù)四大特性: ?并發(fā)事務(wù)問題: ?事務(wù)隔離級別: 總結(jié): ? 這章我們將進入到MySQL基礎(chǔ)篇的最后一章:事務(wù),希望大家可以堅持下去,跟著我一起走完MySQL的學(xué)習(xí)之旅。 MySQL是一種關(guān)系型數(shù)據(jù)庫管理系統(tǒng),支持事務(wù)管理。 事

    2024年02月08日
    瀏覽(23)
  • Go 語言實現(xiàn) MySQL 數(shù)據(jù)庫事務(wù)

    MySQL事務(wù)是指一組數(shù)據(jù)庫操作,它們被視為一個邏輯單元,并且要么全部成功執(zhí)行,要么全部回滾(撤銷)。事務(wù)是數(shù)據(jù)庫管理系統(tǒng)提供的一種機制,用于確保數(shù)據(jù)的一致性和完整性。 事務(wù)具有以下特性(通常由ACID原則定義): 原子性(Atomicity):事務(wù)中的所有操作要么全

    2024年02月08日
    瀏覽(21)
  • 【Spring/MySQL數(shù)據(jù)庫系列】數(shù)據(jù)庫事務(wù)的特點與隔離級別

    【Spring/MySQL數(shù)據(jù)庫系列】數(shù)據(jù)庫事務(wù)的特點與隔離級別

    ?? 前面的話 ?? 本文已經(jīng)收錄到《Spring框架全家桶系列》專欄,本文將介紹有關(guān)數(shù)據(jù)庫事務(wù)的特點以及隔離級別。 ??博客主頁:未見花聞的博客主頁 ??歡迎關(guān)注??點贊??收藏??留言?? ??本文由 未見花聞 原創(chuàng), CSDN 首發(fā)! ??首發(fā)時間:??2023年5月20日?? ??堅

    2024年02月05日
    瀏覽(25)
  • 初識mysql數(shù)據(jù)庫之事務(wù)的概念及操作

    初識mysql數(shù)據(jù)庫之事務(wù)的概念及操作

    目錄 一、數(shù)據(jù)庫多客戶端訪問問題 1. 數(shù)據(jù)庫的CURD無限制帶來的問題 2. 如何解決CURD導(dǎo)致的問題 二、事務(wù)的概念 1. 什么是事務(wù) 2. 事務(wù)的四個屬性 3. mysql對事務(wù)的管理 4. 為什么會有事務(wù) 5. 事務(wù)的版本支持 三、事務(wù)的操作 1. 事務(wù)提交方式 2. 事務(wù)操作的準備工作 2.1 數(shù)據(jù)庫是網(wǎng)

    2024年02月15日
    瀏覽(25)
  • 【MySQL】一文帶你了解數(shù)據(jù)庫索引與事務(wù)

    數(shù)據(jù)庫索引是一種提高數(shù)據(jù)庫查詢效率的數(shù)據(jù)結(jié)構(gòu)。它可以快速地定位和訪問數(shù)據(jù)庫中的數(shù)據(jù),從而大大提高數(shù)據(jù)庫查詢的速度和效率。數(shù)據(jù)庫索引可以根據(jù)不同的查詢需求構(gòu)造多個索引,以最大化提高查詢效率。 數(shù)據(jù)庫索引基于各種字段來創(chuàng)建,在查詢時可以通過索引直接

    2024年02月09日
    瀏覽(82)
  • 【后端面經(jīng)-數(shù)據(jù)庫】MySQL的事務(wù)隔離級別簡介

    目錄 0. 事務(wù)的概念 1. 三類問題 2. 事務(wù)隔離級別 3. 操作指令 4. 總結(jié) 5. 參考博文 事務(wù)指的是一連串的集中操作指令,一個事務(wù)的執(zhí)行必須執(zhí)行完所有的動作才能算作執(zhí)行結(jié)束。事務(wù)具有四個特點,簡記作 ACID : A -Atomicity: 原子性,事務(wù)的執(zhí)行必須保證所有的動作都執(zhí)行完畢;

    2024年02月08日
    瀏覽(32)
  • MySQL:事務(wù)、索引、用戶管理、備份、數(shù)據(jù)庫設(shè)計(三大范式)

    MySQL:事務(wù)、索引、用戶管理、備份、數(shù)據(jù)庫設(shè)計(三大范式)

    事務(wù) (transaction):要么都成功,要么都失敗。 核心 :將一組 SQL 放在一個批次中去執(zhí)行。 原則 ACID :原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability)。 原子性 :一個事務(wù)中的所有步驟 要么都 成功, 要么都 失敗,不能只成功一個步驟。 一致性 :包括

    2023年04月26日
    瀏覽(21)
  • elasticsearch的查詢方式和mysql數(shù)據(jù)庫事務(wù)隔離級別的思考

    elasticsearch的查詢方式和mysql數(shù)據(jù)庫事務(wù)隔離級別的思考

    目錄 普通分頁 解除查詢限制 scroll查詢 search_after 官方改進 輕量級試圖(pit,Point in time) 總結(jié) 項目中用到了?elasticsearch,發(fā)現(xiàn)有幾種查詢方式不太一樣,思考了一下,總結(jié)如下 等同于關(guān)系數(shù)據(jù)庫的分頁查詢,例如 mysql 的 limit,如下 sql 這種查詢方式有一個問題,需要查詢

    2024年01月18日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包