大家好,我3y啊。由于去重逻辑重构了几次,很多股东直呼看不懂,于是我今日再安排一波对代码的解析吧。austin支撑两种去重的类型:N分钟相同内容达到N次去重和一天内N次相同途径频次去重。

Java开源项目音讯推送渠道推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等音讯类型

  • gitee.com/zhongfuchen…
  • github.com/ZhongFuChen…

在最开端,我的第一版完成是这样的:

public void duplication(TaskInfo taskInfo) {
  // 装备示例:{"contentDeduplication":{"num":1,"time":300},"frequencyDeduplication":{"num":5}}
  JSONObject property = JSON.parseObject(config.getProperty(DEDUPLICATION_RULE_KEY, AustinConstant.APOLLO_DEFAULT_VALUE_JSON_OBJECT));
  JSONObject contentDeduplication = property.getJSONObject(CONTENT_DEDUPLICATION);
  JSONObject frequencyDeduplication = property.getJSONObject(FREQUENCY_DEDUPLICATION);
​
  // 案牍去重
  DeduplicationParam contentParams = DeduplicationParam.builder()
     .deduplicationTime(contentDeduplication.getLong(TIME))
     .countNum(contentDeduplication.getInteger(NUM)).taskInfo(taskInfo)
     .anchorState(AnchorState.CONTENT_DEDUPLICATION)
     .build();
  contentDeduplicationService.deduplication(contentParams);
​
​
  // 运营总规矩去重(一天内用户收到最多同一个途径的音讯次数)
  Long seconds = (DateUtil.endOfDay(new Date()).getTime() - DateUtil.current()) / 1000;
  DeduplicationParam businessParams = DeduplicationParam.builder()
     .deduplicationTime(seconds)
     .countNum(frequencyDeduplication.getInteger(NUM)).taskInfo(taskInfo)
     .anchorState(AnchorState.RULE_DEDUPLICATION)
     .build();
  frequencyDeduplicationService.deduplication(businessParams);
}

那时分很简略,基本主体逻辑都写在这个进口上了,应该都能看得懂。后来,群里滴滴哥表明这种代码不行,不能一眼看出来它干了什么。于是怒提了一波pull request重构了一版,进口是这样的:

public void duplication(TaskInfo taskInfo) {
  
  // 装备样例:{"contentDeduplication":{"num":1,"time":300},"frequencyDeduplication":{"num":5}}
  String deduplication = config.getProperty(DeduplicationConstants.DEDUPLICATION_RULE_KEY, AustinConstant.APOLLO_DEFAULT_VALUE_JSON_OBJECT);
  
  //去重
  DEDUPLICATION_LIST.forEach(
    key -> {
      DeduplicationParam deduplicationParam = builderFactory.select(key).build(deduplication, key);
      if (deduplicationParam != null) {
        deduplicationParam.setTaskInfo(taskInfo);
        DeduplicationService deduplicationService = findService(key + SERVICE);
        deduplicationService.deduplication(deduplicationParam);
       }
     }
   );
}

我猜测他的思路便是把构建去重参数挑选详细的去重服务给封装起来了,在最外层的代码看起来就很简练了。后来又跟他聊了下,他的规划思路是这样的:考虑到今后会有其他规矩的去重就把去重逻辑单独封装起来了,之后用策略模版的规划模式进行了重构,重构后的代码 模版不变,支撑各种不同策略的去重,扩展性更高更强更简练

确实牛逼

我根据上面的思路微改了下进口,代码最终演变成这样:

public void duplication(TaskInfo taskInfo) {
  // 装备样例:{"deduplication_10":{"num":1,"time":300},"deduplication_20":{"num":5}}
  String deduplicationConfig = config.getProperty(DEDUPLICATION_RULE_KEY, CommonConstant.EMPTY_JSON_OBJECT);
​
  // 去重
  List<Integer> deduplicationList = DeduplicationType.getDeduplicationList();
  for (Integer deduplicationType : deduplicationList) {
    DeduplicationParam deduplicationParam = deduplicationHolder.selectBuilder(deduplicationType).build(deduplicationConfig, taskInfo);
    if (Objects.nonNull(deduplicationParam)) {
      deduplicationHolder.selectService(deduplicationType).deduplication(deduplicationParam);
     }
   }
}

到这,应该大多数人还能跟上吧?在讲详细的代码之前,咱们先来简略看看去重功用的代码结构(这会对后边看代码有帮助)

