Redis八股学习日记:布隆过滤器

如何使用Redis快速实现布隆过滤器?

先介绍一下什么是布隆过滤器:它是一种概率性数据结构,用来判断某个属于是否在集合中。

特点是:存在误判,但判断不在就一定不在

原理:

布隆过滤器由一个位数组和k个独立的哈希函数组成。
添加元素时 ,通过k个哈希函数将元素映射到数组的k个位置上,将这些位置设置为1。
检查元素是否存在时 :同样计算k个位置,乳沟所有位置都是1,说明元素可能存在;只要有一个位置是0,代表一定不存在。

图为假设:某个key通过hash-1和hash-2两个哈希函数,定位到数组中的值都为1,则说明存在。

为什么会出现误判

如果布隆过滤器判断一个元素不存在集合中,那么这个元素一定不在集合中,如果判断元素存在集合中则不定是真的,因为哈希可能会存在冲突。因此布隆过滤器有误判的概率。

我们现在对布隆过滤器有了前置的理解,接下来回归正题,如何在Redis中快速实现布隆过滤器。


在Redis中实现布隆过滤器有两种方式:

  • 位图手动实现
  • 官方的RedisBloom模块

这两种方式的实现方式:

位图方式就是用Redis的SETBIT/GETBIT命令,自己管理哈希函数和位数组。添加元素时,用K个哈希函数算出K个位置,把这些位置都设成1;查询时同样算Κ个位置,只要有一个是0就说明元素肯定不存在,全是1就说明可能存在。

布隆过滤器由一个位数组和k个哈希函数组成。添加元素时,通过K个哈希函数计算出K个位置,将这些位置设为1。查询时计算同样的k个位置,如果全为1则可能存在,只要有一个为0 则一定不存在。误判发生在不同元素的哈希值产生冲突时。

拓展

关于误判率和参数选择

误判率取决于三个因素:位数组大小m,哈希函数个数k,已插入元素数量n。

理论最优哈希函数个数是k=(m/n)*ln2,大约是0.7倍的位数组于元素数量之比。

实际工程中,一般这样估算:

1)要求误判率 1%,每个元素大约需要 10 bit

2)要求误判率 0.1%,每个元素大约需要 15 bit

3)要求误判率 0.01%,每个元素大约需要 20 bit

比如要存 1000 万个元素,误判率控制在 1%,位数组大约需要1 亿 bit,也就是12MB 左右。


布隆过滤器的使用场景

场景特点:数据量大,允许小概率误判,只需要判断存在性。

1)爬虫URL去重:几亿个URL判断有没有爬过,误判了顶多漏爬一个页面

2)垃圾邮件过滤:黑名单地址判断,误判了可能误杀一封正常邮件,但概率可控

3)推荐系统去重:判断用户有没有看过某个内容,误判了顶多少推一条

4)分布式缓存层:HBase、Cassandra 都用布隆过滤器快速判断某个key 在不在某个HFile/SSTable里,减少磁盘10

如何使用Redis快速实现排行榜

答:用Redis的Sorted Set 来实现。因为Sorted Set里每一个成员都绑定一个score分数,Redis回按照score来排序,而且底层用跳表实现,插入,删除,查排名等操作都是O(logN),百万级数据也能抗住。


核心有如下几个操作:

  • ZADDleaderboard 1000user1添加用户和分数,用户已存在就更新分数
  • ZREVRANKleaderboarduser1查某个用户排第几名,从0开始计数
  • ZREVRANGEleaderboard09WITHSCORES 取前10名,连分数一起返回
  • ZINCRBYleaderboard 500user1给用户加分,打游戏升级、签到加积分都用这个
java 复制代码
// 游戏排行榜示例
Jedis jedis = new Jedis("localhost", 6379);

// 玩家打完一局,更新分数
jedis.zadd("game:rank:202601", 8500, "player_10086");

// 查自己排第几
Long rank = jedis.zrevrank("game:rank:202601", "player_10086");
System.out.println("当前排名:" + (rank + 1)); // 排名从0开始,+1显示

// 拉取前100名展示
Set<Tuple> top100 = jedis.zrevrangeWithScores("game:rank:202601", 0, 99);
for (Tuple t : top100) {
    System.out.println(t.getElement() + " : " + t.getScore());
}

拓展

大规模排行榜分片

当用户数据量达到上亿,或者需要支持多维度的排行榜,就得考虑分片,大致可分为三类:

  1. 时间:每天每周每月各一个key,比如rank:daily:20260115、 rank:weekly:202603
  2. 区间:分数0-1000一个key,1000-10000一个key,查总榜时聚合
  3. 用户:对用户ID取模分到不同key,适合不关心全局排名只看附近排名的场景

关于Sorted Set 底层的跳表解释

Sorted Set 底层的跳表是一种多层链表结构,通过建立索引实现类似二分查找的效果,从而让有序数据的插入,查找和范围查找达到O(log n)的效率

Redis为什么这么快

