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. 【读书笔记】读《编写可维护的JavaScript》 - 编程实践(第二部分)

    本书的第二个部分总结了有关编程实践相关的内容,每一个章节都非常不错,捡取了其中5个章节的内容.对大家组织高维护性的代码具有辅导作用. 5个章节如下—— 一.UI层的松耦合 二.避免使用全局变量 三.事 ...

  2. nodejs图片裁剪、水印(使用images)

    /** * Created by chaozhou on 2015/9/21. */ var images = require("images"); /** * 缩放图像 * @p ...

  3. jsp servlet基础复习 Part1

    jsp和servlet的一些基础知识整理,用于备忘. 一.jsp与servlet的基本关系 1.jsp-->web容器-->servlet-->加载进容器的虚拟机执行-->输出 ...

  4. IIS6服务器的请求流程(图文&源码)

    1.IIS 7开发与管理完全参考手册  http://book.51cto.com/art/200908/146040.htm 2.Web服务IIS 6   https://technet.micro ...

  5. K2P刷机教程转自恩山磨人的小妖精

    K2P刷机指南说明 K2P MTK版发布之初用的是22.5.7.85, 这个版本官改和高恪K2P固件都可以从斐讯固件基础上直接升级, 是所谓直刷.但好景不长, 之后的版本比如22.5.17.33就改了 ...

  6. ES6中的import()函数

    import(specifier) 上面代码中,import函数的参数specifier,指定所要加载的模块的位置.import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要 ...

  7. BBS登录与注册功能

    登录功能 视图函数 def my_login(request): if request.method == 'GET': return render(request, 'login.html') el ...

  8. JSON对象的两个方法

    JSON对象有两个方法,stringify()和parse(). 最简单的方法,这两个方法分别用于吧JavaScript对象序列化为JSON字符串和把JSON字符串解析为原生JavaScript值. ...

  9. git日常使用

    git强制回滚指定版本git reset --hard xxx(版本名) git强制推送git push -f remote(远程地址) branch(远程分支) 查看远程分支 git branch ...

  10. C#打印代码运行时间

    使用以下方法可以准确的记录代码运行的耗时. System.Diagnostics.Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); / ...