京东hotKey-进阶-源码中的架构设计有哪些?

338 阅读1分钟

本片不讲源码

源码参考链接 juejin.cn/spost/72459…

主要分享一下 hotkey服务端 京东hotKey源码中的架构设计
是怎么处理高并发数据及热度计算的

服务端 比较好的一些设计模式和亮点

  • 队列缓冲+异步处理:收集hotkey处理
  • 责任链模式:提升扩展性,维护性
  • 环形buffer:内存优化热度统计
  • 业务模型的封装
  • 工具类的封装
  • 客户端-发布订阅接
  • 客户端-读写分离

1.队列缓冲+异步处理

队列缓冲+异步处理 是非常值得我们学习的一种处理高并发场景的设计模式

队列缓冲

com.jd.platform.hotkey.worker.netty.server.NodesServer#startNettyServer

Netty抽象除了两组线程池, BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络读写操作 image.png

所以

image.png

com.jd.platform.hotkey.worker.keydispatcher.KeyProducer#push

是高并发/很多线程都在进行的操作。

该操作底层 就是调用

public static BlockingQueue<HotKeyModel> QUEUE = new LinkedBlockingQueue<>(2000000);

java.util.concurrent.BlockingQueue#put

image.png

也就是 阻塞队列 通过 ReentrantLock 实现了并发安全,这是其一 其二是 引入队列达到了异步,work线程又可以接着去进行处理新来的neety IO事件,解放了work线程 ,往队列里面放东西 就结束了,总比热度计算结束以后再结束好太多了,否则work线程 就会被热度计算过程捆绑 导致无法及时处理新来的IO事件,热度计算本身就是实时性 放在第一位,这块引入阻塞队列看似只是简单使用,背后的深刻含义还是值得去思考的。

  • 另外 用到了锁 就会影响并发性能,怎么去解决?作者在客户端 key事件推送的设计中给了我们答案 读写分离双map 下面会讲,另外美团leaf中的双buffer 也是利用了这种思想进行设计的

  • 热度统计中使用的环形buffer思想 在百度分布式id-UidGenerator 中也有体现,以及caffeine 中的条状环形buffer 更是对 环形buffer的优化

异步处理

hotkey计算 image.png

count点击计算 image.png

可以看到

  • hotkey计算 消费队列里面的元素 使用的也是多线程进行处理的
  • count点击计算 是单线程执行的
  • 也就是 在设计模式里面 多生产者+单消费者模式
  • 还是 多生产者+多消费者模式

为什么 这里要区别处理 ? 这是因为 hotkey的计算实行性更高,肯定要使用多线程同时计算,更快的拿到是否是热key的结果 但这里多个线程进行hotkey计算 出现了并发安全问题,可以参考 juejin.cn/spost/72459… 里面的讲解 原因以及 解决方案。不过 上面也提到 热度计算,热度计算本身就是实时性 放在第一位,并发安全导致结果出现微小误差也是可以原谅的

2.责任链模式的引入

相比其他设计模式,当下责任链模式个人觉得应该是值得学习的, 也是目前大佬们 极力推荐的一种设计模式 毕竟根据solid原则 组合大于继承嘛

参考代码 image.png

代码上好像也没有多少复杂代码 简简单单一个for循环而已, 所以这也是 作者做的比较好的一个地方,借助spring ioc能力,和order注解控制处理类的处理顺序。

image.png 所以服务端所有的处理逻辑 就放在这四个filter 里面了,极为简洁,维护也很好维护。不是说责任链多好用 就代表着有多难用

3.业务模型的封装

最后想说的其实是程序模型设计相关的,这个其实也是我认为程序员最重要,最重要的能力了

image.png

image.png

  • 业务模型的封装

这部分 讲起来比较虚,但我换一个角度去说,作者为了一个热度计算的项目,是怎么设计文件夹名字的,以及类名字的。这个是很值得学习的一个能力,整个项目的结构才是作者的思想

其实我可以试着帮大家梳理下 整个项目结构是怎么来的?

neety 基本上是中间件开发必备的选型首先肯定要做的事情就是对neety的封装

NodesServerStarter 是 整个项目的开始

image.png

可以看到

image.png

NodesServer  NodesServerHandler

就是负责把io事件责任链的模式分发到 filter文件夹下的4大处理类

image.png

appNameFilter 是为了把每个客户端链接等装起来 后面想把热度结果 推送回去,否则计算出结果 怎么通知客户端呢?

HeartBeatFilter 是为了接受的客户端的心跳,客户端发送一个ping 我会一个pong,

而 hotKeyFilter 和 keyCounterFilter 则是业务领域你想统计的业务 ,这里作者只想统计 hotkey 计算和 点击数计算

这里面 需要解释下 client 文件夹下的ClientChangeListener 是为了 将AppNameFilter 重拿到的客户端信息 维护到 holder 文件夹下的 ClientInfoHolder 中

而推送pusher文件夹 为什么会存在? 因为 我的推送对象 虽然 在ClientInfoHolder 可以拿到,但是我得使用,队列缓冲+异步处理 我的队列和 异步处理逻辑得封装起来 ,也就有了 AppServerPusher

按照这种逻辑 其实 剩下的 rule 文件夹 是为了拿到etcd中的规则 放在KeyRuleHolder 中

cache 文件夹 CaffeineCacheHolder 是封装临时的滑动窗口类

couter和 keydispatcher 文件夹 是为了热度统计时候使用队列缓冲+异步处理模式 得把缓冲队列 和 异步处理逻辑封装起来, 而keylistener 文件夹

就是进行热度计算,及推送结果,所以引入了 tool文件夹下的滑动窗口类 以及 推送类

SlidingWindow slidingWindow = checkWindow(hotKeyModel, key);
List<IPusher> iPushers;

上面说的可能初看 比较混乱,需要细细品味。

4.工具类的封装

caffieine 的封装

如果你项目中要引入 caffieine 感觉可以直接拿过来这些代码直接用了(当然需要适当改造下) 绝对是项目亮点 基本上会涉及 builde建造者模式 + 工厂模式 + 单例模式

image.png

序列化编解码的封装

引用 protostuff工具类 该序列化出了名的快 号称最快的Java序列化框架Protostuff

image.png maven地址

<properties>

    <protostuff.version>1.7.4</protostuff.version>
</properties>

<dependencies>
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>${protostuff.version}</version>
    </dependency>

    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-runtime</artifactId>
        <version>${protostuff.version}</version>
    </dependency>
 </dependencies>