關於Dapper實現讀寫分離的個人思考

yi念之間發表於2020-08-13

概念相關

    為了確保多線上環境資料庫的穩定性和可用性,大部分情況下都使用了雙機熱備的技術。一般是一個主庫+一個從庫或者多個從庫的結構,從庫的資料來自於主庫的同步。在此基礎上我們可以通過資料庫反向代理工具或者使用程式的方式實現讀寫分離,即主庫接受事務性操作比如刪除、修改、新增等操作,從庫接受讀操作。筆者自認為讀寫分離解決的痛點是,資料庫讀寫負載非常高的情況下,單點資料庫存在讀寫衝突,從而導致資料庫壓力過大,出現讀寫操作緩慢甚至出現死鎖或者拒絕服務的情況。它適用與讀大於寫,並可以容忍一段時間內不一致的情況,因為主從同步存在一定的延遲,大致的實現架構圖如下(圖片來自於網路)。
資料庫主從架構    雖然我們可以通過資料庫代理實現讀寫分離,比如mycat,這類方案的優勢就是對程式本身沒有入侵,通過代理本身來攔截sql語句分發到具體資料。甚至是更好的解決方案NewSQL去解決,比如TiDB。但是還是那個原則,無論使用資料庫代理或者NewSQL的情況都是比較重型的解決方案,會增加服務節點和運維成本,有時候還沒到使用這些終極解決方案的地步,這時候我們會在程式中處理讀寫分離,所以有個好的思路去在程式中解決讀寫分離也尤為重要。

基本結構

接下來我們新建三個類,當然這個並不固定,可以根據自己的情況新建類。首先我們新建一個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。寫就沒有這種情況,因為無論哪種場景寫都是要在主庫進行的,除非雙主的情況,這也不是我們本次討論的重點。

使用方式

通過上述方式完成封裝之後,我們在具體資料訪問層適用的時候可以通過如下方式,如果按照預設的方式查詢可以採用如下的方式。在這裡關於寫的操作我們就不展示了,因為寫的情況是固定的

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也有自身提供了操作讀寫分離的具體實現。

總結

    以上就是筆者關於Dapper實現讀寫分離的一些個人想法,這種方法也適合其他類似Dapper偏原生SQL操作的ORM框架。這種方式還有一個優點就是如果在現有的專案中,需要支援讀寫分離的時候,這種操作方式可能對原有程式碼邏輯,入侵不會那麼強,如果你前期封裝還比較合理的話,那麼改動將會非常小。當然這只是筆者的個人的觀點,畢竟具體的實踐方式還需要結合實際專案和業務。本次我個人希望能得到大家更多關於這方面的想法,如果你有更好的實現方式歡迎評論區多多留言。

?歡迎掃碼關注我的公眾號? 關於Dapper實現讀寫分離的個人思考

相關文章