關於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框架。這種方式還有一個優勢就是若是在現有的項目中,須要支持讀寫分離的時候,這種操做方式可能對原有代碼邏輯,入侵不會那麼強,若是你前期封裝還比較合理的話,那麼改動將會很是小。固然這只是筆者的我的的觀點,畢竟具體的實踐方式還須要結合實際項目和業務。本次我我的但願能獲得你們更多關於這方面的想法,若是你有更好的實現方式歡迎評論區多多留言。

工具

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索