EF架構~經過EF6的DbCommand攔截器來實現數據庫讀寫分離~終結~配置的優化和事務裏讀寫的統一

回到目錄html

本講是經過DbCommand攔截器來實現讀寫分離的最後一講,對以前幾篇文章作了一個優化,不管是程序可讀性仍是實用性上都有一個提高,在配置信息這塊,去除了字符串方式的拼接,取而代之的是section數組,這樣在修改配置時更加清晰了;而實用性上,完全改變了讀和寫不能共用一個倉儲對象的缺點,而且在一個事務裏能夠讀寫並存,併爲了數據的一致性,使事務裏的curd操做指向主庫,這一點很重要!web

前幾篇文章的目錄

EF架構~經過EF6的DbCommand攔截器來實現數據庫讀寫分離~再續~添加對各只讀服務器的心跳檢測 (2015-01-09 17:52)redis

EF架構~經過EF6的DbCommand攔截器來實現數據庫讀寫分離~續~添加事務機制 (2015-01-08 14:08)sql

EF架構~經過EF6的DbCommand攔截器來實現數據庫讀寫分離 (2015-01-07 17:31)數據庫

功能架構圖以下

下面咱們來分塊看一下此次的修改

一 配置文件的修改數組

  <configSections>
      <section name="DistributedReadWriteSection" type="Project.DistributedReadWriteForEF.DistributedReadWriteSectionHandler, Project.DistributedReadWriteForEF"/>
  </configSections>
  <DistributedReadWriteSection>
    <add key="readDb1" Ip="192.168.2.71" Port="1433" DbName="background_read1" UserId="sa" Password="zzl123" />
    <add key="readDb2" Ip="192.168.2.71" Port="1433" DbName="TestWrite_Read_Zzl" UserId="sa" Password="zzl123" />
    <add key="readDb3" Ip="192.168.2.29" Port="1433" DbName="TestWrite_Read_Zzl" UserId="sa" Password="1" />
  </DistributedReadWriteSection>
  <appSettings>
    <!-- 只讀服務器的sql鏈接串配置模版-->
    <add key ="readDbConnection" value="data source={0};initial catalog={1};persist security info=True;user id={2};password={3};multipleactiveresultsets=True;application name=EntityFramework"/>
    <add key ="writeDbConnection" value="data source=.;initial catalog=background;persist security info=True;user id=sa;password=zzl123;multipleactiveresultsets=True;application name=EntityFramework"/>
  </appSettings>
  /// <summary>
    /// redis配置信息加載
    /// </summary>
    internal class DistributedReadWriteManager
    {
        /// <summary>
        /// 配置信息實體
        /// </summary>
        public static IList<DistributedReadWriteSection> Instance
        {
            get
            {
                return GetSection();
            }
        }

        private static IList<DistributedReadWriteSection> GetSection()
        {
            var dic = ConfigurationManager.GetSection("DistributedReadWriteSection") as Dictionary<string, DistributedReadWriteSection>;
            return dic.Values.ToList();
        }
    }
    /// <summary>
    /// DistributedReadWriteForEFSection塊,在web.config中提供DistributedReadWriteForEFSection塊定義
    /// </summary>
    internal class DistributedReadWriteSection : ConfigurationSection
    {

        /// <summary>
        /// 主機地址
        /// </summary>
        [ConfigurationProperty("Ip", DefaultValue = "127.0.0.1")]
        public string Ip
        {
            get { return (string)this["Ip"]; }
            set { this["Ip"] = value; }
        }
        /// <summary>
        /// 端口號
        /// </summary>
        [ConfigurationProperty("Port", DefaultValue = "1433")]
        public int Port
        {
            get { return (int)this["Port"]; }
            set { this["Port"] = value; }
        }

        /// <summary>
        /// 數據庫名稱
        /// </summary>
        [ConfigurationProperty("DbName", DefaultValue = "Test")]
        public string DbName
        {
            get { return (string)this["DbName"]; }
            set { this["DbName"] = value; }
        }

        /// <summary>
        /// 數據庫帳號
        /// </summary>
        [ConfigurationProperty("UserId", DefaultValue = "sa")]
        public string UserId
        {
            get { return (string)this["UserId"]; }
            set { this["UserId"] = value; }
        }

        /// <summary>
        /// 數據庫帳號
        /// </summary>
        [ConfigurationProperty("Password", DefaultValue = "sa")]
        public string Password
        {
            get { return (string)this["Password"]; }
            set { this["Password"] = value; }
        }
    }
    internal class DistributedReadWriteSectionHandler : IConfigurationSectionHandler
    {
        #region IConfigurationSectionHandler 成員

        public object Create(object parent, object configContext, System.Xml.XmlNode section)
        {
            Dictionary<string, DistributedReadWriteSection> names = new Dictionary<string, DistributedReadWriteSection>();

            string _key = string.Empty;
            string _ip = string.Empty;
            string _dbName = string.Empty;
            string _userId = string.Empty;
            string _password = string.Empty;
            int _port = 1433;

            foreach (XmlNode childNode in section.ChildNodes)
            {
                if (childNode.Attributes["key"] != null)
                {
                    _key = childNode.Attributes["key"].Value;

                    if (childNode.Attributes["Ip"] != null)
                    {
                        _ip = childNode.Attributes["Ip"].Value;
                    }
                    if (childNode.Attributes["Port"] != null)
                    {
                        _port = Convert.ToInt32(childNode.Attributes["Port"].Value);
                    }
                    if (childNode.Attributes["DbName"] != null)
                    {
                        _dbName = childNode.Attributes["DbName"].Value;
                    }
                    if (childNode.Attributes["UserId"] != null)
                    {
                        _userId = childNode.Attributes["UserId"].Value;
                    }
                    if (childNode.Attributes["Password"] != null)
                    {
                        _password = childNode.Attributes["Password"].Value;
                    }
                    names.Add(_key, new DistributedReadWriteSection { Ip = _ip, Port = _port, DbName = _dbName, UserId = _userId, Password = _password });
                }
            }
            return names;
        }

        #endregion
    }

