基於 EntityFramework 的數據庫主從讀寫分離架構(2)- 改進配置和添加事務支持


     回到目錄,完整代碼請查看https://github.com/cjw0511/NDF.Infrastructure)中的目錄:
     src\ NDF.Data.EntityFramework\MasterSlaves
 
    上一回中( http://www.cnblogs.com/cjw0511/p/4398267.html),我們簡單講述了基於 EF 來實現數據庫讀寫分離的原理。當然,這只是一個 demo 級別的簡單實現,實際上,在我們工作環境中,碰到的情況遠比這復雜多了,例如數據庫連接的配置是通過 config 文件來存儲、在進行數據庫操作時還需要附帶很多事務操作功能等等。今天我們就來聊聊如何處理這些問題。
    
首先,我們來解決數據庫連接字符串存儲與配置文件的問題
     代碼如下:
   
 1  public class DbMasterSlaveCommandInterceptor : DbCommandInterceptor
 2     {
 3         private Lazy<string> masterConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["masterConnectionString"]);
 4         private Lazy<string> slaveConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["slaveConnectionString"]);
 5  
 6         public string MasterConnectionString
 7         {
 8             get { return this.masterConnectionString.Value; }
 9         }
10  
11         public string SlaveConnectionString
12         {
13             get { return this.slaveConnectionString.Value; }
14         }
15  
16  
17         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
18         {
19             this.UpdateConnectionStringIfNeed(interceptionContext, this.SlaveConnectionString);
20         }
21  
22         public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
23         {
24             this.UpdateConnectionStringIfNeed(interceptionContext, this.SlaveConnectionString);
25         }
26  
27         public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
28         {
29             this.UpdateConnectionStringIfNeed(interceptionContext, this.MasterConnectionString);
30         }
31  
32  
33         private void UpdateConnectionStringIfNeed(DbInterceptionContext interceptionContext, string connectionString)
34         {
35             foreach (var context in interceptionContext.DbContexts)
36             {
37                 this.UpdateConnectionStringIfNeed(context.Database.Connection, connectionString);
38             }
39         }
40  
41         /// <summary>
42         /// 此處改進了對連接字符串的修改判斷機制,確認只在 <paramref name="conn"/> 所使用的連接字符串不等效於 <paramref name="connectionString"/> 的情況下才需要修改。
43         /// </summary>
44         /// <param name="conn"></param>
45         /// <param name="connectionString"></param>
46         private void UpdateConnectionStringIfNeed(DbConnection conn, string connectionString)
47         {
48             if (this.ConnectionStringCompare(conn, connectionString))
49             {
50                 ConnectionState state = conn.State;
51                 if (state == ConnectionState.Open)
52                     conn.Close();
53  
54                 conn.ConnectionString = connectionString;
55  
56                 if (state == ConnectionState.Open)
57                     conn.Open();
58             }
59         }
60  
61         private bool ConnectionStringCompare(DbConnection conn, string connectionString)
62         {
63             DbProviderFactory factory = DbProviderFactories.GetFactory(conn);
64  
65             DbConnectionStringBuilder a = factory.CreateConnectionStringBuilder();
66             a.ConnectionString = conn.ConnectionString;
67  
68             DbConnectionStringBuilder b = factory.CreateConnectionStringBuilder();
69             b.ConnectionString = connectionString;
70  
71             return a.EquivalentTo(b);
72         }
73     }

 

再者,我們來聊聊數據庫操作中的事務處理。

    我們都知道,數據庫操作中的事務處理重要包括兩大類:
    1、普通數據庫操作事務處理,該類型由 DbTransaction 事務基類來控制;
    2、分布式事務,這類操作主要由組件 System.Transactions 來控制,最常用的類型包括 Transaction 和 TransactionScope。
    
    具體涉及到普通數據庫事務和分布式事務的意義和區別、普通事務如何會提升為分布式事務等知識點,這里就不贅述了,有興趣的同學可以另行補課。
    這里需要說明的是,在數據庫的事務操作中,很多 dbms 是不支持同一個事務操作不同的數據庫或服務器的。另外某些 dbms 支持同一個事務操作多個數據庫或服務器(自動提升為分布式事務),但是需要 msdtc 的支持。
    
    所以在這里,我改進的方案是,凡是所有的事務操作,不管是普通數據庫事務,還是分布式事務,都“禁用”讀寫分離,即將所有的在事務內的數據庫操作(不管是讀還是寫,雖然這一定程度上不符合“完全的讀寫分離”的本意,但是解決了數據庫事務兼容性的問題,而且大多數項目開發中,包含事務的操作不占多數),都指向 Master 服務器。實際上基於我們前面對數據庫服務器連接字符串的封裝,要實現這一點,只需要改動少量代碼,如下:
 1 public class DbMasterSlaveCommandInterceptor : DbCommandInterceptor
 2     {
 3         private Lazy<string> masterConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["masterConnectionString"]);
 4         private Lazy<string> slaveConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["slaveConnectionString"]);
 5  
 6         public string MasterConnectionString
 7         {
 8             get { return this.masterConnectionString.Value; }
 9         }
