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

news2025/1/9 1:05:20

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

Java开源项目消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型

  • https://gitee.com/zhongfucheng/austin/
  • https://github.com/ZhongFuCheng3y/austin

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

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);
        }
    }
}

到这,应该大多数人还能跟上吧?在讲具体的代码之前,我们先来简单看看去重功能的代码结构(这会对后面看代码有帮助)

去重的逻辑可以统一抽象为:在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里管理

而统一管理着这些服务有个中心的地方,我把这取名为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的子类下构建的:

而具体的去重逻辑实现则都在LimitService下,{一天内相同的用户如果已经收到某渠道内容5次}是在SimpleLimitService中处理使用mgetpipelineSetEX就完成了实现。而{5分钟内相同用户如果收到相同的内容}是在SlideWindowLimitService中处理,使用了lua脚本完成了实现。

LimitService的代码都来源于@caolongxiu的pull request建议大家可以对比commit再学习一番:https://gitee.com/zhongfucheng/austin/pulls/19

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

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

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

为什么ARGV[4]要唯一,具体可以看看zadd这条命令,我们只需要保证每次add进窗口内的成员是唯一的,那么就不会触发有更新的操作(我认为这样设计会更加简单些),而唯一Key用雪花算法比较方便。

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

如果想学Java项目的,强烈推荐我的项目消息推送平台Austin(8K stars),可以用作毕业设计**,可以用作校招,可以看看生产环境是怎么推送消息的。消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型**。

  • https://gitee.com/zhongfucheng/austin/
  • https://github.com/ZhongFuCheng3y/austin

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/613393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高性能服务器为什么需要内存池?

C/C程序为什么比起其它语言开发的程序效率要高&#xff0c;一个很重要的原因就是可以直接操作内存&#xff0c;今天就来讲讲为什么需要内存池的技术。 从一个示例开始 先看下面两段代码&#xff0c;都是去重复的创建和删除对象0x5FFFFF次&#xff0c;他们的执行后的效率怎么样…

DIP第7章知识点

文章目录 7.3 相关7.5 基图像7.6 傅里叶相关变换7.6.1 离散哈特利变换7.6.3 离散正弦变换 DIP的其他章节都好复习&#xff0c;唯独就这个第7章小波变换。复习起来十分头大&#xff0c;所以我开始写他的课后题&#xff0c;雾。 7.3 相关 已知两个连续函数 f ( x ) f(x) f(x) …

一篇博客教会你怎么使用Docker安装Redis

文章目录 1、搜索镜像2、拉取镜像3、下载配置文件4、修改配置文件5、启动容器 今天我们学习使用 Docker 安装 Redis。 1、搜索镜像 docker search redis2、拉取镜像 docker pull redis3、下载配置文件 wget http://download.redis.io/redis-stable/redis.conf下载的配置文件…

云原生Docker容器管理

docker容器相当于一个进程&#xff0c;性能接近于原生&#xff0c;几乎没有损耗&#xff1b; docker容器在单台主机上支持的数量成百上千&#xff1b; 容器与容器之间相互隔离&#xff1b; 镜像是创建容器的基础&#xff0c;可以理解镜像为一个压缩包 docker容器的管理 容器…

彩票中奖率的真相:用 JavaScript 看透彩票背后的随机算法

原本这篇文章是打算叫「假如我是彩票系统开发者」&#xff0c;但细想一下&#xff0c;如果在文章中引用太多的 JavaScript 的话&#xff0c;反而不是那么纯粹&#xff0c;毕竟也只是我的一厢情愿&#xff0c;彩票开发也不全如本文所讲&#xff0c;有所误导的话便也是得不偿失了…

理解 Java 关键字 final

原文链接 理解 Java 关键字 final final可以用来干什么 final是Java中非常常见的一个关键字&#xff0c;可以说每天都在使用它&#xff0c;虽然常见&#xff0c;但却也不见得都那么显而易见&#xff0c;今天就来研究一下final&#xff0c;以加深对它的理解和更合理的运用。 修…

gitstack使用教程

一、下载及安装 下载地址&#xff1a;https://gitstack.com/download/?spma2c4e.10696291.0.0.6d4c19a40qOauc 支持操作系统列表 本文基于2.3.12版本 下载完成后安装&#xff0c;默认安装路径为&#xff1a;c:\GitStack&#xff0c;安装过程中&#xff0c;gitstack服务会启…

医学图像分割 nnUNetV2 分割自定义2d数据集

文章目录 1 环境安装(Pytorch)1.1 安装conda1.1 安装pytorch1.3 安装nnUNet1.4 安装隐藏层&#xff08;可选&#xff09; 2 配置自定义数据集2.1 数据集格式2.2 创建需要目录2.3 数据格式转换2.3.1 修改路径与数据集名称2.3.2 修改训练集与测试集2.3.3 修改掩码所在的文件夹&am…

