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();
modelBuilder.Entity<Collection>().Property(c => c.TableName)
.IsUnicode(false)
.IsRequired()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.Title)
.IsUnicode(false)
.IsRequired()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.Author)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.PublicationName)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.DiscNo)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.ResourceType)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.PublisherUnit)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.Year)
.IsUnicode(false)
.IsOptional()
.HasMaxLength();
modelBuilder.Entity<Collection>().Property(c => c.Period)
.IsUnicode(false)
.IsOptional()
.HasMaxLength(); 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 > )
{
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 - ;
if (connCount == )
{
throw new Exception("连接配置中只包含Master,不包含任何Slave,请检查Web.config,并配置Slave");
} for (int i = ; 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 == )
{
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——读写分离的更多相关文章

  1. 读写分离子系统 - C# SQL分发子系统 - Entity Framework支持

    A2D Framework增加了EF支持,加上原先支持ADO.NET: 支持EF方式 支持ADO.NET方式 这次来讲如何让Entity Framework变成nb的读写分离 1. 先设计EF模型, ...

  2. Entity Framework 6 Recipes 2nd Edition(9-1)译->用Web Api更新单独分离的实体

    第九章 在N层结构的应用程序中使用EF 不是所有的应用都能完全地写入到一个单个的过程中(就是驻留在一个单一的物理层中),实际上,在当今不断发展的网络世界,大量的应用程序的结构包含经典的表现层,应用程, ...

  3. Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

    在使用Entity Framework 实体框架的时候,我们大多数时候操作的都是实体模型Entity,这个和数据库操作上下文结合,可以利用LINQ等各种方便手段,实现起来非常方便,一切看起来很美好.但 ...

  4. Programming Entity Framework 翻译(1)-目录

    1. Introducing the ADO.NET Entity Framework ado.net entity framework 介绍 1 The Entity Relationship Mo ...

  5. net Core 使用MyCat分布式数据库,实现读写分离

    net Core 使用MyCat分布式数据库,实现读写分离 目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 MyCat2.0版本很快就发布了,关于MyCat的动态和一些问题, ...

  6. EF通用数据层封装类(支持读写分离,一主多从)

    浅谈orm 记得四年前在学校第一次接触到 Ling to Sql,那时候瞬间发现不用手写sql语句是多么的方便,后面慢慢的接触了许多orm框架,像 EF,Dapper,Hibernate,Servic ...

  7. Entity Framework 项目使用心得

    在博客园很久了,一直只看不说,这是发布本人的第一个博客. 总结一下在项目中,EntityFramework使用的一下经验拿来和大家分享,希望对大家有用~ 1.         在Entity Fram ...

  8. Entity Framework Core 1.1 升级通告

    原文地址:https://blogs.msdn.microsoft.com/dotnet/2016/11/16/announcing-entity-framework-core-1-1/ 翻译:杨晓东 ...

  9. Entity Framework 6 Recipes 2nd Edition 译 -> 目录 -持续更新

    因为看了<Entity Framework 6 Recipes 2nd Edition>这本书前面8章的翻译,感谢china_fucan. 从第九章开始,我是边看边译的,没有通读,加之英语 ...

随机推荐

  1. django-admin管理后台高级自定义

    django自带的admin后台管理系统,在很多网站中被称为django的杀手级的应用.那么django-admin的适用情形倒底有哪些呢,一般 来说对于大型的商业性的项目通常不用采用django-a ...

  2. centos top 命令详解及退出top命令-使用p键及free命令

    1.作用 top命令用来显示执行中的程序进程,使用权限是所有用户. 2.格式 top [-] [d delay] [q] [c] [S] [s] [i] [n] 3.主要参数 d:指定更新的间隔,以秒 ...

  3. Eclipse 入手配置

    工欲善其事,必先利其器 在安装eclipse后进行简单的配置能够为以后的开发提高不少效率,这里慢慢先记录自己的经验. 一.编码配置 utf-8 1.worksplace: 首先设置就是workspac ...

  4. Python之异常设计(一)

    一 定义 异常分为两类:一类是自动触发异常如除零错误:另一类是通过raise触发. 二 为什么要使用异常 当程序运行时,如果检测到程序错误,Python就会引发异常,我们可以在程序中使用try语句捕获 ...

  5. thinkphp下判断状态值语法

    在thinkphp框架下我们经常会用到状态值的判断:但是这样写会引起语法错误. <div> <if condition="{$res.status} eq '0'" ...

  6. 三重for循环实现对二维数组的按列排序(JavaScript)

    由C语言联想到的:三重for循环实现对二维数组的按列排序,并且牵扯到数据结构. 自己写的,水平有限,本文属于原创,可能存在错误,忘指正~ function circle() { var a = [ [ ...

  7. CentOS 7运维管理笔记(3)----Linux路由器配置

    当正在配置的Linux主机需要作为路由器使用时,通过以下步骤配置后,子网上的计算机就可以访问外网了: 1. 编辑 /etc/sysctl.conf 文件,添加 net.ipv4_ip_forward ...

  8. Ta们,用云计算改变着更多普通人的生活,所以,我们1218

    维族音乐的传承者:为家园建设生态农业:为50万货运司机谋福利:电视游戏行业复兴的倡导者:......还有很多平凡普通的人,不同的主角.不同的情节,用自己的云上轨迹在点滴改变着我们的周遭世界.所以,我们 ...

  9. 密码存储中MD5的安全问题与替代方案

    md5安全吗?有多么地不安全?如何才能安全地存储密码?... md5安全吗? 经过各种安全事件后,很多系统在存放密码的时候不会直接存放明文密码了,大都改成了存放了 md5 加密(hash)后的密码,可 ...

  10. java excel转pdf 工具类

    package com.elitel.hljhr.comm.web.main.controller; import java.io.File; import java.io.FileOutputStr ...