返回

基于 EF 的数据库主从复制、读写分离实现

AI 摘要
本文介绍了实现数据库主从复制和读写分离的重要性和方法。主从复制通过创建与主数据库完全相同的从数据库来提供数据服务的连续性和提高性能。文章以 MySQL 为例,解释了主从复制的工作原理,包括二进制日志(binlog)和线程的交互。在ORM层面上,通过Entity Framework (EF) 中的 DbCommandInterceptor 拦截 SQL 命令,可以实现读写分离:所有 Select 查询操作通过从库执行,而 Insert/Update/Delete 操作通过主库执行。文章提供了具体的代码实现,包括主从库管理类 MasterSlaveManager 和拦截器 MasterSlaveDbInterceptor 的定义和使用。最后,讨论了在实际应用中可能遇到的问题,如事务处理、数据库连接管理以及在一主多从的情况下进行健康检查和负载均衡,鼓励读者深入研究这些高级主题。

  各位朋友,大家好,欢迎大家关注我的博客,我是 Psyne,我的博客地址是https://blog.yuanpei.me。在上一篇博客中,我们提到了通过 DbCommandInterceptor 来实现 EF 中 SQL 针对 SQL 的“日志”功能。我们注意到,在这个拦截器中,我们可以获得当前数据库的上下文,可以获得 SQL 语句中的参数,更一般地,它具备“AOP”特性的扩展能力,可以在执行 SQL 的前后插入相应的动作,这就有点类似数据库中触发器的概念了。今天,我们主要来说一说,基于 EF 实现数据库主从复制和读写分离,希望这个内容对大家有所帮助。

主从复制 & 读写分离

  首先,我们先来了解一个概念:主从复制。那么,什么是主从复制呢?通常,在只有一个数据库的情况下,这个数据库会被称为主数据库。所以,当有多个数据库存在的时候,数据库之间就会有主从之分,而那些和主数据库完全一样的数据库就被称为从数据库,所以,主从复制其实就是指建立一个和主库完全一样的数据库环境

  那么,我们为什么需要主从复制这种设计呢?我们知道,主数据库一般用来存储实时的业务数据,因此如果主数据库服务器发生故障,从数据库可以继续提供数据服务,这就是主从复制的优势之一,即作为数据提供灾备能力。其次,从业务扩展性上来讲,互联网应用的业务增长速度普遍较高,随着业务量越来越大,I/O 的访问频率越来越高,在单机磁盘无法满足性能要求的情况下,通过设置多个从数据库服务器,可以降低磁盘的 I/O 访问频率,进而提高单机磁盘的读写性能。从业务场景上来讲,数据库的性能瓶颈主要在读即查询上,因此将读和写分离,能够让数据库支持更大的并发,这对优化前端用户体验很有意义

  通常来讲,不同的数据库都在数据库层面上实现了主从复制,各自的实现细节上可能会存在差异,譬如 SQLServer 中可以通过“发布订阅”来配置主从复制的策略,而 Oracle 中可以通过 DataGurd 来实现主从复制,甚至你可以直接把主库 Dump 出来再导入到从库。博主没有能力详细地向大家介绍它们的相关细节,可博主相信“万变不离其宗”的道理,这里我们以 MySQL 为例,因为它在互联网应用中更为普遍,虽然坑会相应地多一点:)……

  MySQL 中有一种最为重要的日志 binlog,即二进制日志,它记录了所有的 DDL 和 DML(除查询以外)语句,通过这些日志,不仅可以作为灾备时的数据恢复,同样可以传递给从数据库来达到数据一致的目的。具体来讲,对于每一个主从复制的连接,都有三个线程,即拥有多个从库的主库为每一个从库创建的binlog 输出线程,从库自身的IO 线程SQL 线程

  • 当从库连接到主库时,主库就会创建一个线程然后把 binlog 发送到从库,这是 binlog 输出线程。
  • 当从库执行 START SLAVER 以后,从库会创建一个 I/O 线程,该线程连接到主库并请求主库发送 binlog 里面的更新记录到从库上。从库 I/O 线程读取主库的 binlog 输出线程发送的更新并拷贝这些更新到本地文件(其中包括 relay log 文件)。
  • 从库创建一个 SQL 线程,这个线程读取从库 I/O 线程写到 relay log 的更新事件并执行。

