架构(七)热点探测-HotKey在物联网可以做什么_京东中间件组件 hotkey(1)

return list;

}



在服务启动的时候他就开启了一个定时任务,从map取出数据之后就会通过netty发送出去



public static void startPusher(Long period) {
if (period == null || period <= 0) {
period = 500L;
}
@SuppressWarnings(“PMD.ThreadPoolCreationRule”)
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory(“hotkey-pusher-service-executor”, true));
scheduledExecutorService.scheduleAtFixedRate(() -> {
IKeyCollector<HotKeyModel, HotKeyModel> collectHK = KeyHandlerFactory.getCollector();
List hotKeyModels = collectHK.lockAndGetResult();
if(CollectionUtil.isNotEmpty(hotKeyModels)){
KeyHandlerFactory.getPusher().send(Context.APP_NAME, hotKeyModels);
collectHK.finishOnce();
}

},0, period, TimeUnit.MILLISECONDS);

}



public void send(String appName, List list) {
//积攒了半秒的key集合,按照hash分发到不同的worker
long now = System.currentTimeMillis();

Map<Channel, List<HotKeyModel>> map = new HashMap<>();
for(HotKeyModel model : list) {
    model.setCreateTime(now);
    Channel channel = WorkerInfoHolder.chooseChannel(model.getKey());
    if (channel == null) {
        continue;
    }

    List<HotKeyModel> newList = map.computeIfAbsent(channel, k -> new ArrayList<>());
    newList.add(model);
}

for (Channel channel : map.keySet()) {
    try {
        List<HotKeyModel> batch = map.get(channel);
        HotKeyMsg hotKeyMsg = new HotKeyMsg(MessageType.REQUEST_NEW_KEY, Context.APP_NAME);
        hotKeyMsg.setHotKeyModels(batch);
        channel.writeAndFlush(hotKeyMsg).sync();
    } catch (Exception e) {
        try {
            InetSocketAddress insocket = (InetSocketAddress) channel.remoteAddress();
            JdLogger.error(getClass(),"flush error " + insocket.getAddress().getHostAddress());
        } catch (Exception ex) {
            JdLogger.error(getClass(),"flush error");
        }

    }
}

}


