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

场景映射

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

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

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

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

解决方案

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

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

特点:

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

缺点:

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

集成案例

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

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

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

WebSocketConfig 配置:

/**
* WebSocket配置
* 创建者 爪哇笔记
* 创建时间 2018年5月29日
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

WebSocketServer 配置:

@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据
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
* @author 科帮网 By https://blog.52itstyle.com
*/
@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接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。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. 20164319 刘蕴哲 Exp5 MSF基础应用

    [实践内容] 本实践目标是掌握metasploit的基本应用方式,重点常用的三种攻击方式的思路.具体需要完成: 1.1一个主动攻击实践(这里我做了两个:ms08_067[成功]和ms17_010[成功 ...

  2. koa中间件

    koa是Express的下一代基于node的web框架  目前有1.x和2.0两个版本 1. Express Express是第一代最流行的web框架  它对node.js的http进行了封装 Exp ...

  3. 发布后就很丢人的随笔(c++)

    for循环条件中使用的变量 可以是循环条件中定义的变量 也可以是循环之外的变量 代码: #include <iostream> int main(){ int a = 1; int b = ...

  4. CentOS7 查看显卡信息

    A卡: [root@t1 ~]# lspci | grep -i vga 00:02.0 VGA compatible controller: Cirrus Logic GD 5446 N卡: [ro ...

  5. handle java

    关于java中的引用 我觉得在"think in java"这本书里面讲得很好 在那本书里 他们把引用叫做"句柄"(Handle) java中就是用" ...

  6. java学习(一)

    目录 java简介 java基础 基本语法 java标识符 java变量 变量类型 变量声明 java常量 Java 基本数据类型 内置数据类型 引用数据类型 Java类型转换 java注释 操作符 ...

  7. HDU4521

    一个改变的最长上升子序列(LIS),这种题型做的很少,今天做起来很费劲,查了很多资料,还把最基础的LIS补了一遍,具体的看代码吧,我把思路都放在了注释里面 #include<iostream&g ...

  8. 【Selenium】【BugList1】调用firefox浏览器,报 TypeError: 'module' object is not callable

    #coding=utf-8 from selenium import webdriver driver=webdriver.firefox() 解决方法:firefox改为Firefox

  9. 20155312 张竞予 Exp2 后门原理与实践

    Exp2 后门原理与实践 目录 基础问题回答 (1)例举你能想到的一个后门进入到你系统中的可能方式? (2)例举你知道的后门如何启动起来(win及linux)的方式? (3)Meterpreter有哪 ...

  10. Oracle中将Clob字段转换成字符串

    1. 利用dbms_lob.substr()方法可将对应字段转换成字符串如下 select dbms_lob.substr(content) from NEWS 该方法有个缺点,当content字段长 ...