三个核心原因:

  • 纯内存操作
  • 单线程+IO多路复用
  • 高效的数据结构

内存与磁盘

内存较磁盘的范文速度快了1000倍。Redis讲数据全部放在内存中,除了持久化,基本不跟磁盘打交道


单线程+IO多路复用

Redis用单线程执行核心命令,没有了锁竞争,没了上下文切换。网络IO用epoll多路复用,一个线程就能同时盯着几万个socket连接,哪个连接有数据旧处理哪个,不用傻等。


高效的数据结构

Redis的数据结构都是为了快而生。String的底层是SDS,O(1)获取长度;Hash小数据量用ziplist省内存,大数据用hashtable保证O(1)查询;ZSet用调表,插入查询都是O(logN)。


拓展

我们来对上述三个要点进行更加详细的介绍 :

为什么内存这么快

CPU访问内存走的是总线,数据直接从内存条的DRAM芯片读到CPU缓存,整个过程是电信号传输,纳秒级别。

磁盘:机械硬盘需要等磁头寻道,盘片旋转,物理动作旧很慢。SSD虽然没有机械部件,但要经过主控芯片、闪存颗粒的寻址,还有PCle 或 SATA总线的协议开销,怎么都比内存慢一个数量级。

Redis及那个热数据放在内存,冷数据放在磁盘。

IO多路复用原理

传统的阻塞IO,一个线程只能盯一个连接,连接多了必须开多个线程,线程切换开销大。

**IO多路复用是让一个线程同时监听多个文件描述符。**linux下常用的是epoll:

  1. 把监听到的socket注册到epoll实例
  2. 调用epoll_wait阻塞等待
  3. 有时间就绪时,epoll_wait返回就绪的socket列表
  4. 挨个处理这些socket的读写
    Redis 在Linux下用epoll,macOS下用kqueue,Windows下用select。底层实现不一样,但思路都是样的:用事件驱动替代线程轮询。

单线程为什么不是瓶颈

Redis的瓶颈不在CPU,在网络带宽和内存带宽。一条简单的GET/SET命令,CPU执行时间时微妙级,网络返回才是主要的。

单线程的好处:

  • 没有锁,代码简单
  • 没有线程切换,上下文切换一次要几微妙
  • CPU缓存命中率高,数据结构不会被别的线程修改

数据结构的高效设计
SDS :比c语言原生字符串多存了一个len字段,获取长度O(1)还支持二进制安全,能存图片,序列化对象。

ziplist:紧凑的连续内存结构,省内存但查询是O(N)。Hash、List、ZSet在数据量小的时候都用它。

quicklist:Redis 3.2 之后List 的底层实现,把多个ziplist 用双向链表串起来,兼顾内存和性能。

skiplist:ZSet的底层之一,查询、插入、删除都是O(logN),实现比红黑树简单,范围查询更方便。

为什么在6.0版本引入了多线程

在上面我们讲到了Redis是单线程操作的。好处是:没有锁竞争;没有上下文切换的开销。一个线程配合IO多路复用。

单线程的瓶颈不是在CPU,而是在内存和网络IO

我们来看不同版本对线程的更新操作:

4.0 之前:纯单线程,网络IO、命令执行都在主线程

4.0 引入:后台线程处理大 key 删除、AOF 刷盘这些耗时操作

6.0 引入:多线程处理网络1O,主线程只负责执行命令

拓展

6.0多线程的工作原理

总结:

多线程只用于网络IO,命令执行还是单线程

具体流程:

  1. 主线程来接收到客户端连接,把待读的socket分配给IO线程
  2. IO线程并行读取网络数据,解析成Redis命令
  3. 主线程单线程执行所有命令
  4. 执行完毕后,IO线程并行写响应数据
相关推荐
悟纤2 小时前
Seedance 2.0 API 文生视频 |支持100并发 | [灵龙AI API]
数据库·文生视频·seedance 2.0
czlczl200209252 小时前
MySQL 性能优化:前缀索引(Prefix Index)深度解析
数据库·mysql·性能优化
逍遥德2 小时前
SpringBoot数据库连接池HikariCP,Druid,Tomcat JDBC,DBCP2,c3p0配置使用
数据库·spring boot·tomcat
小菜同学爱学习3 小时前
第一章 初识达梦数据库:基础认知与环境准备
数据库·达梦
zhoutongsheng3 小时前
如何解决ORA-01078参数文件错误_pfile与spfile互相创建恢复
jvm·数据库·python
m0_716255003 小时前
批处理一道例题+答案解析+批处理知识点总结 | 批处理高频易错场景 + 正确写法对照表
数据库·oracle
2401_824222693 小时前
HTML怎么标注字数限制提示_HTML实时字数统计占位【详解】
jvm·数据库·python
d111111111d3 小时前
了解Modbus
网络·笔记·stm32·单片机·嵌入式硬件·学习
稀饭过霍3 小时前
数据类型【TINYINT、SMALLINT、INT、BIGINT、decimal(18,2)】表示意思
数据库