![](https://img-blog.csdnimg.cn/5cbd4eb267694df5b87fa63c8ae05a00.png)




#### 1.3、服务端计算


        服务端监听到netty事件,如果是新的key访问就进入KeyListener的newKey方法



public void newKey(HotKeyModel hotKeyModel, KeyEventOriginal original) {
//cache里的key
String key = buildKey(hotKeyModel);
//判断是不是刚热不久
Object o = hotCache.getIfPresent(key);
if (o != null) {
return;
}

SlidingWindow slidingWindow = checkWindow(hotKeyModel, key);
//看看hot没
boolean hot = slidingWindow.addCount(hotKeyModel.getCount());

if (!hot) {
//如果没hot,重新put,cache会自动刷新过期时间
CaffeineCacheHolder.getCache(hotKeyModel.getAppName()).put(key, slidingWindow);
} else {
hotCache.put(key, 1);

//删掉该key
CaffeineCacheHolder.getCache(hotKeyModel.getAppName()).invalidate(key);

//开启推送
hotKeyModel.setCreateTime(SystemClock.now());

//当开关打开时,打印日志。大促时关闭日志,就不打印了
if (EtcdStarter.LOGGER_ON) {
logger.info(NEW_KEY_EVENT + hotKeyModel.getKey());
}

//分别推送到各client和etcd
for (IPusher pusher : iPushers) {
pusher.push(hotKeyModel);
}

}

}


  
  
         主要是开启了一个滑动窗口SlidingWindow,关键是两个key前缀对应的设定规则,一个是间隔一个是数量,比如机器故障key,2s5个访问数量  
  
         然后SlidingWindow的addCount方法判断是否变成或者本来就是热key



public synchronized boolean addCount(long count) {
//当前自己所在的位置,是哪个小时间窗
int index = locationIndex();
// System.out.println(“index:” + index);
//然后清空自己前面windowSize到2*windowSize之间的数据格的数据
//譬如1秒分4个窗口,那么数组共计8个窗口
//当前index为5时,就清空6、7、8、1。然后把2、3、4、5的加起来就是该窗口内的总和
clearFromIndex(index);

    int sum = 0;
    // 在当前时间片里继续+1
    sum += timeSlices[index].addAndGet(count);
    //加上前面几个时间片
    for (int i = 1; i < windowSize; i++) {
        sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
    }

    lastAddTimestamp = SystemClock.now();

    return sum >= threshold;
}

#### 1.4、热key发送客户端


        计算出热key之后也不是立刻发送客户端的,会先放到队列,然后每10ms推送一次,其实对于体量没那么大的公司,这个其实没必要



public void batchPushToClient() {
AsyncPool.asyncDo(() -> {
while (true) {
try {
List tempModels = new ArrayList<>();
//每10ms推送一次
Queues.drain(hotKeyStoreQueue, tempModels, 10, 10, TimeUnit.MILLISECONDS);
if (CollectionUtil.isEmpty(tempModels)) {
continue;
}

            Map<String, List<HotKeyModel>> allAppHotKeyModels = new HashMap<>();

            //拆分出每个app的热key集合,按app分堆
            for (HotKeyModel hotKeyModel : tempModels) {
                List<HotKeyModel> oneAppModels = allAppHotKeyModels.computeIfAbsent(hotKeyModel.getAppName(), (key) -> new ArrayList<>());
                oneAppModels.add(hotKeyModel);
            }

            //遍历所有app,进行推送
            for (AppInfo appInfo : ClientInfoHolder.apps) {
                List<HotKeyModel> list = allAppHotKeyModels.get(appInfo.getAppName());
                if (CollectionUtil.isEmpty(list)) {
                    continue;
                }

                HotKeyMsg hotKeyMsg = new HotKeyMsg(MessageType.RESPONSE_NEW_KEY);
                hotKeyMsg.setHotKeyModels(list);

                //整个app全部发送
                appInfo.groupPush(hotKeyMsg);
            }

            allAppHotKeyModels = null;

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

}


#### 1.5、客户端缓存热key


        然后就又回到了客户端,客户端收到热key事件写入本地缓存,这里使用的是缓存性能之王Caffeine,这里如果是固定场景可以改写下,在加入缓存的时候根据key把value查出来再设置,这样就不需要在客户端判断是热key并且无值的时候,把值set进去。




public void newKey(HotKeyModel hotKeyModel) {
long now = System.currentTimeMillis();
//如果key到达时已经过去1秒了,记录一下。手工删除key时,没有CreateTime
if (hotKeyModel.getCreateTime() != 0 && Math.abs(now - hotKeyModel.getCreateTime()) > 1000) {
JdLogger.warn(getClass(), "the key comes too late : " + hotKeyModel.getKey() + " now " +
+now + " keyCreateAt " + hotKeyModel.getCreateTime());
}
if (hotKeyModel.isRemove()) {
//如果是删除事件,就直接删除
deleteKey(hotKeyModel.getKey());
return;
}
//已经是热key了,又推过来同样的热key,做个日志记录,并刷新一下
if (JdHotKeyStore.isHot(hotKeyModel.getKey())) {
JdLogger.warn(getClass(), “receive repeat hot key :” + hotKeyModel.getKey() + " at " + now);
}
addKey(hotKeyModel.getKey());
}
private void addKey(String key) {
ValueModel valueModel = ValueModel.defaultValue(key);
if (valueModel == null) {
//不符合任何规则
deleteKey(key);
return;
}
//如果原来该key已经存在了,那么value就被重置,过期时间也会被重置。如果原来不存在,就新增的热key
JdHotKeyStore.setValueDirectly(key, valueModel);
}


### 2、Etcd存储


        Etcd作为一个持久化存储,主要是hotkey为了防止服务单点故障或者发布导致热key丢失,同时还需要对热key规则进行存储。服务端每个节点还会把自己的ip信息放到etcd,让客户端可以拿到服务端的信息进行netty推送


#### 2.1、客户端更新热key


        通过监听对应路径下etcd的数据变化,从而更新本地缓存



private void startWatchHotKey() {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
JdLogger.info(getClass(), “— begin watch hotKey change ----”);
IConfigCenter configCenter = EtcdConfigFactory.configCenter();
try {
KvClient.WatchIterator watchIterator = configCenter.watchPrefix(ConfigConstant.hotKeyPath + Context.APP_NAME);
//如果有新事件,即新key产生或删除
while (watchIterator.hasNext()) {
WatchUpdate watchUpdate = watchIterator.next();

            List<Event> eventList = watchUpdate.getEvents();
            KeyValue keyValue = eventList.get(0).getKv();
            Event.EventType eventType = eventList.get(0).getType();
            try {
                String key = keyValue.getKey().toStringUtf8().replace(ConfigConstant.hotKeyPath + Context.APP_NAME + "/", "");

                //如果是删除key,就立刻删除
                if (Event.EventType.DELETE == eventType) {
                    HotKeyModel model = new HotKeyModel();
                    model.setRemove(true);
                    model.setKey(key);
                    EventBusCenter.getInstance().post(new ReceiveNewKeyEvent(model));
                } else {
                    HotKeyModel model = new HotKeyModel();
                    model.setRemove(false);
                    String value = keyValue.getValue().toStringUtf8();
                    //新增热key
                    JdLogger.info(getClass(), "etcd receive new key : " + key + " --value:" + value);
                    //如果这是一个删除指令,就什么也不干
                    if (Constant.DEFAULT_DELETE_VALUE.equals(value)) {
                        continue;
                    }

                    //手工创建的value是时间戳
                    model.setCreateTime(Long.valueOf(keyValue.getValue().toStringUtf8()));

                    model.setKey(key);
                    EventBusCenter.getInstance().post(new ReceiveNewKeyEvent(model));
                }
            } catch (Exception e) {
                JdLogger.error(getClass(), "new key err :" + keyValue);
            }

        }
    } catch (Exception e) {
        JdLogger.error(getClass(), "watch err");
    }
});

}


#### 2.2、客户端更新规则



private void startWatchRule() {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
JdLogger.info(getClass(), “— begin watch rule change ----”);
try {
IConfigCenter configCenter = EtcdConfigFactory.configCenter();
KvClient.WatchIterator watchIterator = configCenter.watch(ConfigConstant.rulePath + Context.APP_NAME);
//如果有新事件,即rule的变更,就重新拉取所有的信息
while (watchIterator.hasNext()) {
//这句必须写,next会让他卡住,除非真的有新rule变更
WatchUpdate watchUpdate = watchIterator.next();
List eventList = watchUpdate.getEvents();
JdLogger.info(getClass(), "rules info changed. begin to fetch new infos. rule change is " + eventList);

            //全量拉取rule信息
            fetchRuleFromEtcd();
        }
    } catch (Exception e) {
        JdLogger.error(getClass(), "watch err");
    }


});

}



#### 2.3、服务端检查自己在etcd



public void makeSureSelfOn() {
//开启上传worker信息
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(() -> {

    try {
        if (canUpload) {
            uploadSelfInfo();
        }
    } catch (Exception e) {
        //do nothing
    }

}, 0, 5, TimeUnit.SECONDS);

}
private void uploadSelfInfo() {
configCenter.putAndGrant(buildKey(), buildValue(), 8);
}


#### 2.4、检查前端显示的地址,因为hotkey的前端是用jsp做得



@Scheduled(fixedRate = 30000)
public void fetchDashboardIp() {
try {
//获取DashboardIp
List keyValues = configCenter.getPrefix(ConfigConstant.dashboardPath);

    //是空,给个警告
    if (CollectionUtil.isEmpty(keyValues)) {
        logger.warn("very important warn !!! Dashboard ip is null!!!");
        return;
    }

    String dashboardIp = keyValues.get(0).getValue().toStringUtf8();
    NettyClient.getInstance().connect(dashboardIp);

} catch (Exception e) {
    e.printStackTrace();
}

}


### 3、前端显示与规则配置


#### 3.1、服务端发送dashboard


        在热key推送的时候,除了推送给客户端,还会推送给dashboard



@PostConstruct
public void uploadToDashboard() {
AsyncPool.asyncDo(() -> {
while (true) {
try {
//要么key达到1千个,要么达到1秒,就汇总上报给etcd一次
List tempModels = new ArrayList<>();
Queues.drain(hotKeyStoreQueue, tempModels, 1000, 1, TimeUnit.SECONDS);
if (CollectionUtil.isEmpty(tempModels)) {
continue;
}

            //将热key推到dashboard
            DashboardHolder.flushToDashboard(FastJsonUtils.convertObjectToJSON(tempModels));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

}


#### 3.2、dashboard接收热key


        通过netty接收到读取事件之后,存储到队列中



protected void channelRead0(ChannelHandlerContext ctx, String message) {
if (StringUtils.isEmpty(message)) {
return;
}
try {
HotKeyMsg msg = FastJsonUtils.toBean(message, HotKeyMsg.class);
if (MessageType.PING == msg.getMessageType()) {
String hotMsg = FastJsonUtils.convertObjectToJSON(new HotKeyMsg(MessageType.PONG, PONG));
FlushUtil.flush(ctx, MsgBuilder.buildByteBuf(hotMsg));
} else if (MessageType.REQUEST_HOT_KEY == msg.getMessageType()) {
List list = FastJsonUtils.toList(msg.getBody(), HotKeyModel.class);
for (HotKeyModel hotKeyModel : list) {
HotKeyReceiver.push(hotKeyModel);
}
}
} catch (Exception e) {
e.printStackTrace();
}

}


        然后会不断的消费队列中的数据,把数据分别在阻塞队列和本地缓存放一份



public void dealHotKey() {
while (true) {
try {
HotKeyModel model = HotKeyReceiver.take();
//将该key放入实时热key本地缓存中
if (model != null) {
//将key放到队列里,供入库时分批调用
putRecord(model.getAppName(), model.getKey(), model.getCreateTime());
//获取发来的这个热key,存入本地caffeine,设置过期时间
HotKeyReceiver.writeToLocalCaffeine(model);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}


        另外一个异步线程会不断把数据插到mysql



public void insertRecords() {
while (true) {
try {
List records = new ArrayList<>();
Queues.drain(queue, records, 1000, 1, TimeUnit.SECONDS);
if (CollectionUtil.isEmpty(records)) {
continue;
}
List keyRecordList = new ArrayList<>(records.size());
for (IRecord iRecord : records) {
KeyRecord keyRecord = handHotKey(iRecord);
if (keyRecord != null) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年嵌入式&物联网开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新!!

756053775)]

[外链图片转存中…(img-yMEmsiq6-1715756053776)]

[外链图片转存中…(img-gekNLqSp-1715756053776)]

[外链图片转存中…(img-quSn6yjN-1715756053777)]

[外链图片转存中…(img-RA8S8AN8-1715756053777)]

[外链图片转存中…(img-rDjyCk4h-1715756053778)]

[外链图片转存中…(img-XYAG6um9-1715756053779)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新!!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值