关于Dapper实现读写分离的我的思考

概念相关

    为了确保多线上环境数据库的稳定性和可用性,大部分状况下都使用了双机热备的技术。通常是一个主库+一个从库或者多个从库的结构,从库的数据来自于主库的同步。在此基础上咱们能够经过数据库反向代理工具或者使用程序的方式实现读写分离,即主库接受事务性操做好比删除、修改、新增等操做,从库接受读操做。笔者自认为读写分离解决的痛点是,数据库读写负载很是高的状况下,单点数据库存在读写冲突,从而致使数据库压力过大,出现读写操做缓慢甚至出现死锁或者拒绝服务的状况。它适用与读大于写,并能够容忍一段时间内不一致的状况,由于主从同步存在必定的延迟,大体的实现架构图以下(图片来自于网络)。
数据库主从架构    虽然咱们能够经过数据库代理实现读写分离,好比mycat,这类方案的优点就是对程序自己没有入侵,经过代理自己来拦截sql语句分发到具体数据。甚至是更好的解决方案NewSQL去解决,好比TiDB。可是仍是那个原则,不管使用数据库代理或者NewSQL的状况都是比较重型的解决方案,会增长服务节点和运维成本,有时候还没到使用这些终极解决方案的地步,这时候咱们会在程序中处理读写分离,因此有个好的思路去在程序中解决读写分离也尤其重要。sql

基本结构

接下来咱们新建三个类,固然这个并不固定,能够根据本身的状况新建类。首先咱们新建一个ConnectionStringConsts用来存放链接字符串常量,也就是用来存放读取自配置文件或者配置中心的字符串,这里我直接写死,固然你也能够存放多个链接字符串,大体实现以下。数据库

public class ConnectionStringConsts
{
    /// <summary>
    /// 主库链接字符串
    /// </summary>
    public static readonly string MasterConnectionString = "server=db.master.com;Database=crm_db;UID=root;PWD=1";

    /// <summary>
    /// 从库链接字符串
    /// </summary>
    public static readonly string SlaveConnectionString = "server=db.slave.com;Database=crm_db;UID=root;PWD=1";
}

而后新建存储数据库链接字符串主从映射关系的映射类ConnectionStringMapper,这个类的主要功能就是经过链接字符串创建主库和从库的关系,而且根据映射规则返回实际要操做的字符串,大体实现以下网络

public static class ConnectionStringMapper
{
    //存放字符串主从关系
    private static readonly IDictionary<string, string[]> _mapper = new Dictionary<string, string[]>();
    private static readonly Random _random = new Random();

    static ConnectionStringMapper()
    {
        //添加数关系映射
        _mapper.Add(ConnectionStringConsts.MasterConnectionString, new[] { ConnectionStringConsts.SlaveConnectionString });
    }

    /// <summary>
    /// 获取链接字符串
    /// </summary>
    /// <param name="masterConnectionStr">主库链接字符串</param>
    /// <param name="useMaster">是否选择读主库</param>
    /// <returns></returns>
    public static string GetConnectionString(string masterConnectionStr,bool useMaster)
    {
        //是否走主库
        if (useMaster)
        {
            return masterConnectionStr;
        }

        if (!_mapper.Keys.Contains(masterConnectionStr))
        {
            throw new KeyNotFoundException("不存在的链接字符串");
        }

        //根据主库获取从库链接字符串
        string[] slaveStrs = _mapper[masterConnectionStr];
        if (slaveStrs.Length == 1)
        {
            return slaveStrs[0];
        }
        return slaveStrs[_random.Next(0, slaveStrs.Length - 1)];
    }
}

这个类是比较核心的操做,关于实现读写分离的核心逻辑都在这,固然你能够根据本身的具体业务实现相似的操做。接下来,咱们将封装一个DapperHelper的操做,虽然Dapper用起来比较简单方便,可是依然强烈建议!!!封装一个Dapper操做类,这样的话能够统一处理数据库相关的操做,对于之后的维护修改都很是方便,扩展性的时候也会相对容易一些架构

public static class DapperHelper
{
    public static IDbConnection GetConnection(string connectionStr)
    {
        return new MySqlConnection(connectionStr);
    }

    /// <summary>
    /// 执行查询相关操做
    /// </summary>
    /// <param name="sql">sql语句</param>
    /// <param name="param">参数</param>
    /// <param name="useMaster">是否去读主库</param>
    /// <returns></returns>
    public static IEnumerable<T> Query<T>(string sql, object param = null, bool useMaster=false)
    {
        //根据实际状况选择须要读取数据库的字符串
        string connectionStr = ConnectionStringMapper.GetConnectionString(ConnectionStringConsts.MasterConnectionString, useMaster);
        using (var connection = GetConnection(connectionStr))
        {
            return connection.Query<T>(sql, param);
        }
    }

