序言
我晚上有在公司多呆會兒的習慣,是以很多晚上我都是最後一個離開公司的。當然也有一些同僚,跟我一樣喜歡在公司多搞會兒。這篇文章就要從,去年年末一個多搞會的晚上說起,那是一個夜黑風高的晚上,公司應該沒有幾個人在啦,我司一技術男悠悠的走到我的背後,突然一句:“還沒走啊?”!“我日,吓死我啦,你也沒走啊”。此同僚現在已被裁員,走啦,當晚他問我啦一個問題,至此時也沒有機會告知,今天我就在這裡就簡單描述下他當時的問題,其實實作起來簡單的不值一提,不過任何一個簡單的問題往往都會有很多中解決方案,探索找到最佳的解決方案,然後把細節做好,那就是技術的精髓與樂趣所在。我這裡隻抛磚一下,希望能給我老同僚一個思路。
回到問題
首先有如下二張表,字段有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
哇塞,貌似同僚的問題解決啦,哈哈,同僚不急,這隻是單機部署下的結果,如果把這個接口叢集部署的話是什麼結果呢?
使用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;
}
}
再來看此接口運作的結果。結果如下:
表: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);
}
}
}
在叢集下跑此接口看結果,結果如下。
功能實作,同僚可以安息啦。不過這裡還有很多優化,和分布式鎖帶來的弊端,比如一單被分布式鎖,鎖住業務即便後續算法沒有使用該産品,怎麼優雅的釋放鎖,怎麼解決遇到已經使用過的産品後再此配置設定新資源等等,當然也有其他一些實作方案,比如基于redis,zookeeper實作的分布式鎖,我這裡就不說明啦。同僚,你好自珍重,祝多生孩子,多掙錢啊。
總結
接下來是大家最喜歡的總結内容啦,内容有二,如下:
1、希望能關注我其他的文章。
2、部落格裡面有沒有很清楚的說明白,或者你有更好的方式,那麼歡迎加入左上方的2個交流群,我們一起學習探讨。