Redis 漫谈 —— 分布式布隆过滤及内存使用问题分析

简介: Redis 分布式布隆过滤及内存使用问题分析

背景介绍

公司消息推送系统每日会对旗下百万级体量 app 用户进行消息推送;同时,为了防止过度打扰用户,只允许一天之中对同一个用户设备推送最多 4 条运营类消息 —— 我们选择使用基于 redis 的「分布式布隆过滤」策略实现该限制,具体工具为 github 开源项目 Orestes-Bloomfilter

布隆过滤

布隆过滤

参考上图,布隆过滤器的数据结构为元素初始值均为 0 的「比特数组」;当向布隆过滤器「添加」某个元素(如上图 x、y 或 z)时,需要对该元素进行多次不同算法类型的哈希运算,将运算结果根据布隆过滤器长度取余,得到一批对应的数组位置,并将数组中这些位置的值置为 1;当「校验」某个元素是否已添加至布隆过滤器时,我们进行与「添加」元素时相同的计算得到一批数组位置,然后检查数组中这些对应位置上的值是否为 1 —— 若「均为」 1,可以认为被校验的元素已经存在于布隆过滤器中,否则表示被校验的元素尚不存在。

可以看到布隆过滤器是一种非常紧凑的数据格式;虽然存在一定「误判率」,但是相对于传统的将元素直接存放在集合中用于校验的方式,布隆过滤是一种很好的「以微弱的时间代价换空间」的做法。本文不对该算法做深入详尽的介绍,感兴趣的读者可以查阅相关资料。

工程实践

google 的 guava 包中提供了基于内存的布隆过滤器,但是对于分布式应用而言,存在单点问题和过滤状态维护的不便;于是我们对基于 redis 的分布式过滤进行调研,比较了 ReBloom 和 Orestes-Bloomfilter 两款开源工具;前者需要以模块(module)的方式集成至 redis 中;后者直接使用 setbit 命令与 redis 交互进行布隆过滤器的赋值,并且对并发批量操作做了较好的封装,于是我们优先对后者进行了测试:内存占用方面,一个预期以百万分之一的误判率对 200 万元素进行过滤或者说校验的布隆过滤器约占 7 mb 空间;过滤性能方面,4 个线程并发对 25 万元素(即总量 100 万元素)进行批量校验总耗时约 45 秒 —— 综合评估在可接受的范围,于是我们将这个方案落地实施:

public void containAndPut(final List<String> keys) {
    // bloomFilterList 包含 4 个布隆过滤器客户端
    bloomFilterList.forEach(bf -> {
        // 尝试批量添加目标设备信息
        List<Boolean> results = bf.addAll(keys);
        for (int i = results.size() - 1; i >= 0; --i)
            // 将成功添加的设备元素从列表中移除
            if (results.get(i))
                keys.remove(i);
    });
}

上述代码为核心逻辑:由于在「背景介绍」中提到的每天 4 条运营类消息推送的限制,我们需要遍历一组共 4 个布隆过滤器,调用 addAll 方法尝试批量添加目标设备 keys,并在每次操作后将被成功添加的设备元素从列表中移除,最后剩下的便是已经达到发送次数上限的设备列表。

奇怪现象

该方案在生产环境运行稳定,但是在监测观察中我们也发现了一个在测试时疏忽了的奇怪现象 —— 在进行大量数据的并发过滤校验时,redis 的内存占用(used_memory_rss)会一度飙升,而后快速回落至预期水平:

内存占用飙升后回落

排除了 bgsave 因素后我们猜测是 redis 在进行布隆过滤操作时占用了大量内存;于是我们仔细分析了 Orestes-Bloomfilter 中相关代码逻辑:

public List<Boolean> addAll(Collection<T> elements) {
    List<Boolean> added = new ArrayList<>();
    // 使用事务模式
    List<Boolean> results = pool.transactionallyDo(p -> {
        //  遍历所有带校验元素
        for (T value : elements) {
            // 进行多次哈希计算
            for (int position : hash(toBytes(value))) {
                // 将 redis 「布隆过滤器」对应位置的值置为 1
                bloom.set(p, position, true);
            }
        }
    });

    // 判断各个元素是否「初次」添加至布隆过滤器
    boolean wasAdded = false;
    int numProcessed = 0;
    for (Boolean item : results) {
        // 若 item 为 0,表示该位置元素先前非 1,判断为初次添加
        if (!item) wasAdded = true;
        // config().hashes() 返回整数 n,表示对某元素进行了 n 次不同的哈希计算
        if ((numProcessed + 1) % config().hashes() == 0) {
            added.add(wasAdded);
            wasAdded = false;
        }
        numProcessed++;
    }
    return added;
}

