淺解.Net分散式鎖的實現

張龍豪發表於2017-03-01

序言 

我晚上有在公司多呆會兒的習慣,所以很多晚上我都是最後一個離開公司的。當然也有一些同事,跟我一樣喜歡在公司多搞會兒。這篇文章就要從,去年年末一個多搞會的晚上說起,那是一個夜黑風高的晚上,公司應該沒有幾個人在啦,我司一技術男悠悠的走到我的背後,突然一句:“還沒走啊?”!“我日,嚇死我啦,你也沒走啊”。此同事現在已被裁員,走啦,當晚他問我啦一個問題,至此時也沒有機會告知,今天我就在這裡就簡單描述下他當時的問題,其實實現起來簡單的不值一提,不過任何一個簡單的問題往往都會有很多中解決方案,探索找到最佳的解決方案,然後把細節做好,那就是技術的精髓與樂趣所在。我這裡只拋磚一下,希望能給我老同事一個思路。

回到問題

首先有如下二張表,欄位有IsBuyed(0:未使用,1:已使用),ProductNo:產品編號,Count:使用次數。

 

就是針對這張表做需求擴充套件的。

1、每次請求過來,都隨機拿到一個未使用過的產品編號

       public int GetNo()
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.ExecuteScalar<int>("select top 1 ProductNo from  AStore where isBuyed=0 order by newid()");
            }
        }

2、每次請求過來,即為使用產品一次,使用未使用過的產品一次需產品的IsBuyed=1 , Count=Count+1 。

 public bool UsingStore(int no)
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.Execute("update AStore set isBuyed=1  where  and productNo=" + no) > 0;
            }
        }
        public bool MinusStore(int no)
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.Execute("update BStore set [count]=[count]+1 where  and productNo=" + no) > 0;
            }
        }

3、寫一個介面,部署在叢集環境中,模擬請求3秒內一萬個請求,來消費表中只有10個的產品,最終結果為產品不能被多次使用,如果存在多次使用則產品的count將大於1,即為失敗。同學如果你看到啦,問題我給你復原的跟你說的沒多少出入吧?

.Net實現分散式鎖

解決問題我就一步步來遞進,慢慢深入,直至痛楚!!首先我把同事運算元據上面的2個方法先貼出來。

public bool UsingStore(int no)
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.Execute("update AStore set isBuyed=1  where productNo=" + no) > 0;
            }
        }
        public bool MinusStore(int no)
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.Execute("update BStore set [count]=[count]+1 where  productNo=" + no) > 0;
            }
        }
        public int GetNo()
        {
            using (IDbConnection conn = GetConn())
            {
                return conn.ExecuteScalar<int>("select top 1 ProductNo from  AStore where isBuyed=0 order by newid()");
            }
        }

初涉茅廬的同學可能會這樣寫介面。 

 public JsonResult Using1()
        {
            //獲取未使用的產品編號
            var no = data.GetNo();
            if (no != 0)
            {
                //使用此產品
                data.MinusStore(no);
                data.UsingStore(no);
                return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
            }
            else
            {
                //無產品可使用
                return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
            }
        }

單機部署,1萬個請求過來下啊。下面我們看看資料庫的結果是什麼?下面是3次實現結果。每執行一次,執行一下下面的指令碼。

select * from [dbo].[AStore]
update AStore set isbuyed=0,count=0

表:astore 表:bstore 

由結果可以看出,單機部署介面的情況下,還使一些產品被多次消費,這很顯然不符合同學的要求。

那麼我們進一步改進這個介面,使用單機鎖,鎖此方法,來實現此介面,如下。

 public JsonResult Using()
        {
            string key = "%……¥%¥%77123嗎,bnjhg%……%……&+orderno";
            //鎖此操作          
            lock (key)
            {
                //獲取未使用的產品編號
                var no = data.GetNo();
                if (no != 0)
                {
                    //使用此產品
                    data.MinusStore(no);
                    data.UsingStore(no);
                    return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }
                else
                {
                    //此產品已使用過
                    return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }
            }
        }

單機部署此介面,1000個請求來測試此介面

結果如下:

表:astore表:bstore 

哇塞,貌似同事的問題解決啦,哈哈,同事不急,這只是單機部署下的結果,如果把這個介面叢集部署的話是什麼結果呢?

使用nginx做叢集部署,搞5個站點做測試,對得起嗎,同事?

 upstream servers{
       server 192.168.10.150:9000 weight=1; 
       server 172.18.11.79:1112 weight=1;
       server 192.168.10.150:1114 weight=1;
       server 192.168.10.150:1115 weight=1;
       server 192.168.10.150:1116 weight=1;  
 }
 server{
      keepalive_requests 1200;
      listen 8080;
      server_name abc.nginx3.com;
      location ~*^.+$ { 
          proxy_pass http://servers;
        }
}

再來看此介面執行的結果。結果如下:

表:astore表:bstore 

由圖可以看出,站點部署的叢集對的住你,結果可令我們不滿意啊,顯然一個產品還是存在多次消費的情況,這種鎖對叢集部署無用,並且還要排隊,效能也跟不上來。我們來進一步改寫這個介面。如下:

 public JsonResult Using3()
        {
            //鎖此操作
            string key = "%……¥%¥%77123嗎,bnjhg%……%……&+orderno";
            lock (key)
            {
                //獲取未使用的產品編號
                var no = data.GetNo();
                //單號做為key插入memcached,值為true。
                var getResult = AMemcached.cache.Add("Miaodan_ProNo:" + no, true);
                if (getResult)
                {
                    //使用此產品
                    data.MinusStore(no);
                    data.UsingStore(no);
                    return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }
                else
                {
                    //此產品已使用過
                    return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }               
            }
        }

在叢集下跑此介面看結果,結果如下。

表:astore表:bstore 

功能實現,同事可以安息啦。不過這裡還有很多優化,和分散式鎖帶來的弊端,比如一單被分散式鎖,鎖住業務即便後續演算法沒有使用該產品,怎麼優雅的釋放鎖,怎麼解決遇到已經使用過的產品後再此分配新資源等等,當然也有其他一些實現方案,比如基於redis,zookeeper實現的分散式鎖,我這裡就不說明啦。同事,你好自珍重,祝多生孩子,多掙錢啊。

總結

接下來是大家最喜歡的總結內容啦,內容有二,如下:

1、希望能關注我其他的文章。

2、部落格裡面有沒有很清楚的說明白,或者你有更好的方式,那麼歡迎加入左上方的2個交流群,我們一起學習探討。

相關文章