互聯(lián)網(wǎng)線上項(xiàng)目開發(fā)最大坑點(diǎn)-并發(fā)沖突處理
大家可能都有這樣的經(jīng)驗(yàn)蚯嫌,自個兒在家里很多功能很容易實(shí)現(xiàn)渠概,一下就做完了,但是在做線上產(chǎn)品的時候功茴,就變得無比復(fù)雜庐冯,需要花費(fèi)很多的時間。
自己寫的程序在家跑坎穿,所有的業(yè)務(wù)都很正常展父,一旦發(fā)布到線上,就會出現(xiàn)很多bug玲昧,而且很多bug在測試的時候很難重現(xiàn)栖茉,這是在互聯(lián)網(wǎng)開發(fā)的時候經(jīng)常遇到的現(xiàn)象。
這些難以重現(xiàn)的bug孵延,大部分是由于并發(fā)產(chǎn)生的吕漂,為了能讓大家充分的了解并發(fā)的問題,并且建立并發(fā)環(huán)境下的程序設(shè)計(jì)思維尘应,我們?yōu)榇蠹覝?zhǔn)備了幾個小案例
大家來看一下惶凝,這個網(wǎng)站典型的場景,遇到內(nèi)容比較多的情況下犬钢,我們會使用分頁
如果是在移動端苍鲜,我們采用的是上拉刷新和下拉刷新,比如手機(jī)微博
分頁功能由來已久玷犹,我們現(xiàn)在來看下常見的分頁有什么問題呢混滔?
大家看下我展示的這個項(xiàng)目是用thinkPHP來編寫的。很多流行的框架和開源項(xiàng)目都對分頁做了支持歹颓,那么大家來看我演示一個案例坯屿。
看到的現(xiàn)在的第一頁是最新的新聞,如果我點(diǎn)擊下一頁巍扛,相對來說是老一點(diǎn)的新聞领跛。
這個時候我們來做一件事情,我們新打開一個瀏覽器窗口撤奸,剛剛的分頁頁面不要關(guān)閉隔节,我們在后臺里面再發(fā)布5篇新聞,
假設(shè)我們在后臺發(fā)布新聞的同時寂呛,用戶還在瀏覽第二頁怎诫,當(dāng)發(fā)布完畢以后,用戶又點(diǎn)了下一頁贷痪,我們觀察到了奇特的現(xiàn)象
我們看到第3頁和剛剛看到的第二頁完全一樣幻妓,原本用戶點(diǎn)下一頁希望看到再早一些的新聞,結(jié)果看到了一樣的數(shù)據(jù),是我們的分頁程序出問題了嗎肉津?非也强胰。
當(dāng)我們再點(diǎn)下一頁,程序又正常工作了妹沙。剛剛出現(xiàn)的奇怪的那一幕偶洋,其實(shí)就是由于并發(fā)產(chǎn)生的沖突。
在什么地方我們做了并發(fā)操作了呢距糖?其實(shí)就是在一個用戶瀏覽頁面的同時玄窝,還有人在往數(shù)據(jù)庫里面寫入數(shù)據(jù)。你會發(fā)現(xiàn)thinkPHP這樣的框架悍引,還是PHPCMS這樣的開源系統(tǒng)恩脂,他們都存在這樣的bug。
并發(fā)產(chǎn)生的問題趣斤,往往難以捕捉俩块,更難以重現(xiàn),而我們準(zhǔn)備的這個案例浓领,算是并發(fā)案例沖突中相對容易重新的典型案例
我們?nèi)绻{(diào)整一下剛剛操作的順序玉凯,我們會得到一些其他的結(jié)果。
比如說:我們分頁條目數(shù)是每頁5條联贩,在用戶瀏覽某一頁的時候漫仆,后臺管理員發(fā)布了新的新聞,新聞的數(shù)量小于5條的情況下撑蒜,我們點(diǎn)下一頁歹啼,會看到有幾條重復(fù)的新聞玄渗,也有更早的新聞
這樣影響用戶體驗(yàn)座菠,但畢竟我們點(diǎn)一下一的頁時候內(nèi)容還是能接的上的,用戶瀏覽某一頁的時候藤树,后臺發(fā)布的新聞數(shù)量大于分頁條目數(shù)(5條)
那么再點(diǎn)擊下一頁浴滴,其中會有若干條新聞被跳過去了,無論用戶點(diǎn)多少次下一頁岁钓,都看不到那些條目升略,這種產(chǎn)生的并發(fā)沖突后果是很嚴(yán)重的,而且普遍存在屡限。
那么再點(diǎn)擊下一頁品嚣,其中會有若干條新聞被跳過去了,無論用戶點(diǎn)多少次下一頁钧大,都看不到那些條目翰撑,這種產(chǎn)生的并發(fā)沖突后果是很嚴(yán)重的,而且普遍存在啊央。
他具有很好的隱蔽性眶诈,在過去很多年幾乎不被察覺
在大門戶網(wǎng)站時代涨醋,網(wǎng)站編輯并不會頻繁的發(fā)布新聞,而用戶也很少守著新聞列表去逐篇閱讀逝撬,然而在微博誕生以后浴骂,這種問題就暴露出來了。
由于社區(qū)類應(yīng)用信息的產(chǎn)生室友用戶產(chǎn)生的宪潮,不在是靠編輯在后臺發(fā)布的溯警,就好像微博,每時每分秒坎炼,都有很多用戶分享心得內(nèi)容
這樣一來你在查看微博列表的同時愧膀,內(nèi)容就已經(jīng)更新 了 很多,而且很多人打發(fā)無聊的時間谣光,很多人也會去逐一查看檩淋,不停的上劃,把所有遺漏的條目全部看一遍
這是如果項(xiàng)目設(shè)計(jì)不合理萄金,產(chǎn)生并發(fā)事故蟀悦,就會對用戶體驗(yàn)造成極大的影響。
待會我會再講并發(fā)沖突解決方案的時候告訴大家如何解決這種問題
現(xiàn)在我們來看一個更為常見的例子氧敢,大家可能還記得日戈,我們在微信群搞的抽獎的互動,我們的上百份禮品孙乖,瞬間就被秒殺光浙炼。
在做這類搶購與秒殺抽獎等應(yīng)用的時候,并發(fā)將導(dǎo)致更多的問題唯袄。通常比較容易出現(xiàn)的bug有實(shí)際商品的訂單量大于庫存量弯屈。
通俗點(diǎn)來說就是,明明已經(jīng)售完恋拷,但還是有用戶買到了商品资厉,庫存值變?yōu)樨?fù)的。
又或者明明秒殺到商品的用戶蔬顾,訂單失敗宴偿。
還有企業(yè)的項(xiàng)目,在商品秒殺期間诀豁,明明用戶數(shù)量不多窄刘,卻導(dǎo)致服務(wù)器宕機(jī)。諸如此類的問題就不一一列舉了舷胜。
由于是文字直播娩践,打字速度比較慢,大家可以看現(xiàn)在正在進(jìn)行的視屏直播
我們接下來來看一下用常規(guī)思維來梳理業(yè)務(wù)流程程序是怎樣編寫的。
還是以商城秒殺業(yè)務(wù)為例欺矫。首先我們需要用產(chǎn)品庫存這樣的一個字段來記錄庫存信息纱新,每當(dāng)有用戶購買商品的時候,先查看庫存穆趴,判斷庫存大于0的時候脸爱,用戶才能購買
當(dāng)用戶完成購買流程后,將庫存數(shù)量減一未妹,直到所有商品賣完簿废,重復(fù)此過程,直到庫存賣完秒殺活動結(jié)束络它。
如果按常規(guī)的思路來設(shè)計(jì)族檬,這樣的流程是沒有問題的,商品畢竟是一件一件賣出的化戳,但是单料,在互聯(lián)網(wǎng)并發(fā)的情況下,就完全不是這樣的点楼。
要知道熱銷商品很有可能在同一時間扫尖,有多個用戶都在進(jìn)行購買流程操作
按照之前的業(yè)務(wù)設(shè)計(jì),假如有ABCD 4個用戶同時在秒殺某件商品時掠廓,庫存僅剩2件换怖,按照之前的業(yè)務(wù)流程設(shè)計(jì),查詢庫存大于0蟀瞧,就可以繼續(xù)后面的購買操作并付款
然而當(dāng)任意用戶購買成功后庫存即減一沉颂,ABCD4個用戶都認(rèn)為自己查詢時都有庫存,因此他們都可以完成購買流程悦污,導(dǎo)致的結(jié)果就是庫存數(shù)為負(fù)數(shù)铸屉。
也就是說,商品實(shí)際銷售量大于活動的商品數(shù)量塞关,這樣會導(dǎo)致公司的虧損抬探。
有些公司為了解決這個問題子巾,采用了一種思路帆赢,雖說4個人同時操作,但是交易成功的這次網(wǎng)絡(luò)請求到達(dá)服務(wù)器的時間總會有個先后順序
那么可以將訂單支付成功之后的庫存減一之后的值也隨訂單保存线梗,如果這個值小于0椰于,就證明有用戶購買了產(chǎn)品,已經(jīng)是賣完的仪搔,于是標(biāo)記訂單失敗瘾婿。
這樣看上去避免公司造成額外的損失,但卻會給用戶帶來極大的不滿,是一種極差的用戶體驗(yàn)偏陪。它并沒有真正的解決我們的問題抢呆。
當(dāng)然還有些公司解決方案也不高明,我們知道無論是數(shù)據(jù)庫還是文件都可以給他加鎖笛谦,在很早期的程序設(shè)計(jì)和軟件開發(fā)里面抱虐,鎖是解決并發(fā)問題的萬能靈藥。
無論是c++,或java饥脑,提到多進(jìn)程或多線程的時候恳邀,往往也會提到鎖這個字。那么作為最早期的通用解決方案灶轰,用到秒殺方案是否合適呢谣沸?
我們來看一下加鎖后的工作流程:還是ABCD 4個用戶同時秒殺,他們都去查詢庫存笋颤。當(dāng)某一個用戶乳附,比如A的請求,優(yōu)先到達(dá)時伴澄,我們就將數(shù)據(jù)表鎖住许溅,不讓其他的數(shù)據(jù)庫連接來動這張表,待用戶A完成購買流程秉版,將庫存量減一后贤重,把鎖打開,其他的連接才可以再次操作這張表清焕。
如此一來并蝗,可以保障一個用戶查看庫存以及庫存減一這段時間內(nèi),不可能還有其他用戶可以對表做出修改秸妥,這一并發(fā)沖突的問題就沒有了滚停。不過這樣的做法真的合適嗎?
要知道ABCD 4個用戶都是在同一時間段去秒殺的粥惧,由于A用戶在操作中鎖表键畴,導(dǎo)致其他用戶只能等待,而且A完成整個業(yè)務(wù)需要消耗一段時間突雪,只能等A完成以后其他用戶才能操作
這樣一來單位時間內(nèi)的業(yè)務(wù)處理量會大幅降低起惕,我們所看到的現(xiàn)象就是網(wǎng)站卡死,或者服務(wù)器宕機(jī)
關(guān)于并發(fā)性能如何設(shè)計(jì)咏删,我們可能需要單獨(dú)的一次或幾次課來為大家講解惹想。不過鎖這種很原始的并發(fā)沖突解決方案,我們可以看到他并不適合互聯(lián)網(wǎng)項(xiàng)目督函。
之所以大家會有并發(fā)沖突的程序嘀粱,是因?yàn)榇蟛糠殖绦騿T激挪,思維模式都是線性的。
作為程序邏輯思維來講锋叨,線性思維是沒有錯的垄分,因?yàn)橛?jì)算機(jī)執(zhí)行指令的時候本身就是線性的。然而如果把業(yè)務(wù)也看做是線性的娃磺,就會產(chǎn)生問題了锋喜。
任何一個程序操作,他都會消耗一定的時間豌鸡,即便你的CPU速度再快嘿般,也只是縮短了這個時間范圍而已,..
如果只有一個用戶操作涯冠,比如我們在后臺發(fā)布文章炉奴,看自己發(fā)布的新聞,我們是無法感知并發(fā)帶來的沖突的蛇更。這就對我們的程序員提出了更高的要求瞻赶。
理論上來講,所有跨越時間段的操作過程中如果涉及到數(shù)據(jù)修改就會有可能產(chǎn)生并發(fā)沖突派任,因此我們在設(shè)計(jì)程序的時候砸逊,要保障應(yīng)用程序的質(zhì)量,就需要去做并發(fā)沖突處理掌逛,只是實(shí)現(xiàn)業(yè)務(wù)需求與實(shí)現(xiàn)業(yè)務(wù)的同時做好質(zhì)量需求师逸,就是好程序員與壞程序員的差別。
那么分析了產(chǎn)生并發(fā)沖突的原因以后豆混,就比較容易思考解決方案了篓像。大體的思路有兩種:一種是將并發(fā)操作變?yōu)閱尉€操作,另一種是讓所有跨越時間的段的操作不去更改數(shù)據(jù)皿伺。
我們現(xiàn)在來看一下分頁员辩,或者上拉或者下拉刷新的解決方法。我們剛剛提出的2種的解決思路鸵鸥,哪一種比較合適呢奠滑?對于發(fā)布數(shù)據(jù)和瀏覽數(shù)據(jù),比如微博妒穴,我們有可能把這種并發(fā)操作變?yōu)閱尉€操作嗎宋税?好像不太容易。
那么我們能夠走得路就剩下第二條宰翅,也就是跨時間段的過程中不要改變數(shù)據(jù)弃甥,我們剛剛產(chǎn)生的bug到底是什么數(shù)據(jù)改變導(dǎo)致了bug爽室≈希回顧下我們的代碼實(shí)現(xiàn)的本質(zhì)淆攻,就容易找到其中的緣由了。
通常我們在實(shí)現(xiàn)分頁的時候嘿架,首頁看到的是最新的數(shù)據(jù)瓶珊,那么從數(shù)據(jù)庫中取數(shù)據(jù)的sel語句是select * from news order by desc limt 0,10耸彪,這樣取到最新的數(shù)據(jù)伞芹,如果點(diǎn)擊下一頁,查詢語句不變蝉娜,只是分頁條目不在是第0-9唱较,而是第10-19條,如果在這個過程中有新的數(shù)據(jù)插入召川,我們會發(fā)現(xiàn)有一個東西變了南缓,就是原有數(shù)據(jù)在數(shù)據(jù)庫的排序序號變了,如果我新發(fā)布一條數(shù)據(jù)荧呐,原來的第一條最新的新聞就會變成第二條汉形,原來的第10條會變成第11條。這就是一個時間段內(nèi)的操作過程中有數(shù)據(jù)發(fā)生了改變倍阐。
既然我們無法把這樣的并發(fā)操作變成單線操作概疆,我們可以選擇不讓數(shù)據(jù)發(fā)生改變,這樣并發(fā)bug就可以得到很好的解決了峰搪。
需要了解詳細(xì)解決方案的岔冀,我會把無bug程序?qū)嵗窒斫o大家。課后可以聯(lián)系赫赫要資料概耻,或者是聽我們的視屏直播課楣颠,陳老師有詳細(xì)的解決。
跨時間段的讓數(shù)據(jù)不改變不好走咐蚯,那我們可以選擇第一種思路童漩,讓并發(fā)操作變?yōu)閱尉€操作,之前提到的加鎖是解決方案之一春锋,但是對用戶體驗(yàn)不好性能很差矫膨,基本上無法再互聯(lián)網(wǎng)項(xiàng)目中使用。如果不能加鎖期奔,那么常用的解決方案是什么侧馅?
我們可以用隊(duì)列。如果我們將所有的用戶請求進(jìn)行排隊(duì)呐萌,有一個服務(wù)來訂閱這個隊(duì)列馁痴,那么不管有多少用戶訪問,最終到服務(wù)器端肺孤,處理服務(wù)的就只有一個進(jìn)程罗晕。這樣就實(shí)現(xiàn)了一個由并發(fā)操作轉(zhuǎn)換成單線操作济欢。
下面是例子: