1. 程式人生 > >Entity Framework——讀寫分離

Entity Framework——讀寫分離

har client eth onpause sim nta oid del open

1 實現

CustomDbContext擴展了DbContext,其構造函數帶有形式參nameOrConnectionString,可以在使用CustomDbContext時指定數據庫連接字符串。

DbContextFactory包含兩個屬性MasterDbContext和SlaveDbContext,MasterDbContext為主庫上下文,SlaveDbContext為從庫上下文。DbContextFactory還包含了一個方法:UpdateSlaves用於實現對SlaveDbContext的更新,因為SlaveDbContext是從多個配置的從庫隨機取出一個,因此定時檢測不可用從庫,將其從從庫集合中剔除。

JobScheduler為定時任務規劃器,使用Quartz實現。quartz.config和quartz_jobs.xml為定時任務配置文件。

為了使定時任務工作,在WebApiApplication類的Application_Start()函數應添加:

JobScheduler jobScheduler = new JobScheduler();
jobScheduler.log4netPath = AppSettings.Log4netPathForWeb;
jobScheduler.OnStart();

關鍵代碼

/// <summary>
    /// 自定義上下文
    
/// </summary> [DbConfigurationType(typeof(MySqlEFConfiguration))] public class CustomDbContext:DbContext { public CustomDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { Database.SetInitializer<CustomDbContext>(null
); } ...... public DbSet<Collection> Collections { get; set; } public DbSet<CollectionUser> CollectionUsers { get; set; } ...... protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); EntityConfiguration.Set(modelBuilder); } } public class EntityConfiguration { public static void Set(DbModelBuilder modelBuilder) { modelBuilder.Entity<Collection>().Property(c => c.FileName) .IsUnicode(false) .IsRequired() .HasMaxLength(50); modelBuilder.Entity<Collection>().Property(c => c.TableName) .IsUnicode(false) .IsRequired() .HasMaxLength(50); modelBuilder.Entity<Collection>().Property(c => c.Title) .IsUnicode(false) .IsRequired() .HasMaxLength(200); modelBuilder.Entity<Collection>().Property(c => c.Author) .IsUnicode(false) .IsOptional() .HasMaxLength(50); modelBuilder.Entity<Collection>().Property(c => c.PublicationName) .IsUnicode(false) .IsOptional() .HasMaxLength(200); modelBuilder.Entity<Collection>().Property(c => c.DiscNo) .IsUnicode(false) .IsOptional() .HasMaxLength(50); modelBuilder.Entity<Collection>().Property(c => c.ResourceType) .IsUnicode(false) .IsOptional() .HasMaxLength(50); modelBuilder.Entity<Collection>().Property(c => c.PublisherUnit) .IsUnicode(false) .IsOptional() .HasMaxLength(200); modelBuilder.Entity<Collection>().Property(c => c.Year) .IsUnicode(false) .IsOptional() .HasMaxLength(10); modelBuilder.Entity<Collection>().Property(c => c.Period) .IsUnicode(false) .IsOptional() .HasMaxLength(10); modelBuilder.Entity<Collection>().Property(c => c.PublicationDate) .IsOptional(); modelBuilder.Entity<Collection>().Property(c => c.Downloads) .IsOptional(); modelBuilder.Entity<Collection>().Property(c => c.CitationNumber) .IsOptional(); } } /// <summary> /// db上下文工廠 /// </summary> public class DbContextFactory { private static List<string> allSlaves = GetAllSlaves(); private DbContextFactory() { } /// <summary> ////// </summary> public static CustomDbContext MasterDbContext { get { return new CustomDbContext("name=Master"); } } /// <summary> ////// </summary> public static CustomDbContext SlaveDbContext { get { Random rm = new Random(); if (allSlaves.Count > 0) { int i = rm.Next(allSlaves.Count); string name = string.Format("name={0}", allSlaves.ElementAt(i)); return new CustomDbContext(name); } else { return MasterDbContext; } } } /// <summary> /// 獲得所有可用連接 /// </summary> /// <returns></returns> private static List<string> GetAllSlaves() { List<string> connNames = new List<string>(); var conns = ConfigurationManager.ConnectionStrings; if (conns == null) { throw new Exception("ConfigurationManager.ConnectionStrings 是空值,請檢查Web.config"); } var masterConn = conns["Master"]; if (masterConn == null) { throw new Exception("名稱為Master的連接配置不存在,請檢查Web.config"); } //conn中必然包含master,還有一個默認的LocalSqlServer int connCount = conns.Count - 1; if (connCount == 0) { throw new Exception("連接配置中只包含Master,不包含任何Slave,請檢查Web.config,並配置Slave"); } for (int i = 0; i < connCount; i++) { string connName = string.Format("Slave{0}", i); var conn = ConfigurationManager.ConnectionStrings[connName]; if (conn == null) { string msg = string.Format("{0}不存在,請檢查配置Web.config", connName); throw new Exception(msg); } //檢測是否可連接 bool canConn = CanConnect(connName); if (canConn) { connNames.Add(connName); } } return connNames; } public static void UpdateSlaves() { allSlaves = GetAllSlaves(); if (allSlaves.Count == 0) { allSlaves.Add("Master"); } } private static bool CanConnect(string connName) { bool ret = false; DbConnection dbConnection = null; try { string connStr = ConfigurationManager.ConnectionStrings[connName].ToString(); MySqlConnectionFactory factory = new MySqlConnectionFactory(); dbConnection = factory.CreateConnection(connStr); dbConnection.Open();//打不開會拋異常 ret = true; } catch (Exception ex) { } finally { if (dbConnection != null && dbConnection.State == System.Data.ConnectionState.Open) dbConnection.Close(); } return ret; } } public class JobScheduler { /// <summary> /// log4net配置文件位置 /// </summary> public string log4netPath { get; set; } private IScheduler scheduler; public JobScheduler() { } public void OnStart() { //構造函數自動加載Quartz.config,並通過quartz.plugin.xml.fileNames加載~/quartz_jobs.xml try { if (string.IsNullOrWhiteSpace(log4netPath)) { log4netPath = AppSettings.Log4netPathForApp; } //加載日誌 LogConfigLoading.Load(log4netPath); ISchedulerFactory sf = new StdSchedulerFactory(); scheduler = sf.GetScheduler(); scheduler.Start(); } catch (Exception ex) { LogHelper.LogError(ex, "JobScheduler"); } } public void OnStop() { if (scheduler != null && scheduler.IsStarted) { scheduler.Shutdown(false); } } public void OnPause() { if (scheduler != null && scheduler.IsStarted) { scheduler.PauseAll(); } } public void OnContinue() { if (scheduler != null) { bool isJobGroupPaused = false; var groups = scheduler.GetJobGroupNames(); foreach (var group in groups) { isJobGroupPaused = scheduler.IsJobGroupPaused(group); if (!isJobGroupPaused) { break; } } if (isJobGroupPaused) { scheduler.ResumeAll(); } } } } public class DbMonitorJob : IJob { public void Execute(IJobExecutionContext context) { DbContextFactory.UpdateSlaves(); } }

