項目演化系列--分佈式鎖

前言git

  項目初期的時候,通常會發布到一臺主機上,當達到負載極限時,要想提高其性能,要麼提高硬件,要麼多臺主機,然而成本上的花銷,後者比前者便宜太多了,雖然便宜,可是卻更加複雜。github

  大多數編程語言提供的各類鎖只會對同一項目的同一主機的代碼產生做用,當同一項目發佈在多臺主機的時候,這些主機中的項目要造成一個總體,所以原先同步訪問共享資源的代碼將會失去效果。redis

  因爲共享資源多種多樣,如:文件、業務的臨時狀態、數據庫數據等,本章的同步鎖主要解決的是不依賴於主機環境的共享資源,如:數據庫數據;而共享資源依賴於項目環境時,想要同步訪問共享資源,則當某主機共享資源變更時,須要將其同步到其餘主機,也就是集羣服務器了,若是不想要搭建集羣服務器,可將相應的功能剝離出來成爲單一的項目,也就是分佈式結構。sql

  因爲後期必然會演變成分佈式架構,而各個結構又是集羣,所以若是當前狀況下就把項目構架得太多複雜,投入再多的人力也是很難完成的,所以要先簡化結構,一步步實現,至於先集羣仍是先分佈,看我的喜愛了。數據庫

實現編程

  實現的主要目標就是保證任意時刻,只能有一個線程能夠獲得操做的權利。api

  首先來定義鎖的接口,能夠提供2個方法:Lock、Unlock,也能夠只提供Lock,而後返回Unlock,若是Unlock爲null則表示加鎖失敗。緩存

  既然講到惟一,若是不依賴其餘的額外資源的狀況下,不少人應該已經想到了,那就是數據庫表的主鍵,所以實現思路就是加鎖的時候向數據庫中插入一條記錄,那麼成功插入的操做就獲取到了鎖,而後解鎖時,刪除這條記錄便可,初步實現以下:安全

public delegate void UnlockDelegate();

public UnlockDelegate Lock(string key)
{
    using (var conn = new SqlConnection(this.connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand(string.Empty, conn))
        {
            try
            {
                var createdRows = Create(cmd, key);
                if (createdRows > 0)
                {
                    return () =>
                    {
                        DeleteById(cmd, key);
                        conn.Close();
                    };
                }
            }
            catch
            {
                conn.Close();
            }
            return null;
        }
    }
}

private int Create(SqlCommand cmd, string key)
{
    cmd.Parameters.Clear();
    cmd.CommandText = this.insertSql;
    cmd.Parameters.AddWithValue("@id", key);
    return cmd.ExecuteNonQuery();
}

private int DeleteById(SqlCommand cmd, string key)
{
    cmd.Parameters.Clear();
    cmd.CommandText = this.deleteSql;
    cmd.Parameters.AddWithValue("@id", key);
    return cmd.ExecuteNonQuery();
}

  項目運行過程中沒有絕對的安全,總有一些內因、外因致使項目出現錯誤,若是某個主機獲取了鎖之後,該主機由於某些緣由沒有釋放鎖,那麼其餘的主機將會沒法再獲取到該鎖了。服務器

  那麼鎖就須要一個過時時間,所以咱們須要在表中增長一個表示鎖的建立時間,那麼在建立鎖以前就須要先根據key去獲取鎖是否存在,若是存在且已通過期,那麼刪除該記錄才能繼續建立鎖。

  該處的刪除跟解鎖時的刪除是不同的,由於在多線程、併發環境下,程序並不能保證只有惟一一個線程獲取到了已存在的鎖數據,有可能多個線程都獲取到了鎖數據,有的可能已經準備刪除,而有的纔剛剛獲取到,所以此處的刪除必須保證返回的影響行數大於0,不然直接返回null,重構後的代碼以下:

public UnlockDelegate Lock(string key, int expires = 5)
{
    using (var conn = new SqlConnection(this.connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand(string.Empty, conn))
        {
            var createdOn = GetCreatedOnById(cmd, key);
            if (createdOn > 0)
            {
                var nowOn = DateTime.Now.ToUnix();
                if (nowOn - createdOn > expires)
                {
                    var deletedRow = DeleteById(cmd, key);
                    if (deletedRow == 0)
                    {
                        conn.Close();
                        return null;
                    }
                }
            }

            try
            {
                var createdRows = Create(cmd, key, expires);
                if (createdRows > 0)
                {
                    return () =>
                    {
                        DeleteById(cmd, key);
                        conn.Close();
                    };
                }
            }
            catch
            {
                conn.Close();
            }
            return null;
        }
    }
}

  因爲DateTime並無直接轉換成時間戳的方法,所以該方法須要本身擴展,實現思路就是當前時間-1970年的總毫秒數,這裏就不提供代碼了,由於長時間都是依賴於orm來開發的,對sql已經很生疏了,所以各位要的是理解以上實現,不要太在乎代碼。

簡化

  使用數據庫來實現雖然代碼量很少,但須要數據庫的支持,鏈接字符串、表、字段都是可變的,若是不寫死的話,就須要提供很多的配置。

  因爲項目必然會使用到緩存,如:redis、memcache等高性能的緩存系統,而redis中提供了SetNX、Expires這樣的api,若是基於redis實現的話,只要幾行代碼即可完成。

  相應的庫能夠去redis官網查詢,這裏的例子使用的是Sider,代碼以下:

private ThreadwisePool pool;

public RedisMutex(string host)
{
    this.pool = new ThreadwisePool(host);
}

public UnlockDelegate Lock(string key, int expires = 5)
{
    var client = this.pool.GetClient();
    var ok = client.SetNX(key, string.Empty);
    if (!ok)
        return null;

    client.Expire(key, new TimeSpan(0, 0, expires));
    return () => client.Del(key);
}

結束語

  那麼今天分享的文章就到這裏了,若是代碼有錯誤或者有問題的話,請留言,謝謝。

相關文章
相關標籤/搜索