Sse推送实践

Sse推送实践

场景

之前提到了,在多数据源环境下,我们需要对数据源的连接池做一层缓存,不能让每次连接都通过 TCP三次握手 -> 校验用户 -> 校验权限这几个步骤,很耗时,耗费性能。

为此我们需要将已经做过这几个步骤的连接进行一层缓存,感兴趣的同学可以看看这篇文章。juejin.cn/post/727086...

为了能在页面动态的看到,每个连接池的使用情况,使用频率,创建时间,我们有必要对这层缓存的变动做有效的订阅,同时发布给前端。

问题思考

如何当内存变动的时候监听到这个事件?其实很简单,在每次内存变动的时候,发布一个异步的事件,然后订阅这个事件即可。

如何实时地往前端做推送?

这里我做了一些调研:目前可行的 协议有两种:WebSocket和Sse.

Sse优点:

  1. 基于HTTP,协议简单
  2. 支持内置的重新连接,事件id
  3. 企业防火墙数据包检查可以安全通过

WebSocket优点:

  1. 实时通信。
  2. 浏览器原生支持更好

其实现在已经很明白了,使用Sse无疑是更好,更轻量的。

代码实战

  1. 写一套Sse长连接的缓存
typescript 复制代码
/**
 * sse连接缓存,防止重复创建连接
 */
@Component
public class SseEmitterCache {
​
    private static Logger logger = LoggerFactory.getLogger(SseEmitterCache.class);
    private static SseEmitterCache sseEmitterCache;
    
    /**
     * clientId为key
     */
    protected ConcurrentHashMap<String, SseEmitter> connects;//连接
    
    private int maxSize = 200;
​
    //私有化构造器
    protected SseEmitterCache(){
    }
​
    //构造单例
    public static SseEmitterCache getCacheInstance(){
        if(null == sseEmitterCache){
            sseEmitterCache = new SseEmitterCache();
            sseEmitterCache.connects = new ConcurrentHashMap<>();
        }
        return sseEmitterCache;
    }
    
    
    public SseEmitter getCache(String key){
        //删除某个连接如果已达到最大设置连接数,先删除未在使用,并且使用评率最低的连接
        if(connects.size() > maxSize){
            return null;
        }
        if(!connects.containsKey(key)){
            connects.put(key,new SseEmitter());
        }
        return connects.get(key);
    }
    
    
    public boolean hasCache(String key){
        return connects.containsKey(key);
    }
    
    
    public boolean remove(String key){
        connects.remove(key);
        return true;
    }
    
    public List<String> getClientIds(){
        ConcurrentHashMap.KeySetView<String, SseEmitter> strings = connects.keySet();
        Iterator<String> iterator = strings.stream().iterator();
        List<String> clientIds = new ArrayList<>();
        while (iterator.hasNext()){
            clientIds.add(iterator.next());
        }
        return clientIds;
    }
​
    
}
​
  1. 对于你的连接池缓存,每次变动都发布事件
csharp 复制代码
/**
     * 异步发布
     */
    private void sendCache(){
        MyThreadPool.executor().execute(()->applicationContext.publishEvent(new MysqlConPoolCacheChangeEvent(OperationType.CHANGE)));
    }
  1. 监听,并发送给所有的SseEmitterCache中的客户。
typescript 复制代码
/**
 * @author HT
 * 监听MysqlConPoolCacheChangeEvent事件
 */
@Component
public class MysqlConPoolCacheChangeEventListener {
    
    private static Logger logger = LoggerFactory.getLogger(DataSourceServiceImpl.class);
    
    
    @Resource
    private DataSourceService dataSourceService;
    
    
    private SseEmitterCache emitterCache = SseEmitterCache.getCacheInstance();
    
    
    @EventListener
    public void onApplicationEvent(MysqlConPoolCacheChangeEvent event) {
        List<String> clientIds = emitterCache.getClientIds();
        DatasourceCacheVo cacheVo = dataSourceService.showConnPool();
        //如果缓存发生了变动,就给所有的客户端都推送消息。
        clientIds.forEach(clientId ->{
            dataSourceService.sendMsg(clientId, JSONUtil.toJsonStr(cacheVo));
            logger.info("sendMsg:{}",JSONUtil.toJsonStr(cacheVo));
        });
    }
}
  1. Controller层
less 复制代码
 @ApiOperation(value = "创建SSE连接")
    @GetMapping(path = "/createSse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter createSse(String id) {
        return dataSourceService.createSseEmitter(id);
    }
  1. Service层
ini 复制代码
 @Override
    public SseEmitter createSseEmitter(String clientId) {
        if(StringUtils.isEmpty(clientId)){
            clientId = UUID.randomUUID().toString();
        }
        SseEmitter emitter = sseEmitterCache.getCache(clientId);
        logger.info("获取到SSE,id:{}",clientId);
        String finalClientId = clientId;
        sendMsg(clientId,JSONUtil.toJsonStr(this.showConnPool()));
        emitter.onCompletion(()->{
            logger.info("SSE关闭,id:{}",finalClientId);
            sseEmitterCache.remove(finalClientId);
        });
        emitter.onTimeout(()->{
            logger.info("SSE关闭,id:{}",finalClientId);
            sseEmitterCache.remove(finalClientId);
        });
        emitter.onError((e)->{
            logger.info("SSE异常,id:{}",finalClientId);
            sseEmitterCache.remove(finalClientId);
        });
        return emitter;
    }
相关推荐
做系统的大强5 分钟前
脚本说 PASS、OS 读全零 —— 到底是谁在撒谎?
后端·操作系统
傲世仙尊19 分钟前
HTTP深水区-重定向状态码与GETPOST的传参之路
网络·网络协议·http
code2cat1 小时前
【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
java·后端·开发工具·ai agent·mcp
网络小江1 小时前
上网第三十五课:第五周复盘——IP 与 DNS 快问快答
网络协议
可乐鸡翅yeah_1 小时前
hls.js 配置 loadStartPosition,点播起始播放位置踩坑
开发语言·javascript·网络协议·ecmascript·m3u8在线
一条破秋裤2 小时前
TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲
arm开发·网络协议·tcp/ip
Harvil_2 小时前
任务跑一半,API 突然报“对话超长“断流:我给 Agent 修的紧急逃生通道
后端
用户EasyAdminBlazor2 小时前
EasyAdminBlazor SignalR 实时消息源码解析:从 NotificationHub 到站内消息
后端
程序猿乐锅2 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven
用户EasyAdminBlazor2 小时前
EasyAdminBlazor 定时任务:FreeScheduler 可视化调度与任务管理
后端