從構(gòu)建分布式秒殺系統(tǒng)聊聊WebSocket推送通知

前言

秒殺架構(gòu)到后期筹淫,我們采用了消息隊(duì)列的形式實(shí)現(xiàn)搶購(gòu)邏輯,那么之前拋出過這樣一個(gè)問題:消息隊(duì)列異步處理完每個(gè)用戶請(qǐng)求后呢撞,如何通知給相應(yīng)用戶秒殺成功损姜?

場(chǎng)景映射

首先,我們舉一個(gè)生活中比較常見的例子:我們?nèi)ャy行辦理業(yè)務(wù)狸相,一般會(huì)選擇相關(guān)業(yè)務(wù)打印一個(gè)排號(hào)紙薛匪,然后就可以坐在小板凳上玩著手機(jī),等待被小喇叭報(bào)號(hào)脓鹃。當(dāng)小喇叭喊到你所持有的號(hào)碼逸尖,就可以拿著排號(hào)紙去柜臺(tái)辦理自己的業(yè)務(wù)這里,假設(shè)當(dāng)我們?nèi)∨盘?hào)紙的時(shí)候瘸右,銀行根據(jù)時(shí)間段內(nèi)的排隊(duì)情況娇跟,比較人性化的提示戶:排隊(duì)人數(shù)較多,您是否繼續(xù)等待太颤?否的話我們可以換個(gè)時(shí)間段再來辦理苞俘。

由此我們把生活場(chǎng)景映射到真實(shí)的秒殺業(yè)務(wù)邏輯中來:

?我們可以把柜臺(tái)比喻成商品下單處理邏輯單元

?拿到排號(hào)紙說明你進(jìn)入相應(yīng)商品處理隊(duì)列

?拿到排號(hào)紙的請(qǐng)求直接返回前臺(tái),提示用戶搶購(gòu)進(jìn)行中?排號(hào)紙進(jìn)入隊(duì)列后龄章,等待商品業(yè)務(wù)處理邏輯?小喇叭叫到自己的排號(hào)相當(dāng)于服務(wù)端通知用戶秒殺成功吃谣,這時(shí)候可以進(jìn)行支付邏輯

?那些拿不到票號(hào)的同學(xué),相當(dāng)于隊(duì)列已滿直接返回秒殺失敗解決方案通過上面的場(chǎng)景做裙,我們很容易能夠想到一種方案就是服務(wù)端通知岗憋,那么如何做到服務(wù)端異步通知的呢?

下面锚贱,主角開始登場(chǎng)了仔戈,就是我們的Websocket纵朋。WebSocket是HTML5開始提供的一種瀏覽器與服務(wù)器間進(jìn)行全雙工通訊的網(wǎng)絡(luò)技術(shù)靶擦。依靠這種技術(shù)可以實(shí)現(xiàn)客戶端和服務(wù)器端的長(zhǎng)連接,雙向?qū)崟r(shí)通信六水。

HTTP VS WebSocket特點(diǎn):

?異步吧碾、事件觸發(fā)

?可以發(fā)送文本凰盔,圖片等流文件

?數(shù)據(jù)格式比較輕量,性能開銷小滤港,通信高效

?使用ws或者wss協(xié)議的客戶端socket

缺點(diǎn):

?部分瀏覽器不支持廊蜒,瀏覽器支持的程度與方式有區(qū)別趴拧,需要各種兼容寫法。

集成案例由于我們的秒殺架構(gòu)項(xiàng)目案例中使用了SpringBoot山叮,因此集成webSocket也是相對(duì)比較簡(jiǎn)單的著榴。

首先pom.xml引入以下依賴:org.springframework.bootspring-boot-starter-websocketWebSocketConfig

配置:/** * WebSocket配置? */

@Configuration public class WebSocketConfig

{ @Bean public ServerEndpointExporter serverEndpointExporter()

{ return new ServerEndpointExporter(); } }

WebSocketServer 配置:

@ServerEndpoint("/websocket/{userId}")

@Componentpublic class WebSocketServer { private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)屁倔。應(yīng)該把它設(shè)計(jì)成線程安全的脑又。

private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象锐借。

private static CopyOnWriteArraySetwebSocketSet = new CopyOnWriteArraySet(); //與某個(gè)客戶端的連接會(huì)話问麸,需要通過它來給客戶端發(fā)送數(shù)據(jù)

private Session session; //接收userId private String userId=""; /** * 連接建立成功調(diào)用的方法*/

@OnOpen

public void onOpen(Session session,@PathParam("userId") String userId)

{ this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount();

//在線數(shù)加1 log.info("有新窗口開始監(jiān)聽:"+userId+",當(dāng)前在線人數(shù)為" + getOnlineCount());

this.userId=userId; try { sendMessage("連接成功"); } catch (IOException e) { log.error("websocket IO異常"); } }

