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

场景映射

首先,我们举一个生活中比较常见的例子:我们去银行办理业务,一般会选择相关业务打印一个排号纸,然后就可以坐在小板凳上玩着手机,等待被小喇叭报号。当小喇叭喊到你所持有的号码,就可以拿着排号纸去柜台办理自己的业务。

这里,假设当我们取排号纸的时候,银行根据时间段内的排队情况,比较人性化的提示用户:排队人数较多,您是否继续等待?否的话我们可以换个时间段再来办理。

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

  • 我们可以把柜台比喻成商品下单处理逻辑单元
  • 拿到排号纸说明你进入相应商品处理队列
  • 拿到排号纸的请求直接返回前台,提示用户抢购进行中
  • 排号纸进入队列后,等待商品业务处理逻辑
  • 小喇叭叫到自己的排号相当于服务端通知用户秒杀成功,这时候可以进行支付逻辑
  • 那些拿不到票号的同学,相当于队列已满直接返回秒杀失败

解决方案

通过上面的场景,我们很容易能够想到一种方案就是服务端通知,那么如何做到服务端异步通知的呢?下面,主角开始登场了,就是我们的Websocket。

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

特点:

  • 异步、事件触发
  • 可以发送文本,图片等流文件
  • 数据格式比较轻量,性能开销小,通信高效
  • 使用ws或者wss协议的客户端socket,能够实现真正意义上的推送功能

缺点:

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

集成案例

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

首先pom.xml引入以下依赖:

  1. <!-- webSocket 秒杀通知-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-websocket</artifactId>
  5. </dependency>

WebSocketConfig 配置:

  1. /**
  2. * WebSocket配置
  3. * 创建者 爪哇笔记
  4. * 创建时间 2018年5月29日
  5. */
  6. @Configuration
  7. public class WebSocketConfig {
  8. @Bean
  9. public ServerEndpointExporter serverEndpointExporter() {
  10. return new ServerEndpointExporter();
  11. }
  12. }

WebSocketServer 配置:

  1. @ServerEndpoint("/websocket/{userId}")
  2. @Component
  3. public class WebSocketServer {
  4. private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
  5. //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
  6. private static int onlineCount = 0;
  7. //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
  8. private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
  9. //与某个客户端的连接会话,需要通过它来给客户端发送数据
  10. private Session session;
  11. //接收userId
  12. private String userId="";
  13. /**
  14. * 连接建立成功调用的方法*/
  15. @OnOpen
  16. public void onOpen(Session session,@PathParam("userId") String userId) {
  17. this.session = session;
  18. webSocketSet.add(this); //加入set中
  19. addOnlineCount(); //在线数加1
  20. log.info("有新窗口开始监听:"+userId+",当前在线人数为" + getOnlineCount());
  21. this.userId=userId;
  22. try {
  23. sendMessage("连接成功");
  24. } catch (IOException e) {
  25. log.error("websocket IO异常");
  26. }
  27. }
  28. /**
  29. * 连接关闭调用的方法
  30. */
  31. @OnClose
  32. public void onClose() {
  33. webSocketSet.remove(this); //从set中删除
  34. subOnlineCount(); //在线数减1
  35. log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
  36. }
  37. /**
  38. * 收到客户端消息后调用的方法
  39. * @param message 客户端发送过来的消息*/
  40. @OnMessage
  41. public void onMessage(String message, Session session) {
  42. log.info("收到来自窗口"+userId+"的信息:"+message);
  43. //群发消息
  44. for (WebSocketServer item : webSocketSet) {
  45. try {
  46. item.sendMessage(message);
  47. } catch (IOException e) {
  48. e.printStackTrace();
  49. }
  50. }
  51. }
  52. /**
  53. * @param session
  54. * @param error
  55. */
  56. @OnError
  57. public void onError(Session session, Throwable error) {
  58. log.error("发生错误");
  59. error.printStackTrace();
  60. }
  61. /**
  62. * 实现服务器主动推送
  63. */
  64. public void sendMessage(String message) throws IOException {
  65. this.session.getBasicRemote().sendText(message);
  66. }
  67. /**
  68. * 群发自定义消息
  69. * */
  70. public static void sendInfo(String message,@PathParam("userId") String userId){
  71. log.info("推送消息到窗口"+userId+",推送内容:"+message);
  72. for (WebSocketServer item : webSocketSet) {
  73. try {
  74. //这里可以设定只推送给这个userId的,为null则全部推送
  75. if(userId==null) {
  76. item.sendMessage(message);
  77. }else if(item.userId.equals(userId)){
  78. item.sendMessage(message);
  79. }
  80. } catch (IOException e) {
  81. continue;
  82. }
  83. }
  84. }
  85. public static synchronized int getOnlineCount() {
  86. return onlineCount;
  87. }
  88. public static synchronized void addOnlineCount() {
  89. WebSocketServer.onlineCount++;
  90. }
  91. public static synchronized void subOnlineCount() {
  92. WebSocketServer.onlineCount--;
  93. }
  94. }