Java如何实现去重?这是在炫技吗?

去重的逻辑能够统一抽象为:在X时刻段内达到了Y阈值,还记得我曾经说过:「去重」的本质:「事务Key」+「存储。那么去重完成的步骤能够简略分为(我这边存储就用的Redis):

  • 通过KeyRedis获取记载
  • 判别该KeyRedis的记载是否符合条件
  • 符合条件的则去重,不符合条件的则重新塞进Redis更新记载

为了方便调整去重的参数,我把X时刻段Y阈值都放到了装备里{"deduplication_10":{"num":1,"time":300},"deduplication_20":{"num":5}}。目前有两种去重的详细完成:

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

2、一天内相同的用户假如已经收到某途径内容5次,则应该被过滤掉

从装备中心拿到装备信息了今后,Builder便是根据这两种类型去构建出DeduplicationParam,便是以下代码:

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

BuilderDeduplicationService都用了相似的写法(在子类初始化的时分指定类型,在父类统一接收,放到Map里办理

Java如何实现去重?这是在炫技吗?

Java如何实现去重?这是在炫技吗?

而统一办理着这些服务有个中心的地方,我把这取名为DeduplicationHolder

/**
 * @author huskey
 * @date 2022/1/18
 */
@Service
public class DeduplicationHolder {
​
  private final Map<Integer, Builder> builderHolder = new HashMap<>(4);
  private final Map<Integer, DeduplicationService> serviceHolder = new HashMap<>(4);
​
  public Builder selectBuilder(Integer key) {
    return builderHolder.get(key);
   }
​
  public DeduplicationService selectService(Integer key) {
    return serviceHolder.get(key);
   }
​
  public void putBuilder(Integer key, Builder builder) {
    builderHolder.put(key, builder);
   }
​
  public void putService(Integer key, DeduplicationService service) {
    serviceHolder.put(key, service);
   }
}

前面说到的事务Key,是在AbstractDeduplicationService的子类下构建的:

Java如何实现去重?这是在炫技吗?

而详细的去重逻辑完成则都在LimitService下,{一天内相同的用户假如已经收到某途径内容5次}是在SimpleLimitService中处理运用mgetpipelineSetEX就完成了完成。而{5分钟内相同用户假如收到相同的内容}是在SlideWindowLimitService中处理,运用了lua脚本完成了完成。

Java如何实现去重?这是在炫技吗?

LimitService的代码都来源于@caolongxiu的pull request建议大家能够对比commit再学习一番:gitee.com/zhongfuchen…

1、频次去重采用普通的计数去重办法,约束的是每天发送的条数。 2、内容去重采用的是新开发的根据rediszset滑动窗口去重,能够做到严格控制单位时刻内的频次。 3、redis运用lua脚原本确保原子性和减少网络io的损耗 4、rediskey添加前缀做到数据阻隔(后期可能有动态更换去重办法的需求) 5、把详细限流去重办法从DeduplicationService抽取出来,DeduplicationService只需设置构造器注入时注入的AbstractLimitService(详细限流去重服务)类型即可动态更换去重的办法 6、运用雪花算法生成zset的仅有value,score运用的是当前的时刻戳

针对滑动窗口去重,有会引申出新的问题:limit.lua的逻辑?为什么要移除时刻窗口的之前的数据?为什么ARGV[4]参数要仅有?为什么要expire?

Java如何实现去重?这是在炫技吗?

A: 运用滑动窗口能够确保N分钟达到N次进行去重。滑动窗口能够回忆下TCP的,也能够回忆下刷LeetCode时的一些题,那这为什么要移除,就不陌生了。

为什么ARGV[4]要仅有,详细能够看看zadd这条指令,咱们只需要确保每次add进窗口内的成员是仅有的,那么就不会触发有更新的操作(我以为这样规划会愈加简略些),而仅有Key用雪花算法比较方便。

为什么expire?,假如这个key只被调用一次。那就很有可能在redis内存常驻了,expire能避免这种状况。

假如想学Java项目的,强烈推荐我的项目音讯推送渠道Austin(8K stars),能够用作毕业规划**,能够用作校招,能够看看生产环境是怎么推送音讯的。音讯推送渠道推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等音讯类型**。

  • gitee.com/zhongfuchen…
  • github.com/ZhongFuChen…