二 倉儲大叔事務塊修改服務器

   public static void UsingNoMsdtc(IUnitOfWork db, bool isOutest, Action action)
        {
            var objectContext = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)db).ObjectContext;
            try
            {

                objectContext.Connection.Close();
                //強制將全部curd操做維持到主庫
                Project.DistributedReadWriteForEF.CommandInterceptor.IsTransactionScope = true;
                //從新設置連接串
                if (System.Configuration.ConfigurationManager.AppSettings["writeDbConnection"] != null)
                    objectContext.TransactionHandler.DbContext.Database.Connection.ConnectionString = System.Configuration.ConfigurationManager.AppSettings["writeDbConnection"];

                objectContext.Connection.Open();

                using (TransactionScope trans = new TransactionScope())
                {
                    action();
                    trans.Complete();
                    Project.DistributedReadWriteForEF.CommandInterceptor.IsTransactionScope = false;//事務結束將走讀寫分離
                }
            }
            finally
            {
                if (isOutest)//若是是最外層事務,再將鏈接關閉!內部事務與外部事務須要共用一個Connection的鏈接
                    objectContext.Connection.Close(); //只能關閉,不能dispose,由於dispose以後,上下文就沒法獲得連接串了
            }
        }

三 DbCommand攔截器的修改架構

    /// <summary>
    /// SQL命令攔截器
    /// 主要實現EF的讀寫分離
    /// </summary>
    public class CommandInterceptor : DbCommandInterceptor
    {
        static CommandInterceptor()
        {
            readConnList = DistributedReadWriteManager.Instance;

            sysTimer.Enabled = true;
            sysTimer.Elapsed += sysTimer_Elapsed;
            sysTimer.Start();
        }
        /// <summary>
        /// 是否在一個事務中,若是是select,insert,update,delete都走主庫
        /// ThreadStatic標識它只在當前線程有效
        /// </summary>
        [ThreadStatic]
        public static bool IsTransactionScope = false;
        /// <summary>
        /// 鎖住它
        /// </summary>
        private static object lockObj = new object();
        /// <summary>
        /// 按期找沒有在線的數據庫服務器
        /// </summary>
        private static Timer sysTimer = new Timer(5000);
        /// <summary>
        /// 讀庫,從庫集羣,寫庫不用設置走默認的EF框架
        /// </summary>
        private static IList<DistributedReadWriteSection> readConnList;

        #region Private Methods
        private static void sysTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (readConnList != null && readConnList.Any())
            {
                foreach (var item in readConnList)
                {
                    //心跳測試,將死掉的服務器IP從列表中移除
                    var client = new TcpClient();
                    try
                    {
                        client.Connect(new IPEndPoint(IPAddress.Parse(item.Ip), item.Port));
                    }
                    catch (SocketException)
                    {
                        //異常,沒有鏈接上
                        readConnList.Remove(item);
                    }
                    if (!client.Connected)
                    {
                        readConnList.Remove(item);
                    }
                }
            }
        }

        /// <summary>
        /// 處理讀庫字符串
        /// </summary>
        /// <returns></returns>
        private string GetReadConn()
        {
            if (readConnList != null && readConnList.Any())
            {
                var resultConn = readConnList[Convert.ToInt32(Math.Floor((double)new Random().Next(0, readConnList.Count)))];
                return string.Format(System.Configuration.ConfigurationManager.AppSettings["readDbConnection"]
                    , resultConn.Ip
                    , resultConn.DbName
                    , resultConn.UserId
                    , resultConn.Password);
            }
            return string.Empty;
        }
        /// <summary>
        /// 只讀庫的選擇,加工command對象
        /// 說明:事務中,全部語句都走主庫,事務外select走讀庫,insert,update,delete走主庫
        /// 但願:一個WEB請求中,讀與寫的倉儲使用一個,不須要在程序中去從新定義
        /// </summary>
        /// <param name="command"></param>
        private void ReadDbSelect(DbCommand command)
        {
            if (!string.IsNullOrWhiteSpace(GetReadConn()))//若是配置了讀寫分離,就去實現
            {
                command.Connection.Close();
                if (!command.CommandText.StartsWith("insert", StringComparison.InvariantCultureIgnoreCase) && !IsTransactionScope)
                    command.Connection.ConnectionString = GetReadConn();
                command.Connection.Open();
            }
        }
        #endregion

        #region Override Methods
        /// <summary>
        /// Linq to Entity生成的update,delete
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        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)
        {
            ReadDbSelect(command);
            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)
        {
            ReadDbSelect(command);
            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);
        }

        #endregion
    }

好了,到這裏,經過攔截器來實現數據庫讀寫分離的方案就完全完成了,這個版本應該算是個終級了吧,呵呵!感謝您的閱讀!app

回到目錄框架

相關文章
相關標籤/搜索