从构建分布式秒杀系统聊聊WebSocket推送通知

前言

秒杀架构到后期,我们采用了消息队列的形式实现抢购逻辑,那么之前抛出过这样一个问题:消息队列异步处理完每个用户请求后,如何通知给相应用户秒杀成功?

场景映射

首先,我们举一个生活中比较常见的例子:我们去银行办理业务,一般会选择相关业务打印一个排号纸,然后就可以坐在小板凳上玩着手机,等待被小喇叭报号。当小喇叭喊到你所持有的号码,就可以拿着排号纸去柜台办理自己的业务这里,假设当我们取排号纸的时候,银行根据时间段内的排队情况,比较人性化的提示户:排队人数较多,您是否继续等待?否的话我们可以换个时间段再来办理。

由此我们把生活场景映射到真实的秒杀业务逻辑中来:

•我们可以把柜台比喻成商品下单处理逻辑单元

•拿到排号纸说明你进入相应商品处理队列

•拿到排号纸的请求直接返回前台,提示用户抢购进行中•排号纸进入队列后,等待商品业务处理逻辑•小喇叭叫到自己的排号相当于服务端通知用户秒杀成功,这时候可以进行支付逻辑

•那些拿不到票号的同学,相当于队列已满直接返回秒杀失败解决方案通过上面的场景,我们很容易能够想到一种方案就是服务端通知,那么如何做到服务端异步通知的呢?

下面,主角开始登场了,就是我们的Websocket。WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信。

HTTP VS WebSocket特点:

•异步、事件触发

•可以发送文本,图片等流文件

•数据格式比较轻量,性能开销小,通信高效

•使用ws或者wss协议的客户端socket

缺点:

•部分浏览器不支持,浏览器支持的程度与方式有区别,需要各种兼容写法。

集成案例由于我们的秒杀架构项目案例中使用了SpringBoot,因此集成webSocket也是相对比较简单的。

首先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); //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。

private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。

private static CopyOnWriteArraySetwebSocketSet = new CopyOnWriteArraySet(); //与某个客户端的连接会话,需要通过它来给客户端发送数据

private Session session; //接收userId private String userId=""; /** * 连接建立成功调用的方法*/

@OnOpen

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

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

//在线数加1 log.info("有新窗口开始监听:"+userId+",当前在线人数为" + getOnlineCount());

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

/** * 连接关闭调用的方法 */

@OnClose public void onClose()

 { webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 *

@param message 客户端发送过来的消息*/

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

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("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */

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

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

for (WebSocketServer item : webSocketSet) { try { //这里可以设定只推送给这个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 消费配置,通知用户是否秒杀成功: /** * 消费者 spring-kafka 2.0 + 依赖JDK8 * 

 @Component public class KafkaConsumer { @Autowired private ISeckillService seckillService; private static RedisUtil redisUtil = new RedisUtil(); /** * 监听seckill主题,有消息就读取 * @param message */

@KafkaListener(topics = {"seckill"})

public void receiveMessage(String message)

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

if(redisUtil.getValue(array[0])!=null){//control层已经判断了,其实这里不需要再判断了

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

if(result.equals(Result.ok())){ WebSocketServer.sendInfo(array[0].toString(), "秒杀成功");//推送给前台 }

else{ WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台

redisUtil.cacheValue(array[0], "ok");//秒杀结束 } }else{ WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台 } } } webSocket.js 前台通知逻辑: $(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连接发生错误,请刷新页面重试!") }; webSocket.onopen = function(event) { }; webSocket.onmessage = function(event) { var message = event.data; alert(message)//判断秒杀是否成功、自行处理逻辑 }; } }

客户端API 客户端与服务器通信

•send() 向远程服务器发送数据

•close() 关闭该websocket链接 监听函数

•onopen 当网络连接建立时触发该事件

•onerror 当网络发生错误时触发该事件

•onclose 当websocket被关闭时触发该事件

•onmessage

当websocket接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。

Java初高级一起学习分享,共同学习才是最明智的选择,喜欢的话可以我的学习群64弍46衣3凌9,或加资料群69似64陆0吧3

readyState属性 这个属性可以返回websocket所处的状态。

•CONNECTING(0) websocket正尝试与服务器建立连接

 •OPEN(1) websocket与服务器已经建立连接

•CLOSING(2) websocket正在关闭与服务器的连接

•CLOSED(3) websocket已经关闭了与服务器的连接 开源方案 goeasy GoEasy实时Web推送,支持后台推送和前台推送两种:后台推送可以选择Java SDK、 Restful API支持所有开发语言;

前台推送:JS推送。无论选择哪种方式推送代码都十分简单(10分钟可搞定)。由于它支持websocket 和polling两种连接方式所以兼顾大多数主流浏览器,低版本的IE浏览器也是支持的。

 地址:http://goeasy.io/ Pushlets Pushlets 是通过长连接方式实现“推”消息的。

推送模式分为:Poll(轮询)、Pull(拉)。

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

Pushlet 是一个开源的 Comet 框架,Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。

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

总结

其实前面有提过,尽管WebSocket有诸多优点,但是,如果服务端维护很多长连接也是挺耗费资源的,服务器集群以及览器或者客户端兼容性问题,也会带来了一些不确定性因素。大体了解了一下各大厂的做法,大多数都还是基于轮询的方式实现的,比如:腾讯PC端微信扫码登录、京东商城支付成功通知等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值