• Entity Framework——读写分离


    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。这里还应该增加一个额外的监测服务,当有从库不可用时自动报警,供系统维护人员查看。

    注意先写后读的操作

    对于这种操作,若主从同步延迟稍大,那么会造成操作失败,解决的办法是:只操作主库。保守的做法就是只操作主库,一般主从分部在内网的两台机器上,网络通信延迟一旦较大时,就会造成数据无法同步的假象。

    -----------------------------------------------------------------------------------------

    转载与引用请注明出处。

    时间仓促,水平有限,如有不当之处,欢迎指正。

  • 相关阅读:
    吴裕雄--天生自然 PYTHON数据分析:糖尿病视网膜病变数据分析(完整版)
    吴裕雄--天生自然 PYTHON数据分析:糖尿病视网膜病变数据分析(续六)
    吴裕雄--天生自然 PYTHON数据分析:糖尿病视网膜病变数据分析(续五)
    吴裕雄--天生自然 PYTHON数据分析:糖尿病视网膜病变数据分析(续四)
    MaxCompute安全管理指南-案例篇
    MaxCompute安全管理指南-基础篇
    发光的二次元——克拉克拉上云实践
    使用应用程序(Java/Python)访问MaxCompute Lightning进行数据开发
    Sentinel 发布里程碑版本,添加集群流控功能
    高效开发 Dubbo?用 Spring Boot 可得劲!
  • 原文地址:https://www.cnblogs.com/hdwgxz/p/7860139.html
Copyright © 2020-2023  润新知