/** * 連接關(guān)閉調(diào)用的方法 */

@OnClose public void onClose()

?{ webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線數(shù)減1 log.info("有一連接關(guān)閉!當(dāng)前在線人數(shù)為" + getOnlineCount()); } /** * 收到客戶端消息后調(diào)用的方法 *

@param message 客戶端發(fā)送過來的消息*/

@OnMessage public void onMessage(String message, Session session) { log.info("收到來自窗口"+userId+"的信息:"+message); //群發(fā)消息

for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * @param session * @param error */

@OnError public void onError(Session session, Throwable error) { log.error("發(fā)生錯(cuò)誤"); error.printStackTrace(); } /** * 實(shí)現(xiàn)服務(wù)器主動(dòng)推送 */

public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群發(fā)自定義消息 * */

public static void sendInfo(String message,@PathParam("userId") String userId){ log.info("推送消息到窗口"+userId+"钞翔,推送內(nèi)容:"+message);

for (WebSocketServer item : webSocketSet) { try { //這里可以設(shè)定只推送給這個(gè)userId的严卖,為null則全部推送

if(userId==null) { item.sendMessage(message); }else if(item.userId.equals(userId)){ item.sendMessage(message); } } catch (IOException e) { continue; } } }

public static synchronized int getOnlineCount() { return onlineCount; }

public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; }

public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }

KafkaConsumer 消費(fèi)配置,通知用戶是否秒殺成功: /** * 消費(fèi)者 spring-kafka 2.0 + 依賴JDK8 *?

?@Component public class KafkaConsumer { @Autowired private ISeckillService seckillService; private static RedisUtil redisUtil = new RedisUtil(); /** * 監(jiān)聽seckill主題,有消息就讀取 * @param message */

@KafkaListener(topics = {"seckill"})

public void receiveMessage(String message)

