缓存穿透

缓存穿透 是一个很常见的问题。

抛开恶意攻击不谈,大量的传递依然会访问的 Redis 缓存。

比如黑白名单等信息,存储的比较少,但是实际交易中信息的量特别多。

BloomFilter 使用的问题

  1. 如果一个信息一开始有,后来移除了,则无法从 BloomFilter 中移除。

  2. BloomFilter 虽然使用了多次 hash,尽可能的避免 hash 冲突,但是即使同时存在,依然可能是不存在的。

  3. 如今都是分布式系统,如何保证一个地方的信息修改了,同步更新到 BloomFilter 中。

下面,根据这几点,一个个进行解决。

第二个保证

BloomFilter 保证了如果不存在,则一定不存在。如果存在,则不一定存在。

这个涉及到 1、2 两个问题,我们要结合自己的业务,提供业务的正确性保证。

再次查询的必要性

为了避免1,2的问题,你可以在 BloomFilter 中存在的情况下,再次请求一次。

因为不存在,一定不存在。已经可以为你避免了多次查询。

定期移除 BloomFileter 中的脏数据

因为问题1,有时候数据移除了,但是 BloomFiter 中依然存在,为了尽可能的保证准确性,我们需要定期去重新构建我们的 BloomFilter。

为了保证业务的正确进行,我们在构建的过程中,不直接删除旧的过滤器,而是在构建完成中,直接将过滤器替换为最新的。

替换的频率

可以根据自己的业务场景,数据修改的频率来定。

如果不是很频繁的话,一个月重新构建一次也是可以的。(可以直接写一个定时 JOB 去执行。)

不建议特别频繁,因为对性能还是有一定的影响。

替换的数据源

为了每次可以构建正确的过滤器,我们需要将数据存储在一个地方,这个数据可以被所有需要构建的服务访问。

一般建议使用 Redis 集群。

数据如何同步到 BloomFilter

单系统

传统的单系统非常好处理,每次修改直接更新 BloomFilter 即可。

如果是分布式系统怎么办呢?

分布式

分布式系统其实有两种方式。

一种是 pull,一种是 push。

pull

pull 是定时拉取,如果是你想避免大量的网络传输,建议使用增量拉取的方式。(可以按照更新时间,或者数据库字段添加标志位,定时跑批来执行。)

优点:不需要引入复杂的中间件。一个 RPC/HTTP 接口即可。

缺点:存在时延。

push

push 是推送的方式,需要考虑到每一个服务都要接收到对应的变更信息。

你可以使用发布/订阅的方式,比如 MQ/Redis 都提供了这种功能。

优点:消息的即时性。MQ 可以保证消息的不丢失。

缺点:引入中间件,增加程序的复杂性。

数据的持久化

解决了上面的问题之后,还有一个问题我们需要考虑一下。

比如如果我们的程序重启了数据怎么办?

因为我们是采用增量的方式,下次再启动以前的信息就会丢失。

持久化

解决这个问题的答案只有一个,那就是持久化。

把内存中的内容持久化到硬盘中。不过可喜的是,BloomFilter 的内存占用是不大的。

你甚至可以自己实现各序列化,直接写到文件,然后下次项目启动的时候读取一下即可。

EhCahe

写到这里,我就想到了一个本地缓存非常棒的实现 EhCache

提供了数据的持久化功能,值得借鉴。

停机的问题

有时候我们会上线,有时候可能服务挂掉了。

如果运维做的比较好的话,有优雅停机,可以保证执行任务的线程运行完成才被干掉。

顺序

作为数据的加载端,要知道各个服务的执行顺序。

比如我们的数据源,先进行停机。

然后,定时拉取(消息的订阅方)才停机。因为可能刚好这一刻有新的数据过来了还没有处理完成。

总结

这里看起来是 BloomFilter 的用法。

实际上如果你想使用本地缓存,也可以使用完全相同的套路。

本地缓存,可以避免大量的网络IO消耗。毕竟 Redis 再优秀,底层也是需要网络通信的。

随着业务量的激增,Redis 本身的单线程如果存在网络抖动,则会对业务产生比较大的影响。

拓展阅读

bloom-filter

参考资料