    /// <summary>
    /// 执行查询相关操做
    /// </summary>
    /// <param name="connectionStr">链接字符串</param>
    /// <param name="sql">sql语句</param>
    /// <param name="param">参数</param>
    /// <param name="useMaster">是否去读主库</param>
    /// <returns></returns>
    public static IEnumerable<T> Query<T>(string connectionStr, string sql, object param = null, bool useMaster = false)
    {
        //根据实际状况选择须要读取数据库的字符串
        connectionStr = ConnectionStringMapper.GetConnectionString(connectionStr, useMaster);
        using (var connection = GetConnection(connectionStr))
        {
            return connection.Query<T>(sql, param);
        }
    }

    /// <summary>
    /// 执行事务相关操做
    /// </summary>
    /// <param name="sql">sql语句</param>
    /// <param name="param">参数</param>
    /// <returns></returns>
    public static int Execute(string sql, object param = null)
    {
        return Execute(ConnectionStringConsts.MasterConnectionString, sql, param);
    }

    /// <summary>
    /// 执行事务相关操做
    /// </summary>
    /// <param name="connectionStr">链接字符串</param>
    /// <param name="sql">sql语句</param>
    /// <param name="param">参数</param>
    /// <returns></returns>
    public static int Execute(string connectionStr,string sql,object param=null)
    {
        using (var connection = GetConnection(connectionStr))
        {
            return connection.Execute(sql,param);
        }
    }

    /// <summary>
    /// 事务封装
    /// </summary>
    /// <param name="func">操做</param>
    /// <returns></returns>
    public static bool ExecuteTransaction(Func<IDbConnection, IDbTransaction, int> func)
    {
        return ExecuteTransaction(ConnectionStringConsts.MasterConnectionString, func);
    }

    /// <summary>
    /// 事务封装
    /// </summary>
    /// <param name="connectionStr">链接字符串</param>
    /// <param name="func">操做</param>
    /// <returns></returns>
    public static bool ExecuteTransaction(string connectionStr, Func<IDbConnection, IDbTransaction, int> func)
    {
        using (var conn = GetConnection(connectionStr))
        {
            IDbTransaction trans = conn.BeginTransaction();
            return func(conn, trans)>0;
        }
    }
}

    首先和你们说一句很是抱歉的话,这个类我是随手封装的,并无实验是否可用,由于我本身的电脑并无安装数据库这套环境,可是绝对是能够体现我要讲解的思路,但愿你们多多见谅。
    在这里能够看出来Query 查询方法中咱们传递了一个缺省参数useMaster默认值是false,主要的缘由是, 不少时候咱们可能不能彻底的使用事务性操做走主库,读取操做走从库的状况,也就是咱们有些场景可能要选择性读主库,这时候咱们能够经过这个参数去控制。固然这个字段具体的含义根据你的具体业务实际状况而定,其主要原则就是让更多的操做能命中缺省的状况,好比你大部分读操做都须要去主库,那么你能够设置默认值为true,这样的话特殊状况传递false,这样的话会省下许多操做。若是你大部分读操做都是走从库,只有少数场景须要选择性读主库,那么这个参数你能够设置为false。写就没有这种状况,由于不管哪一种场景写都是要在主库进行的,除非双主的状况,这也不是咱们本次讨论的重点。 app

使用方式

经过上述方式完成封装以后,咱们在具体数据访问层适用的时候能够经过以下方式,若是按照默认的方式查询能够采用以下的方式。在这里关于写的操做咱们就不展现了,由于写的状况是固定的框架

string queryPersonSql = "select id,name from Person where id=@id";
var person = DapperHelper.Query<Person>(queryPersonSql, new { id = 1 }).FirstOrDefault();

若是须要存在特殊状况,查询须要选择主库的话能够不使用缺省参数,咱们能够选择给缺省参数传值,好比我要让查询走主库运维

string queryPersonSql = "select id,name from Person where id=@id";
var person = DapperHelper.Query<Person>(queryPersonSql, new { id = 1 }, true).FirstOrDefault();

固然,咱们上面也提到了,缺省值useMaster是true仍是false,这个彻底能够结合自身的业务决定。若是大部分查询都是走从库的状况下,缺省值能够为false。若是大部分查询状况都是走主库的时候,缺省值能够给true。关于以上全部的相关封装,模式并不固定,这一点能够彻底结合本身的实际业务和代码实现,只是但愿能多给你们提供一种思路,其余ORM也有自身提供了操做读写分离的具体实现。dom

总结

    以上就是笔者关于Dapper实现读写分离的一些我的想法,这种方法也适合其余相似Dapper偏原生SQL操做的ORM框架。这种方式还有一个优势就是若是在现有的项目中,须要支持读写分离的时候,这种操做方式可能对原有代码逻辑,入侵不会那么强,若是你前期封装还比较合理的话,那么改动将会很是小。固然这只是笔者的我的的观点,毕竟具体的实践方式还须要结合实际项目和业务。本次我我的但愿能获得你们更多关于这方面的想法,若是你有更好的实现方式欢迎评论区多多留言。

工具

👇欢迎扫码关注个人公众号👇
相关文章
相关标签/搜索