最近有个段子,程序员不要拷贝代码,而是要一行一行的从新抄过去。好嘛,段子归段子,笔主始终以为
think more, code less
,多思考,能少写的代码尽可能少写,从而也与标题呼应上,懒才制造生产力。继上次写的 excel 工具类以后,此次又来给你们 show 一下笔主是如何释放生产力的。java
项目中常常会有触达需求,简单来讲就是群发微信公众号消息。这里有个刚需就是去重,已经下发过的用户再也不下发,多个标签用户之间也可能会存在重复的用户 ,这些都须要去重。git
两层循环遍历
:最 low 的办法不就是套两层循环遍历,时间复杂度 n 方。HashSet
:稍微有点追求的小伙伴可能会想到使用 HashSet 去重,没错,HashSet 确实是个不错的选择,时间复杂度很低。可是面对海量数据,HashSet 彷佛就不太适合了,HashSet 有个弱点就是,空间利用率低,并且元素占用的空间也相对较大。布隆过滤器
:针对海量数据去重场景,布隆过滤器应运而生,关于布隆过滤器网上不少博客都说的很详细。做者下面就简单说一下布隆过滤器的关键点,把主要精力放在如何应用到实际项目中。程序员
使用场景
:判断某个元素是否在海量数据之中。存储结构
:使用 比特
数组存储。添加元素
:
检查元素
:
主要是为了下降哈希冲突引起的偏差,对于 HashMap 来讲,哈希冲突的时候,会用链表或者是红黑树将全部冲突的元素都保存起来。可是对于布隆过滤器不能也不须要把冲突的 key 用链表链接起来,由于他只须要判断 key 是否存在。github
可使用这个在线计算工具:Bloom Filter Calculatorweb
在介绍具体的实现过程以前,先看看做者手撸的去重工具的正确使用姿式。数据库
根据约定将去重器的配置文件放在 deduplication.properties
下。数组
bloomList=goods,wechat
tableInfo=goods:goods_deduplication,wechat:wechat_deduplication
expectedInsertions=goods:200000
复制代码
bloomList
:布隆过滤器 bean 的名称,一个去重业务对应一个值,多个值之间使用逗号分割。tableInfo
:布隆过滤器对应的数据表名,不配置会使用默认值。expectedInsertions
:布隆过滤器的预计数据量,这个对数据的准确性有较大的影响,须要根据实际业务进行评估,默认值为 10000000。配置完上面的信息以后,就可使用去重器了,使用姿式以下图所示,核心代码就一行。传入待比较的 List 和业务对应的布隆过滤器 bean 名称,返回目前还不存在的记录,且会将该记录入库,下次再进行去重,该记录就不会再出现了。微信
测试程序: 多线程
运行程序以前,已存在的数据以下,identifier 为 十二、1六、13 的记录已经存在了。 less
程序运行结果:
数据库状态:
能够看到成功将 十一、1四、15 的记录返回且成功入库。
下面主要讲的是去重器实现的核心步骤,在分析代码以前,先把 ✨去重器源码地址✨ 贴出来,嘻嘻嘻,欢迎你们来 star。该工具的核心代码都在 deduplication 这个包下面。
传入任意的惟一标识 List,或者是指定惟一标识的 CSV 文件进行去重,对任意的业务均适用。
建立布隆过滤
:针对某个去重业务须要一个布隆过滤器与之对应。初始化布隆过滤器
:每一个布隆过滤器的初始化数据从对应的数据库表中取。使用布隆过滤器
:使用初始化事后的布隆过滤器判断元素是否存在,存在入库。步骤很简单,可是想要实现的优雅点,使用起来方便一点,仍是须要 think 一下。
这里做者使用的布隆过滤器是 guava 提供的实现,并无重复造轮子,毕竟 Google 出品。
这里主要解决的问题点有两个:
BloomFilter#create()
方法,可是咱们的目标是懒!一懒到底!没理由新增一个去重业务,咱们就去翻代码,new 一个布隆过滤器出来吧。因此这里咱们须要向 Spring 容器动态注册 bean
的能力,可使用 Spring 提供的 BeanDefinitionRegistryPostProcessor
接口 来实现这个功能。建立布隆过滤核心代码:
上面标出了四个关键点:
not ready
状态,不能开放使用。通过上面的步骤,咱们已经建立好了全部的布隆过滤器。
这一步主要是从数据库中查询数据,而后塞到布隆过滤器中。可能有朋友会问,为何还要把初始化这一步单独拎出来?直接在建立bean的时候初始化不就好了。主要是由于在动态注册 bean 的时候,容器的上下文环境尚未准备好阶段。在容器中还没法获取到其余的 bean 。因此咱们只能把初始化放在这一阶段来作。
在这里做者使用了 ApplicationListener 监听,他能够监听 SpringBoot 应用的生命周期,做者在这里监听了 SpringBoot 启动完成以后的事件,把对布隆过滤器的初始化工做放到这里面来进行。
DeduplicationApplicationListener 监听器核心代码:
initBloomFilter()
初始化每一个布隆过滤器。initBloomFilter 方法:
ApplicationContextAware
动态获取 bean 实例。fillBloomFilter
方法。fillBloomFilter 方法:
这里面主要的工做就是将数据库中的数据塞到布隆过滤器中,一般这一步查询出来的数据会很是的多,这里选择了将查询任务切分,多线程查询,而后再合并子任务,没错!就是借鉴 MapReduce 的思想。
数据塞完以后,该布隆过滤器就能够对外开放使用了,因此会将该布隆过滤器对应的初始化状态置为 true。
通过上面的步骤,已经将布隆过滤器初始化完成了,下面对布隆过滤器进行必定的封装就可使用了。
这一步主要是对布隆过滤器进行封装,使之用起来更加的方便,这里主要支持两种类型的数据源去重,分别是 List 和 CSV 文件。这里都是批量去重,入库也是批量入库。若是须要单条记录去重,例如过滤黑名单,就须要本身封装对应的业务了,由于毕竟不一样用户的业务仍是不太同样的。
对外暴露的两个方法:
go 是一个重载函数,仅对内部使用:
这里再来看看 findNotExistRecordFromList 方法。很简单,遍历传入的 list,若是布隆过滤器判断为还不存在,加入到结果集中返回。
关于去重器的核心步骤就介绍到这里啦,对于实现的细节,感兴趣的朋友能够到 github 上把代码 fork 下来看一哈,有问题欢迎随时与我交流。
✨github:cranberry✨ 是做者最近在写的一个项目,主要是对 java web 开发过程当中的一些常见问题的实现与分析,后续也会持续更新,但愿对你们会有帮助,感受不错也能够给作个 star 一个哦!