上一講中簡單介紹了一個EF環境下通過DbCommand攔截器來實現SQLSERVER的讀寫分離,只是一個最簡單的實現,而如果出現事務情況,還是會有一些問題的,因為在攔截器中我們手動開啟了Connection鏈接,而在一個WEB請求時,如果你的一個變量即用到了read庫又用到了write庫,就會導致到sqlserver端的spid(system process id,系統進程ID,sqlserver里可能是某個數據庫進程序的ID)發生變化 ,而對於這種變化,原本是本地的事務就會自動提升為分布式事務,對MSDTC不了解的同學,可能看我的相關文章,所以,我們使用攔截實現讀寫分離后,在程序里,你的讀和寫的倉儲對象要分別定義,不能共享,而且,你在事務里所以寫的倉儲對象都要使用同一個數據上下文!
當你按着我說的做后,本地事務就不會提升為msdtc了,如圖:
今天我在DbCommand攔截器進行了優化,下面共享一下代碼,如是測試不是真實一項目代碼
/// <summary> /// SQL命令攔截器 /// </summary> public class SqlCommandInterceptor : DbCommandInterceptor { /// <summary> /// 讀庫,從庫集群,寫庫不用設置走默認的EF框架 /// </summary> string readConn = System.Configuration.ConfigurationManager.AppSettings["readDb"] ?? string.Empty; private string GetReadConn() { var readArr = readConn.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var resultConn = string.Empty; if (readArr != null && readArr.Any()) { resultConn = readArr[Convert.ToInt32(Math.Floor((double)new Random().Next(0, readArr.Length)))]; } return resultConn; } //linq to entity生成的update,delete public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { base.NonQueryExecuting(command, interceptionContext);//update,delete等寫操作直接走主庫 } /// <summary> /// 執行sql語句,並返回第一行第一列,沒有找到返回null,如果數據庫中值為null,則返回 DBNull.Value /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { if (!string.IsNullOrWhiteSpace(GetReadConn()))//如果配置了讀寫分離,就去實現 { if (!command.CommandText.StartsWith("insert", StringComparison.InvariantCultureIgnoreCase)) { command.Connection.Close(); command.Connection.ConnectionString = GetReadConn(); command.Connection.Open(); } } base.ScalarExecuting(command, interceptionContext); } /// <summary> /// linq to entity生成的select,insert /// 發送到sqlserver之前觸發 /// warning:在select語句中DbCommand.Transaction為null,而ef會為每個insert添加一個DbCommand.Transaction進行包裹 /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { if (!string.IsNullOrWhiteSpace(GetReadConn()))//如果配置了讀寫分離,就去實現 { if (!command.CommandText.StartsWith("insert", StringComparison.InvariantCultureIgnoreCase)) { command.Connection.Close(); command.Connection.ConnectionString = GetReadConn(); command.Connection.Open(); } } base.ReaderExecuted(command, interceptionContext); } /// <summary> /// 發送到sqlserver之后觸發 /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { base.ReaderExecuted(command, interceptionContext); } }
運行程序可以設置一些測試代碼

public ActionResult Index() { IUnitOfWork db = new backgroundEntities(); IRepository<WebManageUsers> readUser = new BackgroundRepositoryBase<WebManageUsers>(); var a = readUser.GetModel().ToList();//讀庫 using (var trans = new TransactionScope())//事務寫庫 { IRepository<WebManageUsers> userWrite = new BackgroundRepositoryBase<WebManageUsers>(db); IRepository<WebManageMenus> menuWrite = new BackgroundRepositoryBase<WebManageMenus>(db); var entity = new WebManageUsers { WebSystemID = 0, CreateDate = DateTime.Now, DepartmentID = 3, Description = "", Email = "", LoginName = "test", Mobile = "", Operator = "", Password = "", RealName = "test", Status = 1, UpdateDate = DateTime.Now, }; var entity2 = new WebManageMenus { ParentID = 1, About = "", LinkUrl = "", MenuLevel = 1, MenuName = "test", Operator = "", SortNumber = 1, Status = 1, UpdateDate = DateTime.Now, }; userWrite.Insert(entity); menuWrite.Insert(entity2); trans.Complete(); } return View(a); }
最后的結果就是我們想要的,這里說明一點,倉儲大步的讀寫分離沒有數據庫壓力這塊的考慮,只是隨機去訪問某個讀庫。