10  
11         public string SlaveConnectionString
12         {
13             get { return this.slaveConnectionString.Value; }
14         }
15  
16  
17         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
18         {
19             this.UpdateToSlave(interceptionContext);
20         }
21  
22         public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
23         {
24             this.UpdateToSlave(interceptionContext);
25         }
26  
27         public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
28         {
29             this.UpdateToMaster(interceptionContext);
30         }
31  
32  
33         private void UpdateToMaster(DbInterceptionContext interceptionContext)
34         {
35             foreach (var context in interceptionContext.DbContexts)
36             {
37                 this.UpdateConnectionStringIfNeed(context.Database.Connection, this.MasterConnectionString);
38             }
39         }
40  
41         private void UpdateToSlave(DbInterceptionContext interceptionContext)
42         {
43             // 判斷當前會話是否處於分布式事務中
44             bool isDistributedTran = Transaction.Current != null && Transaction.Current.TransactionInformation.Status != TransactionStatus.Committed;
45             foreach (var context in interceptionContext.DbContexts)
46             {
47                 // 判斷該 context 是否處於普通數據庫事務中
48                 bool isDbTran = context.Database.CurrentTransaction != null;
49  
50                 // 如果處於分布式事務或普通事務中,則“禁用”讀寫分離,處於事務中的所有讀寫操作都指向 Master
51                 string connectionString = isDistributedTran || isDbTran ? this.MasterConnectionString : this.SlaveConnectionString;
52  
53                 this.UpdateConnectionStringIfNeed(context.Database.Connection, connectionString);
54             }
55         }
56  
57  
58         /// <summary>
59         /// 此處改進了對連接字符串的修改判斷機制,確認只在 <paramref name="conn"/> 所使用的連接字符串不等效於 <paramref name="connectionString"/> 的情況下才需要修改。
60         /// <para>同時,在必要的情況下才會連接進行 Open 和 Close 操作以及修改 ConnectionString 處理,減少了性能的消耗。</para>
61         /// </summary>
62         /// <param name="conn"></param>
63         /// <param name="connectionString"></param>
64         private void UpdateConnectionStringIfNeed(DbConnection conn, string connectionString)
65         {
66             if (this.ConnectionStringCompare(conn, connectionString))
67             {
68                 this.UpdateConnectionString(conn, connectionString);
69             }
70         }
71  
72         private void UpdateConnectionString(DbConnection conn, string connectionString)
73         {
74             ConnectionState state = conn.State;
75             if (state == ConnectionState.Open)
76                 conn.Close();
77  
78             conn.ConnectionString = connectionString;
79  
80             if (state == ConnectionState.Open)
81                 conn.Open();
82         }
83  
84         private bool ConnectionStringCompare(DbConnection conn, string connectionString)
85         {
86             DbProviderFactory factory = DbProviderFactories.GetFactory(conn);
87  
88             DbConnectionStringBuilder a = factory.CreateConnectionStringBuilder();
89             a.ConnectionString = conn.ConnectionString;
90  
91             DbConnectionStringBuilder b = factory.CreateConnectionStringBuilder();
92             b.ConnectionString = connectionString;
93  
94             return a.EquivalentTo(b);
95         }
96     }

 

    關於上面的代碼,需要說明的一點是,因為要獲取 EF DbContext 的普通數據庫事務狀態,必須得拿到 DbContext.Database.CurrentTransaction 屬性,所以將 UpdateConnectionString 方法拆分成 UpdateToMaster 和 UpdateToSlave 了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM