回到目錄
上一講中簡單介紹了一個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);
}
View Code
最後的結果就是我們想要的,這裡說明一點,倉儲大步的讀寫分離沒有資料庫壓力這塊的考慮,隻是随機去通路某個讀庫。
作者:倉儲大叔,張占嶺,
榮譽:微軟MVP
QQ:853066980
支付寶掃一掃,為大叔打賞!