KafkaConsumer 消费配置,通知用户是否秒杀成功:

  1. /**
  2. * 消费者 spring-kafka 2.0 + 依赖JDK8
  3. * @author 科帮网 By https://blog.52itstyle.com
  4. */
  5. @Component
  6. public class KafkaConsumer {
  7. @Autowired
  8. private ISeckillService seckillService;
  9. private static RedisUtil redisUtil = new RedisUtil();
  10. /**
  11. * 监听seckill主题,有消息就读取
  12. * @param message
  13. */
  14. @KafkaListener(topics = {"seckill"})
  15. public void receiveMessage(String message){
  16. //收到通道的消息之后执行秒杀操作
  17. String[] array = message.split(";");
  18. if(redisUtil.getValue(array[0])!=null){//control层已经判断了,其实这里不需要再判断了
  19. Result result = seckillService.startSeckil(Long.parseLong(array[0]), Long.parseLong(array[1]));
  20. if(result.equals(Result.ok())){
  21. WebSocketServer.sendInfo(array[0].toString(), "秒杀成功");//推送给前台
  22. }else{
  23. WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台
  24. redisUtil.cacheValue(array[0], "ok");//秒杀结束
  25. }
  26. }else{
  27. WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台
  28. }
  29. }
  30. }

webSocket.js 前台通知逻辑:

  1. $(function(){
  2. socket.init();
  3. });
  4. var basePath = "ws://localhost:8080/seckill/";
  5. socket = {
  6. webSocket : "",
  7. init : function() {
  8. //userId:自行追加
  9. if ('WebSocket' in window) {
  10. webSocket = new WebSocket(basePath+'websocket/1');
  11. }
  12. else if ('MozWebSocket' in window) {
  13. webSocket = new MozWebSocket(basePath+"websocket/1");
  14. }
  15. else {
  16. webSocket = new SockJS(basePath+"sockjs/websocket");
  17. }
  18. webSocket.onerror = function(event) {
  19. alert("websockt连接发生错误,请刷新页面重试!")
  20. };
  21. webSocket.onopen = function(event) {
  22. };
  23. webSocket.onmessage = function(event) {
  24. var message = event.data;
  25. alert(message)//判断秒杀是否成功、自行处理逻辑
  26. };
  27. }
  28. }

客户端API

客户端与服务器通信

  • send() 向远程服务器发送数据
  • close() 关闭该websocket链接

监听函数 

  • onopen 当网络连接建立时触发该事件
  • onerror 当网络发生错误时触发该事件
  • onclose 当websocket被关闭时触发该事件
  • onmessage 当websocket接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。msg.data

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

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

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

总结

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

有些小伙伴可能会问了,轮询岂不是会更耗费资源?其实在我看来,有些轮询是不可能穿透到后端数据库查询服务的,比如秒杀,一个缓存标记位就可以判定是否秒杀成功。相对于WS的长连接以及其不确定因素,在秒杀场景下,轮询还是相对比较合适的。

思考

最后,思考一个问题:100件商品,假如有一万人进行抢购,该如何设置队列长度?

秒杀案例:https://gitee.com/52itstyle/spring-boot-seckill

参考

https://blog.52itstyle.com/archives/736/

https://www.xoriant.com/blog/mobility/websocket-web-stateful-now.html

 

