![fe8a9814e608561bba648e64ed8ab996.png](https://i-blog.csdnimg.cn/blog_migrate/a4fae2cebb07054b93661247817ab20f.png)
实时性获取数据的几个方案
对于某些需要实时更新的数据(例如Facebook/Twitter 更新、股价更新、新的博文、赛事结果等)来说,有这么几种解决方案:
Polling(轮询)
在客户端重复的向服务端发送新请求。如果服务器没有新的数据更动,关闭本次连接。然后客户端在稍等一段时间之后,再次发起新请求,一直重复这样的步骤。
缺点:是有一定的时间间隔,如果间隔变小,会对服务端造成比较大的压力。
Long-polling(长轮询)
在长轮询中,客户端发送一个请求到服务端。如果服务端没有新的数据更动,那么本次连接将会被保持,直到等待到更新后的数据,返回给客户端并关闭这个连接。
缺点:服务器维持长连接的资源消耗,且浏览器对于长连接数有限制。
Server-Sent Events
SSE类似于长轮询的机制,但是它在每一次的连接中,不只等待一次数据的更动。客户端发送一个请求到服务端 ,服务端保持这个请求直到一个新的消息准备好,将消息返回至客户端,此时不关闭连接,仍然保持它,供其它消息使用。SSE的一大特色就是重复利用一个连接来处理每一个消息(又称event)。
缺点:并不是所有浏览器都支持。
WebSocket
WebSocket不同于以上的这些技术,因为它提供了一个真正意义上的双向连接,我们会在以后的内容里介绍WebSocket这部分。
相对于WebSocket,Server Sent Events有一些优点:
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 需要自己实现。
- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。
我们来看一下SSE在Spring Boot里面是怎么实现的。
下面是用SSE来实现redis的BLPOP,
@RestController
@RequestMapping("/redis/list")
public class RedisListController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/bLeftPop/{key}")
public SseEmitter bLeftPop(@PathVariable(name = "key")String key){
SseEmitter emitter=new SseEmitter();
applicationContext.publishEvent(new SseQueueEvent(emitter,key,true,SseQueueEvent.TYPE_LEFT));
return emitter;
}
}
请注意Controller的返回值是SseEmitter,我们在controller里面通过Spring的publishEvent发布了一个异步事件。
@Component
@Slf4j
public class RedisQueueListener {
private final StringRedisTemplate redisTemplate;
@Autowired
public RedisQueueListener(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@EventListener
@Async
public void listen(SseQueueEvent event){
event.getSseEmitter().onCompletion(()-> event.setRunning(false));
while (event.isRunning()){
String value;
if(event.getType()==SseQueueEvent.TYPE_LEFT){
value=redisTemplate.opsForList().leftPop(event.getKey(),0, TimeUnit.SECONDS);
}else {
value=redisTemplate.opsForList().rightPop(event.getKey(),0, TimeUnit.SECONDS);
}
try {
SseEmitter.SseEventBuilder builder=SseEmitter.event()
.id(UUID.randomUUID().toString())
.data(value,MediaType.APPLICATION_JSON);
event.getSseEmitter().send(builder);
} catch (Exception e) {
log.info("stop sse {}",e.getMessage());
event.getSseEmitter().complete();
event.setRunning(false);
}
}
}
}
可以看到在异步的监听器里面,执行了leftPop的操作,如果队列没有值,则该操作会阻塞等等。如果SSE断开,则会抛出异常,然后退出监听器。
客户端代码
<script>
var source = new EventSource('/redis/bLeftPop/test');
source.addEventListener("message",function(message){
console.log(message);
});
</script>
Java客户端代码
@RunWith(JUnit4.class)
@Slf4j
public class RedisRestApplicationTests {
@Test
public void sseClient() throws InterruptedException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://localhost:8080/redis/list/bLeftPop/test")
.build();
EventSource.Factory factory = EventSources.createFactory(client);
EventSource eventSource = factory.newEventSource(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
log.info("{}:{}",id,data);
}
});
Thread.sleep(10000);
}
}