解剖屎山,寻觅黄金之第二弹 天天通讯

2024-9-21 05:18:29来源:程序员客栈

大家好,我【wǒ】3y啊。由于去重逻辑重【chóng】构了几次,好【hǎo】多股东【dōng】直【zhí】呼看不懂,于【yú】是我今【jīn】天再【zài】安排一【yī】波对代码的【de】解析吧。austin支持两种去重的类型:N分钟相同【tóng】内容达【dá】到N次【cì】去重和一天内N次相同渠道【dào】频次去重【chóng】。

在最开始,我的第一版实现是这样的:


(资料图片仅供参考)

publicvoidduplication(TaskInfotaskInfo){//配置示例:{"contentDeduplication":{"num":1,"time":300},"frequencyDeduplication":{"num":5}}JSONObjectproperty=JSON.parseObject(config.getProperty(DEDUPLICATION_RULE_KEY,AustinConstant.APOLLO_DEFAULT_VALUE_JSON_OBJECT));JSONObjectcontentDeduplication=property.getJSONObject(CONTENT_DEDUPLICATION);JSONObjectfrequencyDeduplication=property.getJSONObject(FREQUENCY_DEDUPLICATION);//文案去重DeduplicationParamcontentParams=DeduplicationParam.builder().deduplicationTime(contentDeduplication.getLong(TIME)).countNum(contentDeduplication.getInteger(NUM)).taskInfo(taskInfo).anchorState(AnchorState.CONTENT_DEDUPLICATION).build();contentDeduplicationService.deduplication(contentParams);//运【yùn】营总规则【zé】去重(一天内用户【hù】收到【dào】最【zuì】多【duō】同一个渠道的消息【xī】次数【shù】)Longseconds=(DateUtil.endOfDay(newDate()).getTime()-DateUtil.current())/1000;DeduplicationParambusinessParams=DeduplicationParam.builder().deduplicationTime(seconds).countNum(frequencyDeduplication.getInteger(NUM)).taskInfo(taskInfo).anchorState(AnchorState.RULE_DEDUPLICATION).build();frequencyDeduplicationService.deduplication(businessParams);}

那时候【hòu】很简单,基【jī】本主【zhǔ】体逻辑都写在这个【gè】入口【kǒu】上了【le】,应【yīng】该都能看得懂。后来,群里滴滴哥表示这种代码不行,不能一眼看出【chū】来它干了什么【me】。于【yú】是怒提了一【yī】波【bō】pull request重构了一版,入【rù】口是这样【yàng】的:

publicvoidduplication(TaskInfotaskInfo){//配置样【yàng】例:{"contentDeduplication":{"num":1,"time":300},"frequencyDeduplication":{"num":5}}Stringdeduplication=config.getProperty(DeduplicationConstants.DEDUPLICATION_RULE_KEY,AustinConstant.APOLLO_DEFAULT_VALUE_JSON_OBJECT);//去【qù】重DEDUPLICATION_LIST.forEach(key->{DeduplicationParamdeduplicationParam=builderFactory.select(key).build(deduplication,key);if(deduplicationParam!=null){deduplicationParam.setTaskInfo(taskInfo);DeduplicationServicededuplicationService=findService(key+SERVICE);deduplicationService.deduplication(deduplicationParam);}});}

我猜想他的思路就是把【bǎ】构建去【qù】重【chóng】参数和选择具体的去重服务给封装【zhuāng】起来了,在【zài】最外层的【de】代码看起来就【jiù】很简洁了【le】。后【hòu】来又跟【gēn】他聊了下,他的【de】设计思【sī】路是这样的【de】:考【kǎo】虑到以后会有其他【tā】规则的去重就把【bǎ】去重【chóng】逻辑单独封装起来了【le】,之后用策略模版的设计【jì】模式进行【háng】了重构,重构后的代【dài】码 模版不变,支【zhī】持各种不同【tóng】策略的【de】去重,扩展【zhǎn】性更高更【gèng】强更简洁

确实牛逼。

我基于上面的思路微改了下入口,代码最终演变成这样:

publicvoidduplication(TaskInfotaskInfo){//配置样【yàng】例:{"deduplication_10":{"num":1,"time":300},"deduplication_20":{"num":5}}StringdeduplicationConfig=config.getProperty(DEDUPLICATION_RULE_KEY,CommonConstant.EMPTY_JSON_OBJECT);//去【qù】重ListdeduplicationList=DeduplicationType.getDeduplicationList();for(IntegerdeduplicationType:deduplicationList){DeduplicationParamdeduplicationParam=deduplicationHolder.selectBuilder(deduplicationType).build(deduplicationConfig,taskInfo);if(Objects.nonNull(deduplicationParam)){deduplicationHolder.selectService(deduplicationType).deduplication(deduplicationParam);}}}

到这,应该大多数人还【hái】能跟上吧?在【zài】讲具【jù】体【tǐ】的代码之前,我们先来简单【dān】看看去重功能【néng】的代【dài】码结构(这会对后面看代【dài】码有帮助【zhù】)

去重的逻辑可以【yǐ】统一抽象为:在X时【shí】间段内达到了【le】Y阈值【zhí】,还记得我曾经说过【guò】:「去重【chóng】」的【de】本质:「业务【wù】Key」+「存储」。那么去重实现的步骤可以简单分为(我【wǒ】这边存储【chǔ】就【jiù】用【yòng】的Redis):

通【tōng】过Key从【cóng】Redis获取记录判断该【gāi】Key在Redis的记录【lù】是【shì】否符合【hé】条件符合条件【jiàn】的则去重,不符合条件的则【zé】重新塞进Redis更新记录

为【wéi】了方便调整去重的参【cān】数,我把X时间段和Y阈值【zhí】都放到了配置【zhì】里{"deduplication_10":{"num":1,"time":300},"deduplication_20":{"num":5}}。目前有【yǒu】两【liǎng】种去重的具体实现:

1、5分钟内相同用户如果收到相同的内容,则应该被过滤掉

2、一天内相同的用户如果【guǒ】已【yǐ】经收【shōu】到【dào】某渠道内容5次,则应该被过滤掉

从【cóng】配置【zhì】中心拿到配置信息【xī】了以后,Builder就是【shì】根据这【zhè】两种类型去构建出DeduplicationParam,就是以下【xià】代码:

DeduplicationParamdeduplicationParam=deduplicationHolder.selectBuilder(deduplicationType).build(deduplicationConfig,taskInfo);

Builder和DeduplicationService都【dōu】用【yòng】了【le】类似的写法【fǎ】(在【zài】子类初始化的时候指定类型,在父类统一接【jiē】收,放到Map里管【guǎn】理)

而统一【yī】管理着这些服【fú】务有个中【zhōng】心的地方,我把这取名为【wéi】DeduplicationHolder

/***@authorhuskey*@date2022/1/18*/@ServicepublicclassDeduplicationHolder{privatefinalMapbuilderHolder=newHashMap>(4);privatefinalMapserviceHolder=newHashMap>(4);publicBuilderselectBuilder(Integerkey){returnbuilderHolder.get(key);}publicDeduplicationServiceselectService(Integerkey){returnserviceHolder.get(key);}publicvoidputBuilder(Integerkey,Builderbuilder){builderHolder.put(key,builder);}publicvoidputService(Integerkey,DeduplicationServiceservice){serviceHolder.put(key,service);}}

前面【miàn】提到的业务Key,是在AbstractDeduplicationService的【de】子【zǐ】类下构建的:

而具【jù】体的去重逻【luó】辑【jí】实现则都在【zài】LimitService下,{一天内【nèi】相同的用户【hù】如【rú】果已经收到某渠道内【nèi】容【róng】5次}是在SimpleLimitService中处理使用【yòng】mget和pipelineSetEX就完【wán】成了【le】实现。而{5分钟内相同用户如果收到【dào】相【xiàng】同的内容}是在SlideWindowLimitService中处理,使用了lua脚本完【wán】成了实现【xiàn】。

LimitService的代码都【dōu】来源于@caolongxiu的【de】pull request,建议大【dà】家可以对【duì】比commit再学习一【yī】番:https://gitee.com/zhongfucheng/austin/pulls/19

1、频次去重采用普通的计数去重方法,限制的是每天发送的条数。

2、内容去重采用的是新【xīn】开【kāi】发的基于redis中zset的滑动窗口去重,可【kě】以做到【dào】严格控制单位时间【jiān】内的频次。

3、redis使用lua脚本来保证原子性和减少网络io的损耗

4、redis的key增【zēng】加前缀做到数据隔离(后期可能有动【dòng】态【tài】更换去重方法的需求)

5、把具【jù】体限流【liú】去重方法【fǎ】从DeduplicationService抽取出来【lái】,DeduplicationService只需设置构造器注入时注【zhù】入【rù】的【de】AbstractLimitService(具体限流去【qù】重服务)类型【xíng】即可动态更换去【qù】重的方法 6、使用雪花算法生成zset的【de】唯【wéi】一value,score使用的是当前的时间【jiān】戳

针对滑动窗【chuāng】口去重【chóng】,有会引申出新【xīn】的问【wèn】题:limit.lua的【de】逻辑?为什么要【yào】移除时【shí】间窗口的之前的【de】数【shù】据?为什么ARGV[4]参【cān】数要【yào】唯一?为什么要expire?

A: 使【shǐ】用滑【huá】动窗【chuāng】口【kǒu】可以保证【zhèng】N分钟【zhōng】达到N次【cì】进行去重。滑动窗口可以回【huí】顾下TCP的,也可以回顾下刷【shuā】LeetCode时的一些题,那这为【wéi】什【shí】么要移除,就不陌生了。

为什么【me】ARGV[4]要唯一,具体可【kě】以看看zadd这【zhè】条【tiáo】命令,我们只【zhī】需要【yào】保证每次【cì】add进窗口【kǒu】内【nèi】的成【chéng】员是唯一的,那【nà】么就不会触发有更新的操作(我认为这样设计会更加简单些),而唯【wéi】一Key用雪【xuě】花算法比较【jiào】方便。

为什么expire?,如果【guǒ】这个key只被调【diào】用一次。那就【jiù】很【hěn】有可能在redis内存常驻了【le】,expire能避免这【zhè】种情况。

推荐项目

最【zuì】后再叨叨吧,很多人可能会发一段截【jié】图,跑来问我【wǒ】为什么要这样写,为什么要以【yǐ】这【zhè】种【zhǒng】方【fāng】式实【shí】现,能不能以这种【zhǒng】方式实【shí】现【xiàn】。这时候,我更想看到的是:你已经【jīng】实现了第二种【zhǒng】方式了,然后探【tàn】讨你写的这【zhè】种方案好【hǎo】不好,现有【yǒu】的代码差在哪【nǎ】里。

毕竟【jìng】问问【wèn】题很【hěn】简单,我又不【bú】是客服,总【zǒng】不能没诚意的问题【tí】我都得一一回答吧。

如果想学Java项目【mù】的,我【wǒ】还是强烈推荐我的开源【yuán】项【xiàng】目消息【xī】推送平台Austin,可以用作毕业设计,可以用【yòng】作【zuò】校【xiào】招,可以看【kàn】看生【shēng】产环境是怎么推送消息的。

仓库【kù】地址(可点击阅读原文跳转【zhuǎn】):https://gitee.com/zhongfucheng/austin

我开【kāi】通了股东服务内容【róng】,感兴趣可以点击【jī】下方看看【kàn】,主要针对的【de】是项目【mù】哟

VIP服务

为你推荐

最新资讯

股票软件