定時器配置文件quartz_jobs.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- This file contains job definitions in schema version 2.0 format -->
<job-scheduling-data xmlns= "http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance " version ="2.0 ">
  <processing-directives>
    <overwrite-existing-data>true</overwrite-existing-data>
  </processing-directives>
  <schedule>
    <job>
      <name>DbMonitorJob</name>
      <group>DbMonitorJobGroup</group>
      <description>更新可用從庫</description>
      <job-type>HY_WebApi.TaskScheduler.Jobs.DbMonitorJob, HY_WebApi.TaskScheduler</job-type>
      <durable>true</durable>
      <recover>false</recover>
    </job>
    <trigger>
      <simple>
        <name>DbMonitorJobTrigger</name>
        <group>DbMonitorJobTriggerGroup</group>
        <description>更新可用從庫</description>
        <job-name>DbMonitorJob</job-name>
        <job-group>DbMonitorJobGroup</job-group>
        <misfire-instruction>SmartPolicy</misfire-instruction>
        <repeat-count>-1</repeat-count>
        <repeat-interval>10000</repeat-interval>
      </simple>
    </trigger>
  </schedule>
</job-scheduling-data>

定時任務配置文件quartz.config

# You can configure your scheduler in either <quartz> configuration section
# or in quartz properties file
# Configuration section has precedence