在消息推送时我们使用上述方法对用户设备进行批量校验;基本思路是对用户设备信息进行多次不同的哈希计算,并尝试将 redis 中比特数组对应位置的值置为 1,只需其中任一位置操作成功便可判定该设备信息先前不存在,否则表示该用户设备已存在布隆过滤器中;程序与 redis 进行交互时使用的具体命令是 SETBIT key offset value,用于对 key 对应的字符串中 offset 位置赋值 value(0或1),并返回该位置先前的值。

继续分析 transactionallyDo 的方法逻辑:

public <T> List<T> transactionallyDo(Consumer<Pipeline> f, String... watch) {
    return (List<T>) safelyReturn(jedis -> {
        Pipeline p = jedis.pipelined();
        if (watch.length != 0) {
            p.watch(watch);
        }
        // 标记 redis 事务开始
        p.multi();
        // 执行调用方(此处为 addAll 方法)指定操作
        f.accept(p);
        // 执行 redis 事务
        Response<List<Object>> exec = p.exec();
        // 获取操作结果
        p.sync();
        return exec.get();
    });
}

可以看到代码中为了保证对大量元素进行布隆过滤操作的性能,与 redis 交互时使用了「管道」 —— 即在单次交互中批量发送操作指令以减少网络 I/O;同时,为了保证并发安全性,每一批次的管道命令操作都在一个「事务」中进行。查阅相关文档,可以发现无论是管道模式还是事务模式都需要消耗 redis 内存:

While the client sends commands using pipelining, the server will be forced to queue the replies, using memory.

To perform a transaction in Redis, we first call MULTI, followed by any sequence of commands we intend to execute, followed by EXEC. When seeing MULTI, Redis will queue up commands from that same connection until it sees an EXEC, at which point Redis will execute the queued commands sequentially without interruption.

在管道模式下,redis 在回应客户端请求前需要缓存所有操作指令对应的结果;在事务模式下,redis 在真正执行该事务前需要缓存相应的操作指令;与此同时,我们的消息推送应用以并发的方式进行分布式布隆过滤 —— 综合考虑这些因素就可以理解为何 redis 进程会出现短暂的内存占用高峰了。

问题解决

虽然在内存占用达到极值(maxmemory)后,redis 仍可以继续使用内存进行操作指令缓存等非数据存储工作,但是出于稳定性考虑我们可能希望对这种情况做一定的限制;由于查明了内存快速上升的具体原因,相应的控制措施也就比较明了了 —— 可以降低进行布隆过滤的并发数或减少了管道模式下批量操作指令数来到达效果。

参考资料

目录
相关文章
|
6月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
469 2
|
6月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
418 6
|
运维 NoSQL 测试技术
Redis:内存陡增100%深度复盘
本文深度分析了Redis内存陡增100%的一些细节和解决方案。
499 1
Redis:内存陡增100%深度复盘
|
7月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
4月前
|
NoSQL 算法 Redis
【Docker】(3)学习Docker中 镜像与容器数据卷、映射关系!手把手带你安装 MySql主从同步 和 Redis三主三从集群!并且进行主从切换与扩容操作,还有分析 哈希分区 等知识点!
Union文件系统(UnionFS)是一种**分层、轻量级并且高性能的文件系统**,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem) Union 文件系统是 Docker 镜像的基础。 镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
636 5
|
5月前
|
存储 缓存 NoSQL
工作 10 年!Redis 内存淘汰策略 LRU 和传统 LRU 差异,还傻傻分不清
小富带你深入解析Redis内存淘汰机制:LRU与LFU算法原理、实现方式及核心区别。揭秘Redis为何采用“近似LRU”,LFU如何解决频率老化问题,并结合实际场景教你如何选择合适策略,提升缓存命中率。
670 3
|
5月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
444 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
5月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
7月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
283 8
|
8月前
|
存储 监控 NoSQL
流量洪峰应对术:Redis持久化策略与内存压测避坑指南
本文深入解析Redis持久化策略与内存优化技巧,涵盖RDB快照机制、AOF重写原理及混合持久化实践。通过实测数据揭示bgsave内存翻倍风险、Hash结构内存节省方案,并提供高并发场景下的主从复制冲突解决策略。结合压测工具链构建与故障恢复演练,总结出生产环境最佳实践清单。
320 9

热门文章

最新文章