在什么地方我們做了并發(fā)操作了呢寥院?其實(shí)就是在一個(gè)用戶瀏覽頁(yè)面的同時(shí),還有人在往數(shù)據(jù)庫(kù)里面寫(xiě)入數(shù)據(jù)。你會(huì)發(fā)現(xiàn)thinkPHP這樣的框架涛酗,還是PHPCMS這樣的開(kāi)源系統(tǒng),他們都存在這樣的bug偷厦。
并發(fā)產(chǎn)生的問(wèn)題商叹,往往難以捕捉,更難以重現(xiàn)
比如說(shuō):我們分頁(yè)條目數(shù)是每頁(yè)5條沪哺,在用戶瀏覽某一頁(yè)的時(shí)候沈自,后臺(tái)管理員發(fā)布了新的新聞,新聞的數(shù)量小于5條的情況下辜妓,我們點(diǎn)下一頁(yè)枯途,會(huì)看到有幾條重復(fù)的新聞,也有更早的新聞
這樣影響用戶體驗(yàn)籍滴,但畢竟我們點(diǎn)一下一的頁(yè)時(shí)候內(nèi)容還是能接的上的酪夷,用戶瀏覽某一頁(yè)的時(shí)候,后臺(tái)發(fā)布的新聞數(shù)量大于分頁(yè)條目數(shù)(5條)
那么再點(diǎn)擊下一頁(yè)孽惰,其中會(huì)有若干條新聞被跳過(guò)去了晚岭,無(wú)論用戶點(diǎn)多少次下一頁(yè),都看不到那些條目勋功,這種產(chǎn)生的并發(fā)沖突后果是很?chē)?yán)重的坦报,而且普遍存在。
在大門(mén)戶網(wǎng)站時(shí)代狂鞋,網(wǎng)站編輯并不會(huì)頻繁的發(fā)布新聞片择,而用戶也很少守著新聞列表去逐篇閱讀,然而在微博誕生以后骚揍,這種問(wèn)題就暴露出來(lái)了字管。
由于社區(qū)類(lèi)應(yīng)用信息的產(chǎn)生室友用戶產(chǎn)生的,不在是靠編輯在后臺(tái)發(fā)布的信不,就好像微博嘲叔,每時(shí)每分秒,都有很多用戶分享心得內(nèi)容
這樣一來(lái)你在查看微博列表的同時(shí)抽活,內(nèi)容就已經(jīng)更新 了 很多硫戈,而且很多人打發(fā)無(wú)聊的時(shí)間,很多人也會(huì)去逐一查看酌壕,不停的上劃掏愁,把所有遺漏的條目全部看一遍
這是如果項(xiàng)目設(shè)計(jì)不合理歇由,產(chǎn)生并發(fā)事故,就會(huì)對(duì)用戶體驗(yàn)造成極大的影響果港。
如何解決這種問(wèn)題沦泌??辛掠?谢谦?
我們?cè)谖⑿湃焊愕某楠?jiǎng)的互動(dòng),我們的上百份禮品萝衩,瞬間就被秒殺光回挽。
在做這類(lèi)搶購(gòu)與秒殺抽獎(jiǎng)等應(yīng)用的時(shí)候,并發(fā)將導(dǎo)致更多的問(wèn)題猩谊。通常比較容易出現(xiàn)的bug有實(shí)際商品的訂單量大于庫(kù)存量千劈。
通俗點(diǎn)來(lái)說(shuō)就是,明明已經(jīng)售完牌捷,但還是有用戶買(mǎi)到了商品墙牌,庫(kù)存值變?yōu)樨?fù)的。又或者明明秒殺到商品的用戶暗甥,訂單失敗喜滨。還有企業(yè)的項(xiàng)目,在商品秒殺期間撤防,明明用戶數(shù)量不多虽风,卻導(dǎo)致服務(wù)器宕機(jī)。
還是以商城秒殺業(yè)務(wù)為例寄月。首先我們需要用產(chǎn)品庫(kù)存這樣的一個(gè)字段來(lái)記錄庫(kù)存信息辜膝,每當(dāng)有用戶購(gòu)買(mǎi)商品的時(shí)候,先查看庫(kù)存漾肮,判斷庫(kù)存大于0的時(shí)候内舟,用戶才能購(gòu)買(mǎi)
當(dāng)用戶完成購(gòu)買(mǎi)流程后,將庫(kù)存數(shù)量減一初橘,直到所有商品賣(mài)完,重復(fù)此過(guò)程充岛,直到庫(kù)存賣(mài)完秒殺活動(dòng)結(jié)束保檐。
如果按常規(guī)的思路來(lái)設(shè)計(jì),這樣的流程是沒(méi)有問(wèn)題的崔梗,商品畢竟是一件一件賣(mài)出的夜只,但是,在互聯(lián)網(wǎng)并發(fā)的情況下蒜魄,就完全不是這樣的扔亥。
要知道熱銷(xiāo)商品很有可能在同一時(shí)間场躯,有多個(gè)用戶都在進(jìn)行購(gòu)買(mǎi)流程操作
按照之前的業(yè)務(wù)設(shè)計(jì),假如有ABCD 4個(gè)用戶同時(shí)在秒殺某件商品時(shí)旅挤,庫(kù)存僅剩2件踢关,按照之前的業(yè)務(wù)流程設(shè)計(jì),查詢庫(kù)存大于0粘茄,就可以繼續(xù)后面的購(gòu)買(mǎi)操作并付款
然而當(dāng)任意用戶購(gòu)買(mǎi)成功后庫(kù)存即減一签舞,ABCD4個(gè)用戶都認(rèn)為自己查詢時(shí)都有庫(kù)存,因此他們都可以完成購(gòu)買(mǎi)流程柒瓣,導(dǎo)致的結(jié)果就是庫(kù)存數(shù)為負(fù)數(shù)儒搭。
也就是說(shuō),商品實(shí)際銷(xiāo)售量大于活動(dòng)的商品數(shù)量芙贫,這樣會(huì)導(dǎo)致公司的虧損搂鲫。
有些公司為了解決這個(gè)問(wèn)題,采用了一種思路磺平,雖說(shuō)4個(gè)人同時(shí)操作魂仍,但是交易成功的這次網(wǎng)絡(luò)請(qǐng)求到達(dá)服務(wù)器的時(shí)間總會(huì)有個(gè)先后順序
那么可以將訂單支付成功之后的庫(kù)存減一之后的值也隨訂單保存,如果這個(gè)值小于0褪秀,就證明有用戶購(gòu)買(mǎi)了產(chǎn)品蓄诽,已經(jīng)是賣(mài)完的,于是標(biāo)記訂單失敗媒吗。
這樣看上去避免公司造成額外的損失仑氛,但卻會(huì)給用戶帶來(lái)極大的不滿,是一種極差的用戶體驗(yàn)闸英。它并沒(méi)有真正的解決我們的問(wèn)題锯岖。
當(dāng)然還有些公司解決方案也不高明,我們知道無(wú)論是數(shù)據(jù)庫(kù)還是文件都可以給他加鎖甫何,在很早期的程序設(shè)計(jì)和軟件開(kāi)發(fā)里面出吹,鎖是解決并發(fā)問(wèn)題的萬(wàn)能靈藥。
無(wú)論是c++,或java辙喂,提到多進(jìn)程或多線程的時(shí)候捶牢,往往也會(huì)提到鎖這個(gè)字。那么作為最早期的通用解決方案巍耗,用到秒殺方案是否合適呢秋麸?
我們來(lái)看一下加鎖后的工作流程:還是ABCD 4個(gè)用戶同時(shí)秒殺,他們都去查詢庫(kù)存炬太。當(dāng)某一個(gè)用戶灸蟆,比如A的請(qǐng)求,優(yōu)先到達(dá)時(shí)亲族,我們就將數(shù)據(jù)表鎖住炒考,不讓其他的數(shù)據(jù)庫(kù)連接來(lái)動(dòng)這張表可缚,待用戶A完成購(gòu)買(mǎi)流程,將庫(kù)存量減一后斋枢,把鎖打開(kāi)帘靡,其他的連接才可以再次操作這張表。
如此一來(lái)杏慰,可以保障一個(gè)用戶查看庫(kù)存以及庫(kù)存減一這段時(shí)間內(nèi)测柠,不可能還有其他用戶可以對(duì)表做出修改,這一并發(fā)沖突的問(wèn)題就沒(méi)有了缘滥。不過(guò)這樣的做法真的合適嗎轰胁?
要知道ABCD 4個(gè)用戶都是在同一時(shí)間段去秒殺的,由于A用戶在操作中鎖表朝扼,導(dǎo)致其他用戶只能等待赃阀,而且A完成整個(gè)業(yè)務(wù)需要消耗一段時(shí)間,只能等A完成以后其他用戶才能操作
這樣一來(lái)單位時(shí)間內(nèi)的業(yè)務(wù)處理量會(huì)大幅降低擎颖,我們所看到的現(xiàn)象就是網(wǎng)站卡死榛斯,或者服務(wù)器宕機(jī)
關(guān)于并發(fā)性能如何設(shè)計(jì),我們可能需要單獨(dú)的一次或幾次課來(lái)為大家講解搂捧。不過(guò)鎖這種很原始的并發(fā)沖突解決方案驮俗,我們可以看到他并不適合互聯(lián)網(wǎng)項(xiàng)目。
之所以大家會(huì)有并發(fā)沖突的程序允跑,是因?yàn)榇蟛糠殖绦騿T王凑,思維模式都是線性的。
作為程序邏輯思維來(lái)講聋丝,線性思維是沒(méi)有錯(cuò)的索烹,因?yàn)橛?jì)算機(jī)執(zhí)行指令的時(shí)候本身就是線性的。然而如果把業(yè)務(wù)也看做是線性的弱睦,就會(huì)產(chǎn)生問(wèn)題了百姓。
任何一個(gè)程序操作,他都會(huì)消耗一定的時(shí)間况木,即便你的CPU速度再快垒拢,也只是縮短了這個(gè)時(shí)間范圍而已,
如果只有一個(gè)用戶操作火惊,比如我們?cè)诤笈_(tái)發(fā)布文章子库,看自己發(fā)布的新聞,我們是無(wú)法感知并發(fā)帶來(lái)的沖突的矗晃。這就對(duì)我們的程序員提出了更高的要求。
理論上來(lái)講宴倍,所有跨越時(shí)間段的操作過(guò)程中如果涉及到數(shù)據(jù)修改就會(huì)有可能產(chǎn)生并發(fā)沖突张症,因此我們?cè)谠O(shè)計(jì)程序的時(shí)候仓技,要保障應(yīng)用程序的質(zhì)量,就需要去做并發(fā)沖突處理俗他,只是實(shí)現(xiàn)業(yè)務(wù)需求與實(shí)現(xiàn)業(yè)務(wù)的同時(shí)做好質(zhì)量需求脖捻,就是好程序員與壞程序員的差別。
那么分析了產(chǎn)生并發(fā)沖突的原因以后兆衅,就比較容易思考解決方案了地沮。大體的思路有兩種:一種是將并發(fā)操作變?yōu)閱尉€操作,另一種是讓所有跨越時(shí)間的段的操作不去更改數(shù)據(jù)羡亩。
我們現(xiàn)在來(lái)看一下分頁(yè)摩疑,或者上拉或者下拉刷新的解決方法。我們剛剛提出的2種的解決思路畏铆,哪一種比較合適呢雷袋?對(duì)于發(fā)布數(shù)據(jù)和瀏覽數(shù)據(jù),比如微博辞居,我們有可能把這種并發(fā)操作變?yōu)閱尉€操作嗎楷怒?好像不太容易。
那么我們能夠走得路就剩下第二條瓦灶,也就是跨時(shí)間段的過(guò)程中不要改變數(shù)據(jù)鸠删,我們剛剛產(chǎn)生的bug到底是什么數(shù)據(jù)改變導(dǎo)致了bug≡籼眨回顧下我們的代碼實(shí)現(xiàn)的本質(zhì)刃泡,就容易找到其中的緣由了。
通常我們?cè)趯?shí)現(xiàn)分頁(yè)的時(shí)候每界,首頁(yè)看到的是最新的數(shù)據(jù)捅僵,那么從數(shù)據(jù)庫(kù)中取數(shù)據(jù)的sel語(yǔ)句是select * from news order by desc limt 0,10眨层,這樣取到最新的數(shù)據(jù)庙楚,如果點(diǎn)擊下一頁(yè),查詢語(yǔ)句不變趴樱,只是分頁(yè)條目不在是第0-9馒闷,而是第10-19條,如果在這個(gè)過(guò)程中有新的數(shù)據(jù)插入叁征,我們會(huì)發(fā)現(xiàn)有一個(gè)東西變了纳账,就是原有數(shù)據(jù)在數(shù)據(jù)庫(kù)的排序序號(hào)變了,如果我新發(fā)布一條數(shù)據(jù)捺疼,原來(lái)的第一條最新的新聞就會(huì)變成第二條疏虫,原來(lái)的第10條會(huì)變成第11條。這就是一個(gè)時(shí)間段內(nèi)的操作過(guò)程中有數(shù)據(jù)發(fā)生了改變。
既然我們無(wú)法把這樣的并發(fā)操作變成單線操作卧秘,我們可以選擇不讓數(shù)據(jù)發(fā)生改變呢袱,這樣并發(fā)bug就可以得到很好的解決了。
需要了解詳細(xì)解決方案的翅敌,我會(huì)把無(wú)bug程序?qū)嵗窒斫o大家羞福。課后可以聯(lián)系赫赫要資料,或者是聽(tīng)我們的視屏直播課蚯涮,陳老師有詳細(xì)的解決治专。
跨時(shí)間段的讓數(shù)據(jù)不改變不好走,那我們可以選擇第一種思路遭顶,讓并發(fā)操作變?yōu)閱尉€操作张峰,之前提到的加鎖是解決方案之一,但是對(duì)用戶體驗(yàn)不好性能很差液肌,基本上無(wú)法再互聯(lián)網(wǎng)項(xiàng)目中使用挟炬。如果不能加鎖,那么常用的解決方案是什么嗦哆?
我們可以用隊(duì)列谤祖。如果我們將所有的用戶請(qǐng)求進(jìn)行排隊(duì),有一個(gè)服務(wù)來(lái)訂閱這個(gè)隊(duì)列老速,那么不管有多少用戶訪問(wèn)粥喜,最終到服務(wù)器端,處理服務(wù)的就只有一個(gè)進(jìn)程橘券。這樣就實(shí)現(xiàn)了一個(gè)由并發(fā)操作轉(zhuǎn)換成單線操作额湘。