quartz.scheduler.instanceName = ServerScheduler

# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 1
quartz.threadPool.threadPriority = Normal

# job initialization plugin handles our xml reading, without it defaults are used
quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz
quartz.plugin.xml.fileNames = ~\quartz_jobs.xml

# export this server to remoting context
quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
quartz.scheduler.exporter.port = 555
quartz.scheduler.exporter.bindName = QuartzScheduler
quartz.scheduler.exporter.channelType = tcp
quartz.scheduler.exporter.channelName = httpQuartz

web項目配置文件Web.config

<configuration>
<connectionStrings>
    <clear/><!--清除默認的連接字符串,務必加上!!!-->
    <add name="Master" connectionString="Database=hy_webapi_n;Data Source=192.168.107.65;User Id=root;Password=cnki2016;CharSet=utf8;port=3306" providerName="MySql.Data.MySqlClient" />
    <add name="Slave0" connectionString="Database=hy_webapi_n;Data Source=192.168.107.62;User Id=root;Password=cnki2016;CharSet=utf8;port=3306" providerName="MySql.Data.MySqlClient" />
    <add name="Slave1" connectionString="Database=hy_webapi_n;Data Source=192.168.107.63;User Id=root;Password=cnki2016;CharSet=utf8;port=3306" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>

......
</configuration>

加載定時器

public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
            JobScheduler jobScheduler = new JobScheduler();
            jobScheduler.log4netPath = AppSettings.Log4netPathForWeb;
            jobScheduler.OnStart();
        }
}

2 代碼分析

最核心的部分是DbContextFactory。下面詳細分析其設計與實現。

獲得web.config配置文件中的連接名稱

使用靜態私鑰變量allSlaves來表示從庫集合,這樣做的好處是:靜態私有變量只在使用前初始化一次,當第一次被allSlaves使用時初始化一次,即調用GetAllSlaves()方法獲得所有可用的從庫。當第二次使用allSlaves時,即當SlaveDbContext屬性第二次被調用時,不在計算allSlaves。大部分時間都花費在測試數據庫是否可用,因此不在重復計算allSlaves節省了時間。直接的效果就是由於檢測數據庫是否可用的影響可以忽略不計。

不可使用單例模式

由於檢測數據庫是否可用相對耗費時間的比例較大,於是想到通過單例模式來實現DbContextFactory,這樣會導致系統報錯:The operation cannot be completed because the DbContext has been disposed.其原因就在於使用DbContext時,慎重使用單例模式,全局的DbContext會引起第二次調用出錯,即第一次調用後DbContext資源即被釋放。

類似於單例模式的實現,即全局的DbContext,也是不可取的。

基於上述考慮設計實現SlaveDbContext,在每次被調用時,都會返回一個新的實例。

多從庫隨機選擇

當配置了多個從庫時,應隨機從從庫集合中選擇一個。於是使用偽隨機數生成器Random。

所有從庫不可用時切換到主庫

當所有從庫都不可用時,SlaveDbContext值為MasterDbContext。這裏還應該增加一個額外的監測服務,當有從庫不可用時自動報警,供系統維護人員查看。

註意先寫後讀的操作

對於這種操作,若主從同步延遲稍大,那麽會造成操作失敗,解決的辦法是:只操作主庫。保守的做法就是只操作主庫,一般主從分部在內網的兩臺機器上,網絡通信延遲一旦較大時,就會造成數據無法同步的假象。

Entity Framework——讀寫分離