{ //收到通道的消息之后執(zhí)行秒殺操作 String[] array = message.split(";");

if(redisUtil.getValue(array[0])!=null){//control層已經(jīng)判斷了布轿,其實(shí)這里不需要再判斷了

Result result = seckillService.startSeckil(Long.parseLong(array[0]), Long.parseLong(array[1]));

if(result.equals(Result.ok())){ WebSocketServer.sendInfo(array[0].toString(), "秒殺成功");//推送給前臺(tái) }

else{ WebSocketServer.sendInfo(array[0].toString(), "秒殺失敗");//推送給前臺(tái)

redisUtil.cacheValue(array[0], "ok");//秒殺結(jié)束 } }else{ WebSocketServer.sendInfo(array[0].toString(), "秒殺失敗");//推送給前臺(tái) } } } webSocket.js 前臺(tái)通知邏輯: $(function(){ socket.init(); });

var basePath = "ws://localhost:8080/seckill/";

socket = { webSocket : "", init : function() { //userId:自行追加 if ('WebSocket' in window) { webSocket = new WebSocket(basePath+'websocket/1'); } else if ('MozWebSocket' in window) { webSocket = new MozWebSocket(basePath+"websocket/1"); } else { webSocket = new SockJS(basePath+"sockjs/websocket"); } webSocket.onerror = function(event) { alert("websockt連接發(fā)生錯(cuò)誤哮笆,請(qǐng)刷新頁(yè)面重試!") }; webSocket.onopen = function(event) { }; webSocket.onmessage = function(event) { var message = event.data; alert(message)//判斷秒殺是否成功、自行處理邏輯 }; } }

客戶端API 客戶端與服務(wù)器通信

?send() 向遠(yuǎn)程服務(wù)器發(fā)送數(shù)據(jù)

?close() 關(guān)閉該websocket鏈接 監(jiān)聽函數(shù)

?onopen 當(dāng)網(wǎng)絡(luò)連接建立時(shí)觸發(fā)該事件

?onerror 當(dāng)網(wǎng)絡(luò)發(fā)生錯(cuò)誤時(shí)觸發(fā)該事件

?onclose 當(dāng)websocket被關(guān)閉時(shí)觸發(fā)該事件

?onmessage

當(dāng)websocket接收到服務(wù)器發(fā)來的消息的時(shí)觸發(fā)的事件汰扭,也是通信中最重要的一個(gè)監(jiān)聽事件稠肘。

Java初高級(jí)一起學(xué)習(xí)分享,共同學(xué)習(xí)才是最明智的選擇萝毛,喜歡的話可以我的學(xué)習(xí)群64弍46衣3凌9项阴,或加資料群69似64陸0吧3

readyState屬性 這個(gè)屬性可以返回websocket所處的狀態(tài)。

?CONNECTING(0) websocket正嘗試與服務(wù)器建立連接

??OPEN(1) websocket與服務(wù)器已經(jīng)建立連接

?CLOSING(2) websocket正在關(guān)閉與服務(wù)器的連接

?CLOSED(3) websocket已經(jīng)關(guān)閉了與服務(wù)器的連接 開源方案 goeasy GoEasy實(shí)時(shí)Web推送笆包,支持后臺(tái)推送和前臺(tái)推送兩種:后臺(tái)推送可以選擇Java SDK环揽、 Restful API支持所有開發(fā)語言;

前臺(tái)推送:JS推送庵佣。無論選擇哪種方式推送代碼都十分簡(jiǎn)單(10分鐘可搞定)薯演。由于它支持websocket 和polling兩種連接方式所以兼顧大多數(shù)主流瀏覽器,低版本的IE瀏覽器也是支持的秧了。

?地址:http://goeasy.io/ Pushlets Pushlets 是通過長(zhǎng)連接方式實(shí)現(xiàn)“推”消息的。

推送模式分為:Poll(輪詢)序无、Pull(拉)验毡。

地址:http://www.pushlets.com/

Pushlet 是一個(gè)開源的 Comet 框架,Pushlet 使用了觀察者模型:客戶端發(fā)送請(qǐng)求,訂閱感興趣的事件帝嗡;服務(wù)器端為每個(gè)客戶端分配一個(gè)會(huì)話 ID 作為標(biāo)記晶通,事件源會(huì)把新產(chǎn)生的事件以多播的方式發(fā)送到訂閱者的事件隊(duì)列里。

?地址:https://github.com/wjw465150/Pushlet

總結(jié)

其實(shí)前面有提過哟玷,盡管WebSocket有諸多優(yōu)點(diǎn)狮辽,但是一也,如果服務(wù)端維護(hù)很多長(zhǎng)連接也是挺耗費(fèi)資源的,服務(wù)器集群以及覽器或者客戶端兼容性問題喉脖,也會(huì)帶來了一些不確定性因素椰苟。大體了解了一下各大廠的做法,大多數(shù)都還是基于輪詢的方式實(shí)現(xiàn)的树叽,比如:騰訊PC端微信掃碼登錄舆蝴、京東商城支付成功通知等等。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末题诵,一起剝皮案震驚了整個(gè)濱河市洁仗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌性锭,老刑警劉巖赠潦,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異草冈,居然都是意外死亡她奥,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門疲陕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來方淤,“玉大人,你說我怎么就攤上這事蹄殃⌒” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵诅岩,是天一觀的道長(zhǎng)讳苦。 經(jīng)常有香客問我,道長(zhǎng)吩谦,這世上最難降的妖魔是什么鸳谜? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮式廷,結(jié)果婚禮上咐扭,老公的妹妹穿的比我還像新娘。我一直安慰自己滑废,他們只是感情好蝗肪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蠕趁,像睡著了一般薛闪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俺陋,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天豁延,我揣著相機(jī)與錄音昙篙,去河邊找鬼。 笑死诱咏,一個(gè)胖子當(dāng)著我的面吹牛苔可,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胰苏,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼硕蛹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了硕并?” 一聲冷哼從身側(cè)響起法焰,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倔毙,沒想到半個(gè)月后埃仪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陕赃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年卵蛉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片么库。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡傻丝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诉儒,到底是詐尸還是另有隱情葡缰,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布忱反,位于F島的核電站泛释,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏温算。R本人自食惡果不足惜怜校,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望注竿。 院中可真熱鬧茄茁,春花似錦、人聲如沸巩割。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)喂分。三九已至,卻和暖如春机蔗,著一層夾襖步出監(jiān)牢的瞬間蒲祈,已是汗流浹背甘萧。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留梆掸,地道東北人扬卷。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像怪得,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卑硫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,409評(píng)論 1 92
  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢夢(mèng)敢當(dāng)閱讀 8,914評(píng)論 0 50
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理欢伏,服務(wù)發(fā)現(xiàn)入挣,斷路器硝拧,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 女媧彩石落湖間,錦繡仙島不一般恢氯。 高秋時(shí)節(jié)踏碧波,夕陽(yáng)西下不思還酿雪。 (2016年10月7日于仙島湖畔)
    仙島湖公子閱讀 453評(píng)論 0 1
  • 師范對(duì)于我們來說醋安,是最難忘也是驚喜的地方柠辞。 我們的感情在這里開始也在這里結(jié)束叭首,離畢業(yè)只剩下幾天的時(shí)間,去...
    全球最美100個(gè)地方閱讀 211評(píng)論 0 1