EF 中主从复制的实现

  虽然从数据库层面上做主从复制会更简单一点,可在很多时候,这些东西其实更贴近 DBA 的工作,而且不同数据库在操作流程上还都不一样,搞这种东西注定不能成为“通用”的知识领悟。对开发人员来说,EF 和 Dapper 这样的 ORM 更友好一点,如果可以在 ORM 层面上做触发器和存储过程,可能 SQL 看起来就没有那么讨厌了吧!博主的公司因为要兼顾主流的数据库,所以,不可能在数据库层面上去做主从复制,最终我们是通过 EF 来实现主从复制。

  其实,讲了这么多主从复制的原理,对我们来说,这篇文章的实现则是非常简单的。因为通过 DbCommandInterceptor 我们能拦截到 SQL 命令,所以,只要是 Select 命令全部走从库,Insert/Update/Delete 全部走主库,这样就实现了读写分离。怎么样,是不是感觉相当简单啊!当然,前提是要准备好主从库的屋里环境,这些就让 DBA 去折腾吧(逃。好了,下面一起来看具体代码,首先我们定义一个主从库管理类 MasterSlaveManager:

public static class MasterSlaveManager
{
    private static MasterSalveConfig _config => LoadConfig();

    /// <summary>
    /// 加载主从配置
    /// </summary>
    /// <param name="fileName">配置文件</param>
    /// <returns></returns>
    public static MasterSalveConfig LoadConfig(string fileName = "masterslave.config.json")
    {
        if (!File.Exists(fileName)) throw new Exception(string.Format("配置文件{0}不存在", fileName));
        return JsonConvert.DeserializeObject<MasterSalveConfig>(File.ReadAllText(fileName));
    }

    /// <summary>
    /// 切换到主库
    /// </summary>
    /// <param name="command">DbCommand</param>
    public static void SwitchToMaster(DbCommand command, string serverName = "")
    {
        var masterServer = string.IsNullOrEmpty(serverName) ? 
            _config.Masters.FirstOrDefault() : _config.Masters.FirstOrDefault(e => e.ServerName == serverName);
        if (masterServer == null) throw new Exception("未配置主库服务器或者服务器名称不正确");
        //切换数据库连接
        ChangeDbConnection(command, masterServer);
    }

    /// <summary>
    /// 切换到从库
    /// </summary>
    /// <param name="command">DbCommand</param>
    public static void SwitchToSlave(DbCommand command, string serverName = "")
    {
        var salveServer = string.IsNullOrEmpty(serverName) ?
             _config.Slaves.FirstOrDefault() : _config.Slaves.FirstOrDefault(e => e.ServerName == serverName);
        if (salveServer == null) throw new Exception("未配置从库服务器或者服务器名称不正确");
        //切换数据库连接
        ChangeDbConnection(command, salveServer);
    }

    /// <summary>
    /// 切换数据库连接
    /// </summary>
    /// <param name="command"></param>
    /// <param name="dbServer"></param>
    private static void ChangeDbConnection(DbCommand command, DbServer dbServer)
    {
        var conn = command.Connection;
        if (conn.State == System.Data.ConnectionState.Open) conn.Close();
        conn.ConnectionString = dbServer.ConnectionString;
        conn.Open();
    }
}

接下来,和之前关于 EF 中的 SQL 拦截器类似,我们定义一个名为 MasterSlaveDbInterceptor 的拦截器:

public class MasterSlaveDbInterceptor : DbCommandInterceptor
{
    public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        //Insert/Update(写操作)走主库
        MasterSlaveManager.SwitchToMaster(command);
        base.NonQueryExecuting(command, interceptionContext);
    }

     public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        //Select(读操作)走从库
        var sqlText = command.CommandText;
        if (!sqlText.ToUpper().StartsWith("INSERT") || !sqlText.ToUpper().StartsWith("UPDATE"))
            MasterSlaveManager.SwitchToSlave(command);
        base.ScalarExecuting(command, interceptionContext);
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        //Select(读操作)走从库
        var sqlText = command.CommandText;
        if (!sqlText.ToUpper().StartsWith("INSERT") || !sqlText.ToUpper().StartsWith("UPDATE"))
            MasterSlaveManager.SwitchToSlave(command);
        base.ReaderExecuting(command, interceptionContext);
    }
}

至此,我们就实现了基于 EF 的数据库主从复制、读写分离。其实,更严谨的说法是,主从复制是在数据层面上完成的,而读写分离则是在代码层面上完成。当然,实际应用中需要考虑事务、数据库连接等因素,这里我们仅仅提供一种思路。这里我们的配置文件中,对主、从数据库进行了简单配置,即一主一从。在实际应用中,可能我们会遇到一注多从的情况,在这个基础上,我们又可以延申出新的话题,譬如在存在多个从库的情况下,通过心跳检测来检查从库服务器的健康状态,以及如何为不同的从库服务器设置权重,实现多个从库服务器的负载均衡等等。我们在微服务中提出的**“健康检查”“负载均衡”**等概念,其实都可以映射到这里来,我想这是真正值得我们去深入研究的地方。

本文小结

  并没有,いじょう

Built with Hugo v0.126.1
Theme Stack designed by Jimmy
已创作 274 篇文章,共计 1038468 字