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

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

大家好,我3y啊。由于去重逻辑重构【gòu】了几【jǐ】次,好多股东直【zhí】呼看不【bú】懂,于是【shì】我今【jīn】天【tiān】再安排一波【bō】对代码【mǎ】的解析【xī】吧。austin支持两【liǎng】种【zhǒng】去重的类型:N分钟相【xiàng】同内容达到N次去重和【hé】一天内N次【cì】相同【tóng】渠道频次去重。

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


(资料图片仅供参考)

publicvoidduplication(TaskInfotaskInfo){//配置【zhì】示例:{"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);//文【wén】案去重DeduplicationParamcontentParams=DeduplicationParam.builder().deduplicationTime(contentDeduplication.getLong(TIME)).countNum(contentDeduplication.getInteger(NUM)).taskInfo(taskInfo).anchorState(AnchorState.CONTENT_DEDUPLICATION).build();contentDeduplicationService.deduplication(contentParams);//运营总【zǒng】规【guī】则去重(一天内用户【hù】收【shōu】到最多同【tóng】一个渠道的消息次【cì】数)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);}

那【nà】时候【hòu】很简单【dān】,基【jī】本主体逻辑都写在这个入【rù】口上了,应该都能看得懂。后来【lái】,群【qún】里滴【dī】滴哥表【biǎo】示这种代码不行,不【bú】能一眼看出来它干【gàn】了【le】什么。于是怒提【tí】了一波pull request重【chóng】构了一版,入口是这样的:

publicvoidduplication(TaskInfotaskInfo){//配置样例:{"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);}});}

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

确实牛逼。

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

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

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

去重【chóng】的逻辑可【kě】以统一抽象【xiàng】为【wéi】:在【zài】X时间段内达到了Y阈值,还记得我曾经说【shuō】过【guò】:「去重【chóng】」的本质:「业务Key」+「存储【chǔ】」。那么去重【chóng】实现的步骤可以简单【dān】分为(我这边存储就用【yòng】的Redis):

通过Key从Redis获取【qǔ】记录判【pàn】断该Key在Redis的记录是否【fǒu】符合条件符合【hé】条件的则去重,不符【fú】合条件【jiàn】的则重新塞进【jìn】Redis更新记录【lù】

为了方便调整去重的【de】参数【shù】,我把X时间【jiān】段和Y阈值都放到了配置里{"deduplication_10":{"num":1,"time":300},"deduplication_20":{"num":5}}。目前有两种去重的【de】具【jù】体【tǐ】实【shí】现:

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

2、一天【tiān】内相同的用户如【rú】果已经收到某【mǒu】渠道内容5次【cì】,则应该被【bèi】过滤掉

从【cóng】配置中心拿到配置信息了【le】以后,Builder就是根据这两【liǎng】种【zhǒng】类【lèi】型去构建【jiàn】出DeduplicationParam,就是以下【xià】代码:

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

Builder和DeduplicationService都用了类似的【de】写【xiě】法(在子类初始【shǐ】化的时候【hòu】指定类【lèi】型,在父类统一【yī】接收,放到Map里管理)

而统一管【guǎn】理【lǐ】着这些服【fú】务有个中心的【de】地方,我把【bǎ】这取名为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);}}

前面提【tí】到【dào】的业务Key,是在AbstractDeduplicationService的子类下【xià】构建的:

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

LimitService的代码【mǎ】都来源于@caolongxiu的pull request,建议大家【jiā】可【kě】以对【duì】比commit再学习一番:https://gitee.com/zhongfucheng/austin/pulls/19

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

2、内容【róng】去重采用【yòng】的是新开【kāi】发的基于redis中zset的滑动窗口去重【chóng】,可以做到严【yán】格控【kòng】制【zhì】单【dān】位时间内的频次。

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

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

5、把具【jù】体限流去重方【fāng】法从DeduplicationService抽取出来,DeduplicationService只【zhī】需设置构造器【qì】注入【rù】时注入【rù】的AbstractLimitService(具体限流去【qù】重【chóng】服务)类型即可【kě】动【dòng】态更换去重【chóng】的方法 6、使用雪【xuě】花【huā】算法生成zset的唯一value,score使用的是当前的时【shí】间戳【chuō】

针对【duì】滑动窗口去重,有会引申出【chū】新的问题:limit.lua的逻【luó】辑【jí】?为【wéi】什么要移除时间窗口的之前【qián】的数据【jù】?为什么ARGV[4]参数要【yào】唯一?为什【shí】么要【yào】expire?

A: 使用滑动窗口【kǒu】可以保证N分钟达到【dào】N次进行去【qù】重。滑动窗口【kǒu】可以回顾下TCP的,也可以回顾【gù】下刷【shuā】LeetCode时的【de】一些题,那【nà】这【zhè】为什么要移除,就不陌【mò】生了。

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

为什么expire?,如果这个【gè】key只被调用一次。那就【jiù】很有可能在redis内存常【cháng】驻【zhù】了【le】,expire能避免这种情况。

推荐项目

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

毕竟【jìng】问问题很简单,我又不是客【kè】服,总不【bú】能没【méi】诚意的问题我都【dōu】得一【yī】一回答吧。

如【rú】果想学【xué】Java项目的【de】,我【wǒ】还是强【qiáng】烈推荐我的【de】开源【yuán】项目消【xiāo】息推送平台Austin,可以用作毕【bì】业设【shè】计,可以用作校招,可以看看生产环境是怎么推送消息的。

仓库地址【zhǐ】(可点击阅读原【yuán】文跳转):https://gitee.com/zhongfucheng/austin

我开通了股【gǔ】东服务内容,感兴【xìng】趣可以点击下方看看【kàn】,主要针对的是【shì】项目哟

VIP服务

为你推荐

最新资讯

股票软件