从构建分布式秒杀系统聊聊WebSocket推送通知的更多相关文章

  1. 从构建分布式秒杀系统聊聊Disruptor高性能队列

    前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步.文章标题来自码友 简介 LMAX Disruptor是一个高性能的线程间消息库.它源于LMAX对并发性,性能和非阻塞算法 ...

  2. 从构建分布式秒杀系统聊聊Lock锁使用中的坑

    前言 在单体架构的秒杀活动中,为了减轻DB层的压力,这里我们采用了Lock锁来实现秒杀用户排队抢购.然而很不幸的是尽管使用了锁,但是测试过程中仍然会超卖,执行了N多次发现依然有问题.输出一下代码吧,可 ...

  3. 从构建分布式秒杀系统聊聊验证码 给大家推荐8个SpringBoot精选项目

    前言 为了拦截大部分请求,秒杀案例前端引入了验证码.淘宝上很多人吐槽,等输入完秒杀活动结束了,对,结束了...... 当然了,验证码的真正作用是,有效拦截刷单操作,让羊毛党空手而归. 验证码 那么到底 ...

  4. SpringBoot开发案例从0到1构建分布式秒杀系统

    前言 ​最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路.俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场 ...

  5. Golang websocket推送

    Golang websocket推送 在工作用主要使用的是Java,也做过IM(后端用的netty websocket).最近想通过Golang重写下,于是通过websocket撸了一个聊天室. 项目 ...

  6. iOS---iOS10适配iOS当前所有系统的远程推送

    一.iOS推送通知简介 众所周知苹果的推送通知从iOS3开始出现, 每一年都会更新一些新的用法. 譬如iOS7出现的Silent remote notifications(远程静默推送), iOS8出 ...

  7. 用 Go 编写一个简单的 WebSocket 推送服务

    用 Go 编写一个简单的 WebSocket 推送服务 本文中代码可以在 github.com/alfred-zhong/wserver 获取. 背景 最近拿到需求要在网页上展示报警信息.以往报警信息 ...

  8. 现代IM系统中消息推送和存储架构的实现

    现代IM系统中消息推送和存储架构的实现-云栖社区-阿里云 https://yq.aliyun.com/articles/253242

  9. android系统下消息推送机制

    一.推送方式简介: 当前随着移动互联网的不断加速,消息推送的功能越来越普遍,不仅仅是应用在邮件推送上了,更多的体现在手机的APP上.当我们开发需要和服务器交互的应用程序时,基本上都需要获取服务器端的数 ...

随机推荐

  1. 关于servelt的相关介绍

    1.@WebServlet注解的作用 在Servlet 3.0中,使用@WebServlet注解可实现servlet和url的映射,它告知容器哪些Servlet会提供服务以及额外信息,其作用相当于之前 ...

  2. javascript Hoisting变量提升

    1. 看人家举的两个例子,我认为这里的判断是否定义: !var 其实就是 指是否在函数function里面定义了.只有在funciton里面定义了了,js才hoist到最上面去找这个变量的值,否则就按 ...

  3. C# 两个datatable中的数据快速比较返回交集或差集[z]

    最基本的写法无非是写多层foreach循环,数据量多了,循环的次数是乘积增长的. 这里推荐使用Except()差集.Intersect()交集,具体性能没有进行对比. 如果两个datatable的字段 ...

  4. UVa156

    #include <bits/stdc++.h> using namespace std; map<string,int> cnt; vector<string> ...

  5. oracle的部分增删查改

    1. 创建表空间 create tablespace (demo)表名 logging datafile( 表空间存放的位置)  ‘D:\app\Administrator\oradata\orcl\ ...

  6. 将bean转换成XML字符串

    package com.sinoservices.bms.bbl.rest.bean; import javax.xml.bind.annotation.XmlAccessType; import j ...

  7. vscode+MinGW+cmake设置轻量ide

    本地随手写一些题目的时候,发现visual studio非常庞大emmm vscodevscode是一个轻量编辑器 (1)vscode插件与设置自动同步 在两个电脑上,用vscode可以同步插件 ,利 ...

  8. Java学习笔记:多线程(二)

    与线程生命周期相关的方法: sleep 调用sleep方法会进入计时等待状态,等待时间到了,进入就绪状态. yield 调用yield方法会让别的线程执行,但是不确保真正让出.较少使用,官方注释都说 ...

  9. form表单内容JSON格式转化

    form表单提交时,对于Content-type为application/json是提交时需要转换成json格式,据说form enctype=‘application/json’这样就可以,然而在我 ...

  10. tp5 数据库

    连接数据库: 在config下面的database.php里. 查找数据: halt(Db::name('studys')->column('name','age')); 也可以用find fi ...