本片不讲源码
源码参考链接 juejin.cn/spost/72459…
主要分享一下 hotkey服务端 京东hotKey源码中的架构设计
是怎么处理高并发数据及热度计算的
服务端 比较好的一些设计模式和亮点
- 队列缓冲+异步处理:收集hotkey处理
- 责任链模式:提升扩展性,维护性
- 环形buffer:内存优化热度统计
- 业务模型的封装
- 工具类的封装
- 客户端-发布订阅接
- 客户端-读写分离
1.队列缓冲+异步处理
队列缓冲+异步处理 是非常值得我们学习的一种处理高并发场景的设计模式
队列缓冲
com.jd.platform.hotkey.worker.netty.server.NodesServer#startNettyServer
Netty抽象除了两组线程池, BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络读写操作
所以
com.jd.platform.hotkey.worker.keydispatcher.KeyProducer#push
是高并发/很多线程都在进行的操作。
该操作底层 就是调用
public static BlockingQueue<HotKeyModel> QUEUE = new LinkedBlockingQueue<>(2000000);
java.util.concurrent.BlockingQueue#put
也就是 阻塞队列 通过 ReentrantLock 实现了并发安全,这是其一 其二是 引入队列达到了异步,work线程又可以接着去进行处理新来的neety IO事件,解放了work线程 ,往队列里面放东西 就结束了,总比热度计算结束以后再结束好太多了,否则work线程 就会被热度计算过程捆绑 导致无法及时处理新来的IO事件,热度计算本身就是实时性 放在第一位,这块引入阻塞队列看似只是简单使用,背后的深刻含义还是值得去思考的。
-
另外 用到了锁 就会影响并发性能,怎么去解决?作者在客户端 key事件推送的设计中给了我们答案 读写分离双map 下面会讲,另外美团leaf中的双buffer 也是利用了这种思想进行设计的
-
热度统计中使用的环形buffer思想 在百度分布式id-UidGenerator 中也有体现,以及caffeine 中的条状环形buffer 更是对 环形buffer的优化
异步处理
hotkey计算
count点击计算
可以看到
- hotkey计算 消费队列里面的元素 使用的也是多线程进行处理的
- count点击计算 是单线程执行的
- 也就是 在设计模式里面 多生产者+单消费者模式
- 还是 多生产者+多消费者模式
为什么 这里要区别处理 ? 这是因为 hotkey的计算实行性更高,肯定要使用多线程同时计算,更快的拿到是否是热key的结果 但这里多个线程进行hotkey计算 出现了并发安全问题,可以参考 juejin.cn/spost/72459… 里面的讲解 原因以及 解决方案。不过 上面也提到 热度计算,热度计算本身就是实时性 放在第一位,并发安全导致结果出现微小误差也是可以原谅的
2.责任链模式的引入
相比其他设计模式,当下责任链模式个人觉得应该是值得学习的, 也是目前大佬们 极力推荐的一种设计模式 毕竟根据solid原则 组合大于继承嘛
参考代码
代码上好像也没有多少复杂代码 简简单单一个for循环而已, 所以这也是 作者做的比较好的一个地方,借助spring ioc能力,和order注解控制处理类的处理顺序。
所以服务端所有的处理逻辑 就放在这四个filter 里面了,极为简洁,维护也很好维护。不是说责任链多好用 就代表着有多难用。
3.业务模型的封装
最后想说的其实是程序模型设计相关的,这个其实也是我认为程序员最重要,最重要的能力了
- 业务模型的封装
这部分 讲起来比较虚,但我换一个角度去说,作者为了一个热度计算的项目,是怎么设计文件夹名字的,以及类名字的。这个是很值得学习的一个能力,整个项目的结构才是作者的思想
其实我可以试着帮大家梳理下 整个项目结构是怎么来的?
neety 基本上是中间件开发必备的选型首先肯定要做的事情就是对neety的封装
NodesServerStarter 是 整个项目的开始
可以看到
NodesServer NodesServerHandler
就是负责把io事件责任链的模式分发到 filter文件夹下的4大处理类
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建造者模式 + 工厂模式 + 单例模式
序列化编解码的封装
引用 protostuff工具类 该序列化出了名的快 号称最快的Java序列化框架Protostuff
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>