場景
我們現(xiàn)在有一個餐館名叫王小二菜館
,用戶會不時(shí)的訪問我們的在線點(diǎn)餐系統(tǒng)截珍,這個系統(tǒng)的核心功能是點(diǎn)餐炸渡,我們做這個需求的逐步迭代:
- 用戶訪問我們系統(tǒng)時(shí),我們返回
歡迎光臨王小二菜館
偷拔; - 用戶訪問我們系統(tǒng)時(shí),我們返回歷史訪問情況
歡迎光臨王小二菜館沉颂,您是第N位客戶
条摸; - 用戶訪問我們系統(tǒng)時(shí)悦污,我們返回目前的點(diǎn)餐情況
歡迎光臨王小二菜館铸屉,您的取餐號為X,您前面還有N位進(jìn)餐用戶切端,請耐心等待...
彻坛,用戶可以手工刷新查看最新排隊(duì)號; - 什么踏枣?都9102年了昌屉,還要用戶手工刷新?用戶進(jìn)入頁面后茵瀑,不想手工刷新间驮,需要實(shí)時(shí)查看就餐情況;
方案
針對上面提出的逐步迭代的需求马昨,我們來各個擊破竞帽,并總結(jié)其中涉及到的關(guān)鍵知識點(diǎn):
這里假設(shè)我們只有一臺應(yīng)用服務(wù)器
1. 用戶訪問我們系統(tǒng)時(shí),我們返回歡迎光臨王小二菜館
鸿捧;
有過編程經(jīng)歷的童鞋一定會說屹篓,這個問題難道不是基本操作嗎?還需多言匙奴?我們java猿兒只需開啟一個tomcat堆巧,引入spring-mvc,寫一個controller泼菌,輕輕松松get一分谍肤,So easy!
當(dāng)然思路肯定是這樣,不過這里需要提出的是哗伯,tomcat作為一個應(yīng)用服務(wù)器荒揣,其實(shí)已經(jīng)為我們做好了請求承接和請求分發(fā)的事兒,所以我們只用簡簡單單寫一段代碼就輕松搞定笋颤,其實(shí)這兒還是有很大學(xué)問的乳附,如何接收用戶請求以及分發(fā)請求其實(shí)都是tomcat幫我們透明處理了内地,tomcat在這做這些事情時(shí)其實(shí)干了兩個關(guān)鍵的事兒:
- 接收請求:tomcat需要建立一個本地socket鏈接來處理每一次請求;
- 請求分發(fā):針對每一次請求赋除,tomcat需要做協(xié)議解析得到相應(yīng)的請求體阱缓,并根據(jù)配置好的線程模型來分發(fā)這次請求,默認(rèn)tomcat是BIO模型即每個請求一個線程举农,然后每個線程執(zhí)行我們相應(yīng)代碼的邏輯荆针,返回
歡迎光臨王小二菜館
,需要注意的是這些線程由一個線程池進(jìn)行維護(hù)颁糟,每次請求都會從這個線程池中獲取航背,如果并發(fā)請求過多則會引發(fā)排隊(duì);
知識點(diǎn):應(yīng)用服務(wù)器棱貌、線程模型玖媚、線程池;
2. 用戶訪問我們系統(tǒng)時(shí)婚脱,我們返回歷史訪問情況歡迎光臨王小二菜館今魔,您是第N位客戶
;
在簡單介紹了應(yīng)用服務(wù)器后障贸,我們暫且先不深挖错森,關(guān)注問題您是第N位客戶
,這不就是一個簡單的計(jì)數(shù)器嗎篮洁?So easy涩维,我定義一個全局變量,來一個請求+1不就ok了嘛袁波,這又是一道送分題??
不過等等瓦阐,我們剛剛講到,每個請求都會從線程池拿出一個線程锋叨,如果我們定義一個全局變量垄分,這里我們一定能夠保證每個請求+1后,后續(xù)線程都能看到最新的值嗎娃磺?這可不一定呢薄湿,另外,同時(shí)來了多個線程偷卧,他們同時(shí)+1豺瘤,那么是不是就少算了呀?
這里就碰到了我們經(jīng)典的一致性問題了听诸,同一個共享變量坐求,一個線程寫入,下一個線程一定可以看到嗎晌梨?多個一起寫桥嗤,最后的數(shù)據(jù)一定可以保證符合預(yù)期嗎须妻?
有過一定經(jīng)驗(yàn)的童鞋肯定會說,加個鎖吧泛领,一步到位荒吏,當(dāng)然這是一種解決方法,我們還有更好的方法呢渊鞋。
知識點(diǎn):可見性绰更、順序性、鎖锡宋;
3. 用戶訪問我們系統(tǒng)時(shí)儡湾,我們返回目前的點(diǎn)餐情況歡迎光臨王小二菜館,您的取餐號為X执俩,您前面還有N位進(jìn)餐用戶徐钠,請耐心等待...
;
這應(yīng)該是我們平時(shí)經(jīng)常碰到的情況:需求很短奠滑,范圍很大丹皱,無形需求最為致命??
讓我們來詳細(xì)分解妒穴,首先每個用戶要有一個排隊(duì)號宋税,另外要知道目前有多少用戶正在進(jìn)餐,另外前面用戶吃完后讼油,需要通知第X位用戶可以就餐杰赛,且取餐號越小,越快就位矮台。
每個用戶一個排隊(duì)號乏屯,嗯,這是已知題瘦赫,pass辰晕;
前面有多個用戶進(jìn)餐,且他們吃完后通知确虱,嗯哼含友,這個問題可是有那么點(diǎn)難度呀,這里我們可以使用兩個數(shù)據(jù)結(jié)構(gòu)校辩,一個用于保存目前正在進(jìn)餐的用戶列表L1窘问,一個保存目前正在排隊(duì)的用戶列表L2,叫號請求過來時(shí)宜咒,先生成順序號惠赫,然后看L1是否滿,如果滿了就放到L2故黑,L1里面的用戶吃完后儿咱,從L2取一個放入L1庭砍,bingo,不過這兒我們需要注意的問題是混埠,L1和L2都是需要支持多個線程訪問的逗威,需要保證線程安全。
知識點(diǎn):并發(fā)容器岔冀、線程安全凯旭;
4. 什么?都9102年了使套,還要用戶手工刷新罐呼?用戶進(jìn)入頁面后,不想手工刷新侦高,需要實(shí)時(shí)查看就餐情況嫉柴;
猿兒:這難道不是一個送分題?我三十秒刷新一次奉呛,不就行了计螺。
PM:什么,三十秒鐘瞧壮,五秒鐘我都嫌長了登馒,用戶不耐煩走了怎么辦?轉(zhuǎn)換率誰來負(fù)責(zé)咆槽?
被暴擊后的猿兒頓時(shí)阻塞陈轿,開始了長達(dá)五秒鐘的沉默,空氣瞬間凝固...
猿兒別心灰意冷秦忿,每一次暴擊都是你成長的機(jī)會麦射,??
這里提供一個不那么優(yōu)雅的方案,我們設(shè)計(jì)一個信號量字典灯谣,key是用戶順序號潜秋,value是一個信號量,默認(rèn)信號量被征用胎许,然后用戶排隊(duì)請求過來如果需要排隊(duì)峻呛,那么在對應(yīng)信號量上面做超時(shí)等待,5秒超時(shí)呐萨,當(dāng)L1用戶用餐完畢拿到下一個排隊(duì)順序號杀饵,釋放信號量,該用戶即可就餐谬擦,嗯哼切距,not bad。
當(dāng)然惨远,這個方案有一定風(fēng)險(xiǎn)谜悟,第一題我們講過话肖,應(yīng)用服務(wù)器默認(rèn)會使用一個線程來處理當(dāng)前請求,線程陷入等待葡幸,那豈不是線程池可用線程-1最筒,服務(wù)器可用風(fēng)險(xiǎn)+1,這里我們期待更好的解決方案??蔚叨。
知識點(diǎn):信號量床蜘,線程等待、線程喚醒
ok蔑水, 到這里邢锯,我們就我們四個小需求做了簡單的設(shè)計(jì),里面也引出了一些并發(fā)編程的關(guān)鍵知識點(diǎn)搀别,也解決了我們的任務(wù)丹擎,??+4,??歇父。
等等蒂培,這些知識點(diǎn)都只是提出來了而已呀,還沒具體講解呢榜苫,別著急护戳,后續(xù)我會開一個小系列來專門整理講解這些知識點(diǎn),敬請期待~