python基础----05-----函数的多返回值、函数的多种参数使用形式、函数作为参数传递、lambda匿名函数

一 函数的多返回值 if __name__ __main__:# 演示使用多个变量&#xff0c;接收多个返回值def test_return ():return 1,hello,Truex,y,z test_return()print(x)print(y)print(z)1helloTrue二 函数的多种参数使用形式 分为以下四种。 2.1 位置参数 位置参数调用函数时根据…

卡尔曼滤波与组合导航原理(五)序贯Kalman滤波

量测维数很高&#xff0c;而且能写成很多分量&#xff0c;每一个分量可以看成一个小量测&#xff0c;可以序贯进行量测更新 优点是&#xff1a;计算快&#xff0c;数字稳定性更好&#xff0c;我们知道矩阵求逆是和维数的三次方成正比&#xff0c;分成小矩阵求逆快&#xff08;都…

自学大语言模型之Bert和GPT的区别

Bert和GPT的区别 起源 2018 年&#xff0c;Google 首次推出 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;。该模型是在大量文本语料库上结合无监督和监督学习进行训练的。 BERT 的目标是创建一种语言模型&#xff0c;可以理解句子中…

(Day1)配置云开发提供的模板

创建云开发项目 打开微信开发者工具&#xff1b;点击“项目”->“新建项目”&#xff1b;输入项目名称和选择项目所要保存的目录&#xff1b;输入自己的AppID&#xff1b; AppID的获取&#xff0c;需要登陆微信公众平台&#xff0c;并点击“开发管理”->"开发设置…

凸优化系列——凸函数

1.凸函数的定义 凸函数直观上来说&#xff0c;就是两点之间的函数值小于两点连线的函数值 常见凸函数 线性函数既是凸函数&#xff0c;也是凹函数 对于二次函数&#xff0c;如果Q矩阵是半正定矩阵&#xff0c;那么它的二阶导为Q为半正定矩阵&#xff0c;根据凸性判定的二阶条…

SpringCloud微服务架构 --- 高级篇

一、初识Sentinel 1.1、雪崩问题及解决方案 1.1.1、雪崩问题 微服务调用链路中的某个服务故障&#xff0c;引起整个链路中的所有微服务都不可用&#xff0c;这就是雪崩。 1.1.2、解决雪崩问题的常见方式有四种 1.1.2.1、超时处理 设定超时时间&#xff0c;请求超过一定时间…

Swagger原理

最近在基于Swagger进行二次开发&#xff0c; 来对项目的接口进行管理&#xff0c;功能实现了&#xff0c;但是不清楚swagger的工作原理&#xff0c;为了后续能更好利用Swagger来管理接口&#xff0c;而且能借鉴Swagger的原理&#xff0c;将项目中其他信息可视化展示&#xff0c…

什么是测试驱动开发?测试驱动开发有什么优点?

目录 前言 什么是TDD或测试驱动开发&#xff1f; 什么是软件单元测试&#xff1f; 什么是TDD&#xff1f; 测试驱动开发的好处 最后的想法 前言 测试是任何软件开发项目中最重要的步骤之一。如果跳过此过程&#xff0c;则结果可能是灾难性的-对项目和公司而言。但是什么时…

K8s in Action 阅读笔记——【11】Understanding Kubernetes internals

K8s in Action 阅读笔记——【11】Understanding Kubernetes internals 11.1 Understanding the architecture Kubernetes集群分为两个部分&#xff1a; k8s控制平面工作节点 控制平面的组件 构成控制平面的组件有&#xff1a; etcd&#xff1a;etcd是一个分布式的持久化键…

javascrip基础二十八:说说函数节流和防抖?有什么区别?如何实现?

一、是什么 本质上是优化高频率执行代码的一种手段 如&#xff1a;浏览器的 resize、scroll、keypress、mousemove 等事件在触发时&#xff0c;会不断地调用绑定在事件上的回调函数&#xff0c;极大地浪费资源&#xff0c;降低前端性能 为了优化体验&#xff0c;需要对这类事…

类与对象知识总结+构造函数和析构函数 C++程序设计与算法笔记总结(二) 北京大学 郭炜

类和对象 结构化程序设计 C语言使用结构化程序设计&#xff1a; 程序 数据结构 算法 程序由全局变量以及众多相互调用的函数组成。 算法以函数的形式实现&#xff0c;用于对数据结构进行操作。 结构化程序设计的不足&#xff1a; 结构化程序设计中&#xff0c;函数和其所…

《嵌入式系统》知识总结11:STM32串口

串行通信vs并行通信 • 并行&#xff1a;使用8根数据线一次传送一个字节&#xff08;或使用16根数据线一次传送2个字节&#xff0c;...&#xff09; • 串行&#xff1a;使用少量数据信号线&#xff08;8根以下&#xff09;&#xff0c;将数据逐位分时传送 • 并行vs串行&…