回到目錄,完整代碼請查看(
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 了。
