目录
(第一炮)一、Redis?常用数据结构?
1. 项目里面到了Redis,为什么选用Redis?
2. Redis 是什么?
3. Redis和关系型数据库的本质区别有哪些?
4. Redis 的线程模型了解吗?
5. Redis 为什么是单线程?真的是单线程的吗?单线程的Redis为什么快?Redis 6.0为何引入多线程?
5.1. 为什么是单线程?
5.2. 真的是单线程的吗?
5.3. 单线程的Redis为什么快?
5.4. Redis 6.0为何引入多线程?
6. 支持哪几种数据类型?
7. Redis的hash是怎么实现的?
8. 为啥redis zset使用跳跃链表而不用红黑树实现?
9. Redis中String的实际应用场景有哪些?
10. Redis中List的实际应用场景有哪些?
11. Redis中Sorted Set的实际应用场景有哪些?
12. Redis中数据结构的高级用法有哪些?
13. Redis中的队列为空怎么解决?
14. 位图bitmap的常用用途有哪些?
15. HyperLogLog的常用用途有哪些?
16. pfadd 这个 pf 是什么意思?
17. 布隆过滤器怎么使用?
18. 为什么使用Redis跳跃表(Skip List)实现有序集合(sorted set)?
19. Redis有哪些适合的场景?
20. Redis主要消耗什么资源的数据?
21. Redis 有哪些功能?
22. Redis 有哪些使用场景?
23. Redis如何设置密码及验证密码?
24. Redis的五个参数是什么?
25. Redis的常用数据类型
26. Redis的常用场景
(第一炮)一、Redis?常用数据结构?
1. 项目里面到了Redis,为什么选用Redis?
因为传统的关系型数据库如Mysql已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等
等,都很容易把数据库打崩,所以引用了缓存中间件,目前市面上比较常用的缓存中间件有Redis 和
Memcached 不过综合和考虑了他们的优缺点,最后选择了Redis。
2. Redis 是什么?
Redis 全称 Remote Dictionary Server。
它是一个 Key-Value 类型的内存数据库/存储系统,它支持存储的value类型相对更多,包括string(字符串)、
list(列表)、set、、(集)、zset(有序集)、hash(哈希)等。
这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性
的。
在此基础上,Redis支持各种不同方式的排序。
为了保证效率,数据都是缓存在内存中,Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录
文件,并且在此基础上实现了master-slave(主从)同步。
它也很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘
上进行保存。
因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的Key-Value
DB。
与此同时,Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限
制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能。
比方说用他的List来做FIFO双向链表,实现一个轻量级的高性能消息队列服务,用他的Set可以做高性能的tag系统
等等。
另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主
要局限在较小数据量的高性能操作和运算上。
3. Redis和关系型数据库的本质区别有哪些?
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,
官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用
去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
使用多路I/O复用模型,非阻塞IO;
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了 VM
机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
4. Redis 的线程模型了解吗?
Redis 内部使用文件事件处理器 file event handler ,这个文件事件处理器是单线程的,所以 Redis才叫做单线程
的模型。
它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 Socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个
Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的
事件处理器进行处理
再次介绍:Redis线程模型
Redis的线程模型包括Redis 6.0之前和Redis 6.0。 下面介绍的是Redis 6.0之前。
Redis 是基于 Reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于
这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个
Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。
IO多路复用是 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计 的。多路指的是多
个 Socket 连接,复用指的是复用一个线程。多路复用主要有三种技术: Select,Poll,Epoll。
Epoll 是最新的也是目前最好的多路复用技术。
模型如下图:
文件事件处理器的结构包含了四个部分
- 多个 Socket
Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件:
当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个AE_READABLE 事件,当
socket 变得可写时,socket 就会产生一个 AE_WRITABLE 事件。
- IO 多路复用程序
- 文件事件分派器
- 事件处理器
包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器 对应不同的 socket 事件:
如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器,如果是客户端要写数据到 Redis(读、写请求
命令),那么会为 socket 关联命令请求处理器,如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复
处理器。
多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket, 当这些
Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、 同步、每次一个
事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程 序才会继续向文件分派器
传送下一个事件。
下图是客户端与 Redis 通信的一次完整的流程:
(1)Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。
(2)如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初
AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处 理器会与客户
端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件 与命令请求处理器关联起
来。
(3)如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,
IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事 件,由于该 socket 的
AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该 事件交给命令请求处理器处理,命令
请求处理器读取事件中的命令并完成。操作完成后,Redis 会 将该 socket 的 AE_WRITABLE 事件与命令回复处理
器关联。
(4)如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会 压入队
列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响 应数据写入 socket
中,供客户端读取。
(5)命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。
5. Redis 为什么是单线程?真的是单线程的吗?单线程的Redis为什么快?Redis 6.0为何引入多线程?
5.1. 为什么是单线程?
- 代码更清晰,处理逻辑更简单;
- 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
- 不存在多线程切换而消耗CPU;
- 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;
- 缺点:无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善
5.2. 真的是单线程的吗?
Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;
redis内部使用了基于epoll的多路复用,也可以多部署几个redis 服务器解决单线程的问题;
redis主要的性能瓶颈是内存 & 网络;
内存好说,加内存条就行了,而网络才是大麻烦,所以redis6内存好说,加内存条就行了;
而网络才是大麻烦,所以redis6.0引入了多线程的概念,
redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。
5.3. 单线程的Redis为什么快?
Redis是单线程的,但它采用了异步非阻塞的I/O模型和内存管理机制,因此具有很高的性能和响应速度。
具体来说,Redis采用了以下几种技术来提高性能:
- 基于内存操作Redis数据存储在内存中,内存的读写速度比磁盘快很多,因此Redis可以快速地处理数据读写
请求。
- 异步非阻塞的I/O模型Redis采用事件驱动模型,通过单线程的事件循环机制,可以高效地处理大量的并发请
求。
- 单线程避免了线程切换和锁竞争的开销,同时可以避免多线程编程中的数据同步和死锁等问题,从而减少了
系统的复杂度和出错的可能性。
- Redis采用了多种数据结构和算法来优化数据操作,如基数过滤器、跳跃表、压缩列表等,可以在保证数据结
构正确性的前提下提高读写性能。
综合以上几点,单线程的Redis可以充分利用内存和CPU资源,同时通过异步非阻塞的I/O模型和内存管理机制,
可以实现非常高效的数据读写和处理。
换种说法:Redis单线程为什么快?
Redis之所以能够快速地处理请求,主要是因为它采用了单线程的方式处理请求。单线程的优势在于避免了线程切
换的开销和锁竞争的问题,从而提高了Redis的性能。
具体来说,Redis的单线程模型有以下几个优点:
- 单线程操作,避免了线程切换的开销线程切换是非常耗费CPU资源的,因为需要保存和恢复线程的上下文信
息。Redis采用单线程的方式处理请求,避免了线程切换的开销,从而提高了处理请求的效率。
- 避免了锁竞争的问题在多线程环境下,如果多个线程同时访问同一个数据结构,就会产生锁竞争的问题。为
了避免锁竞争,需要使用锁机制来保护共享数据结构,但是锁机制会降低程序的并发性。Redis采用单线程的
方式处理请求,避免了锁竞争的问题,从而提高了程序的并发性。
- 纯内存操作,优化了CPU缓存在多线程环境下,多个线程访问同一个数据结构时,会导致CPU缓存的失效,
从而降低程序的性能。Redis采用单线程的方式处理请求,可以优化CPU缓存,提高程序的执行效率。
- 避免了上下文切换的开销在多线程环境下,线程之间的切换需要保存和恢复线程的上下文信息,这个过程会
消耗大量的CPU资源。Redis采用单线程的方式处理请求,避免了上下文切换的开销,提高了程序的执行效
率。
- 采⽤了⾮阻塞I/O多路复用机制
总的来说,Redis采用单线程的方式处理请求,可以避免线程切换、锁竞争和CPU缓存失效等问题,从而提高了程
序的执行效率。
但是,在某些高并发的场景下,单线程模型可能会成为Redis的瓶颈,这时可以通过利用多核CPU和分布式集群等
方式来提高Redis的性能。
5.4. Redis 6.0为何引入多线程?
很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。 随着互联网的飞速发展,互联网业务系统所要处理的线
上流量越来越大,Redis的单线程模式会导致系统消耗很多时间在网络 IO 上,从而降低吞吐量。
要提升 Redis的性能有两个方向:
- 优化网络 I/O 模块
- 提高机器内存读写的速度
后者依赖于硬件的发展,暂时无解。
所以只能从前者下手,网络 I/O 的优化又可以分为两个方向: 零拷贝技术或者 DPDK 技术利用多核优势。
零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和
Linux 零拷贝技术。
而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
总结起来,Redis支持多线程主要就是两个原因:
- 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
- 多线程任务可以分摊 Redis 同步 IO 读写负荷
Redis作者Antirez在RedisConf 2019分享时曾提到:Redis 6 引入的多线程IO特性对性能提升至少是一倍以上。
6. 支持哪几种数据类型?
Redis支持五种数据类型
String、hash、list、set、zset
1. String字符串
字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,⽽且 其他几种数据结构都是在字符串类型基础
上构建的,我们常使⽤的 set key value 命令就是字符串。常⽤在缓存、计数、共享Session、限速等。
2. Hash哈希
在Redis中,哈希类型是指键值本⾝⼜是⼀个键值对结构,哈希可以⽤来存放用户信息,比如实现购物⻋。
3. List列表(双向链表)
列表(list)类型是⽤来存储多个有序的字符串。可以做简单的消息队列的功能。
4. Set集合
集合(set)类型也是⽤来保存多个的字符串元素,但和列表类型不⼀ 样的是,集合中不允许有重复元素,并且集
合中的元素是⽆序的,不能通过索引下标获取元素。利⽤ Set 的交集、并集、差集等操作,可以计算共同喜好,
全部的喜好,⾃⼰独有的喜好等功能。
5. Sorted Set有序集合(跳表实现)
Sorted Set 多了⼀个权重参数 Score,集合中的元素能够按Score 进⾏排列。
可以做排⾏榜应⽤,取 TOP N 操作。
如果是Redis高级用户,还需要加上下面几种数据结构:HyperLogLog、Geo、Pub/Sub。
如果还想加分,那你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML
7. Redis的hash是怎么实现的?
Redis的hash数据结构是使用哈希表来实现的,其中每个键值对都被存储在哈希表中的一个桶(bucket)中。
每个桶包含一个链表,链表中存储着哈希冲突的键值对,这些键值对的键经过哈希运算后得到的哈希值相同。
在Redis中,每个哈希表都有一个初始大小,当哈希表中的键值对数量超过某个阈值时,Redis会自动对哈希表进
行rehash,即扩大哈希表的大小,以便能够容纳更多的键值对。
在rehash过程中,Redis会创建一个新的哈希表,并将旧哈希表中的键值对逐个移动到新哈希表中,完成后再将新
哈希表作为当前哈希表使用。
通过使用哈希表来实现hash数据结构,Redis能够在O(1)的时间复杂度内实现插入、查找和删除操作,使得Redis
的hash数据结构在存储大量键值对时表现出色。
8. 为啥redis zset使用跳跃链表而不用红黑树实现?
Redis之所以使用跳跃表skiplist而不是红黑树,主要是因为跳跃表的实现比红黑树要简单,而且对于有序集合这个
数据结构来说,跳跃表的效率和红黑树相当,甚至在一些情况下,跳跃表的效率还会更高一些。
此外,跳跃表在插入和删除操作时,不需要像红黑树那样进行频繁的旋转操作,因此,跳跃表的实现也更加容易
维护和扩展。
在并发环境下红黑树在插⼊和删除时需要rebalance,性能不如跳表
总之,Redis选择跳跃表作为有序集合的底层实现,主要是为了保证数据结构的高效性和简单性。
9. Redis中String的实际应用场景有哪些?
String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。
SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。
这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。
但是真实的开发环境中,很多仔可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的人他就喜欢
把对象或者List转换为JSONString进行存储,拿出来再反序列话什么的
String的实际应用场景比较广泛的有:
- 缓存功能String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作
为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降
低后端数据库的压力。
- 计数器许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结
果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
- 共享用户Session用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面
- 缓存Cookie但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用
户Session的更新和获取都可以快速完成。大大提高效率。
10. Redis中List的实际应用场景有哪些?
List 是有序列表,
比如可通过 List 存储列表型的数据结构,类似粉丝列表、文章的评论列表之类等
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一
个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一
页一页走。
比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。
List本身就是我们在开发过程中比较常用的数据结构了,热点数据更不用说了。
消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。
比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞
的“抢”列表尾部的数据。文章列表或者数据分页展示的应用。
比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文
章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以
完美解决分页查询功能。大大提高查询效率
11. Redis中Sorted Set的实际应用场景有哪些?
Redis中Sorted Set的实际应用场景有哪些
Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。
有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间
的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted
set数据结构作为选择方案。
排行榜:有序集合经典使用场景。例如视频⽹站需要对用户上传的视频做排行榜,榜单维护可能是
多方面:按照时间、按照播放量、按照获得的赞数等。
用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线
程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
微博热搜榜,就是有个后面的热度值,前面就是名称
12. Redis中数据结构的高级用法有哪些?
- Bitmap 位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter);
- HyperLogLog供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;
- Geospatial可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?这三个其实也可以算作一种数据结构,你如果只知道五种基础类型那只能拿60分,如果你能讲出高级用法,那就觉得你有点东西。
- pub/sub功能是订阅发布功能,可以用作简单的消息队列。
- Pipeline可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
- LuaRedis 支持提交 Lua 脚本来执行一系列的功能。秒杀场景经常使用这个,利用他的原子性
13. Redis中的队列为空怎么解决?
客户端是通过队列的 pop 操作来获取消息,然后进行处理,处理完了再接着获取消息,再进行处理。
如此循环往复,这便是作为队列消费者的客户端的⽣命周期。
可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据,这就是
浪费⽣命的空轮询。
空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢
查询可能会显著增多。
解决方式很简单,让线程睡一秒 Thread.sleep(1000)
14. 位图bitmap的常用用途有哪些?
我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是0,要记录
365 天。
如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365个位,46
个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直
接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
当我们要统计月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。这时就可
以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活
跃。然后到月底遍历一次位图就可以得到月度活跃用户数。
这个类型不仅仅可以用来让我们改二进制改字符串值,最经典的就是用户连续签到。
key 可以设置为 前缀:用户id:年月 譬如
setbit sign:123:1909 0 1
代表用户ID=123签到,签到的时间是19年9月份,0代表该月第一天,1代表签到了第二天没有签到,无需处理,
系统默认为0
第三天签到 setbit sign:123:1909 2 1 可以查看一下目前的签到情况,显示第一天和第三天签到了,前8天目前共
签到了2天
127.0.0.1:6379> setbit sign:123:1909 0 1
0
127.0.0.1:6379> setbit sign:123:1909 2 1
0
127.0.0.1:6379> getbit sign:123:1909 0
1
127.0.0.1:6379> getbit sign:123:1909 1
0
127.0.0.1:6379> getbit sign:123:1909 2
1
127.0.0.1:6379> getbit sign:123:1909 3 0
127.0.0.1:6379> bitcount sign:123:1909 0 0 2
15. HyperLogLog的常用用途有哪些?
如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日
期。
这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。
但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。
这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。
你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用
户 ID。
当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字
就是这个页面的 UV 数据。
没错,这是一个非常简单的方案。
但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非
常浪费空间。
如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?
其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有
更好的解决方案呢?
HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计
数。
pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是,pfcount 和 scard用法是一
样的,直接获取计数值。
127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 1
127.0.0.1:6379> pfadd codehole user2
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 2
127.0.0.1:6379> pfadd codehole user3
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 3
127.0.0.1:6379> pfadd codehole user4
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 4
16. pfadd 这个 pf 是什么意思?
pfadd 这个 pf 是什么意思
它是 HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的⾸字⺟缩写,老师觉得他发型很酷,看起来是个
佛系教授。
他底层有点复杂,他是怎么做到这么⼩的结构,存储这么多数据的?
也是很取巧大家有空可以看下我之前的⽂章。
布隆过滤器
HyperLogLog 数据结构来进⾏估数,它非常有价值,可以解决很多精确度不⾼的统计需求。
但是如果我们想知道某一个值是不是已经在
HyperLogLog 结构⾥面了,它就无能为⼒了,它只提供了pfadd 和 pfcount 方法,没有提供 pfcontains 这种方
法。讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去
重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10
17. 布隆过滤器怎么使用?
HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。
但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了pfadd 和
pfcount 方法,没有提供 pfcontains 这种方法。
讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,
去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10
你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,
过滤掉那些已经存在的记录。
问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上
么?
127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
布隆过滤器的initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽
可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。
布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也
无伤大雅。
比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点
误判率就带来巨大的改变。
在爬主系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 URL 太多了,几千万几个
亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。
它可以大幅降低去重存储消耗,只不过也会使得爬主系统错过少量的页面。
布隆过滤器在 NoSQL 数据库领域使用非常⼴泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB
内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先
通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。邮箱系统的垃圾邮件过滤功能
也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,
这个就是误判所致,概率很低。他其实还有很多用法,我没怎么讲,比如限流,附近的人GeoHash等等,我们公
司的附近的人也是用它实现的
18. 为什么使用Redis跳跃表(Skip List)实现有序集合(sorted set)?
Redis跳跃表(Skip List)是一种有序数据结构,它可以用来实现有序集合(sorted set)等数据类型。
它是通过在链表中添加多级索引来实现快速查找的。跳跃表由多个层级组成,每一层级都是一个有序的链表,其
中第一层级就是原始的链表。
在每一层级中,每个节点都可能会有一个指向下一层级的指针,这些指针可以让我们在跳跃表中快速地查找节
点。
这些指针的添加是通过一定概率随机生成的,因此跳跃表的高度是随机的。
在跳跃表中,查找一个节点的复杂度是O(log n),这个复杂度与平衡树的复杂度相当。
但是,跳跃表的实现比平衡树更加简单,因此更加高效。
19. Redis有哪些适合的场景?
- 会话缓存(Session Cache)
最常用的一种使用Redis的情景是会话缓存(session cache)。
用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。
当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现
在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。
甚至广为人知的商业平台Magento也提供Redis的插件。
- 全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了
Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地
FPC。
再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。
此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度
加载你曾浏览过的页面。
- 队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台
来使用。
Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利
用Redis创建非常好的后端工具,以满足各种队列需求。
例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。
- 排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。
集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提
供了这两种数据结构。
所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面
一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这
里看到。
- 发布/订阅
最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。
我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能
来建立聊天系统!
20. Redis主要消耗什么资源的数据?
前面我们已经知道Redis是一个高性能键值对数据库,使用内存作为数据存储介质。由于其出色的性能和可靠性,
Redis被广泛应用于Web应用程序,消息队列,数据缓存和内容分发网络等领域。
在使用过程中,我们就需要了解Redis的资源消耗情况,这有助于我们更合理地使用Redis。
Redis主要消耗的资源是内存和CPU。
Redis使用内存作为数据存储介质,因此占用较多内存是不可避免的。
随着数据量的增长,内存占用也会呈现出逐渐增加的趋势。
因此,合理设置内存大小是非常必要的。
与此同时,Redis也会使用CPU帮助处理客户端请求,并执行一些后台任务。
当客户端请求数量增加或后台任务增多时,CPU的占用率也会增加。
为了优化Redis的资源消耗,我们可以从以下几个方面进行改进:
- 合理设置内存大小
合理的内存大小既不能导致Redis缺乏足够的内存处理请求,也不能浪费过多的内存资源。
因此,需要根据实际情况
- 选择合适的内存大小
控制客户端连接数。
客户端数量过多会对CPU和内存资源带来很大压力,因此我们需要控制客户端连接数。
这可以通过限制客户端并发连接数、设置客户端连接超时时间等方式来实现。
- 定期清理过期数据
Redis支持设置过期时间,过期数据会被自动清理。
为了避免过期数据占用过多内存,我们需要定期清理过期数据。
- 使用Redis集群
Redis集群可以通过多节点的方式分散内存和CPU资源,减少对单节点的资源消耗。
21. Redis 有哪些功能?
Redis的主要功能有如下:
- 基于本机内存的缓存
当调用api访问数据库时,假如此过程需要2秒,如果每次请求都要访问数据库,那将对服务器造成巨大的压
力,如果将此sql的查询结果存到Redis中,再次请求时,直接从Redis中取得,而不是访问数据库,效率将得
到巨大的提升,Redis可以定时去更新数据(比如1分钟)。
- 持久化机制
如果电脑重启,写入内存的数据是不是就失效了呢,这时Redis还提供了持久化的功能。
- 哨兵(Sentinel)和复制
Sentinel可以管理多个Redis服务器,它提供了监控、提醒以及自动的故障转移功能;复制则是让Redis服务器
可以配备备份的服务器;Redis也是通过这两个功能保证Redis的高可用;
- 集群(Cluster)
单台服务器资源总是有上限的,CPU和IO资源可以通过主从复制,进行读写分离,把一部分CPU和IO的压力
转移到从服务器上,但是内存资源怎么办,主从模式只是数据的备份,并不能扩充内存;现在我们可以横向
扩展,让每台服务器只负责一部分任务,然后将这些服务器构成一个整体,对外界来说,这一组服务器就像
是集群一样。
22. Redis 有哪些使用场景?
- Redis是基于内存的 NoSQL 数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作
- 通过list取最新的N条数据
- 模拟类似于token这种需要设置过期时间的场景
- 发布订阅消息系统
- 定时器、计数器
23. Redis如何设置密码及验证密码?
设置密码:config set requirepass 123456
授权密码:auth 123456
24. Redis的五个参数是什么?
Redis的五个参数是:
- host
Redis服务器的IP地址或主机名。
- port
Redis服务器的端口号。
- password
Redis服务器的密码(如果有的话)。
- db
Redis服务器的数据库编号。
- decode_responses
设置返回值是否为字符串类型(默认为False,返回字节类型)
25. Redis的常用数据类型
有五种常用数据类型:String、List、Hash、Set、Zset
- String
是最常用的一种数据类型,普通的key- value 存储都可以归为此类。
其中Value既可以是数字也可以是字符串。
使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。
- Hash
是一个键值(key => value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的 映射表,hash 特别适合用于存储对象,并且可以像数据
库中update一个属性一样只修改某一项属性值。
- Set
是一个无序的天然去重的集合,即Key-Set。
此外还提供了交集、并集等一系列直接操作集 合的方法,
对于求共同好友、共同关注什么的功能实现特别方便。
- List
是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,
因此支持正向、反向双重查找。
通过List,我们可以很方面的获得类似于最新回复这类的功能实现。
- SortedSet
类似于java中的TreeSet,是Set的可排序版。
此外还支持优先级排序,维护了一个score 的参数来实现。
适用于排行榜和带权重的消息队列等场景。
左边是 Redis 3.0版本的,也就是《Redis 设计与实现》
可以看到,Redis 数据类型的底层数据结构随着版本的更新也有所不同,比如:
在 Redis 3.0 版本中 List 对象的底层数据结构由「双向链表」或「压缩表列表」实现,但是在 3.2 版本之后,List
数据类型底层数据结构是由 quicklist 实现的;在最新的 Redis 代码中,压缩列表数据结构已经废弃了,交由
listpack 数据结构来实现了。
26. Redis的常用场景
- 缓存
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降
低数据库的压力。
Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓 存的场合非常多。
- 排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。
Redis提供的有序集 合数据类构能实现各种复杂的排行榜应用。
- 计数器
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。
为了保证数据实时效,每次浏览 都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。
Redis提供的incr命令来实现计 数器功能,内存操作,性能非常好,非常适用于这些计数场景。
- 分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系
统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由 session
服务及内存数据库管理。
- 分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如 全局
ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场
合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。
可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用
中要考虑的细节要更多。
- 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且 传统
的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实 现这些功
能。如在微博中的共同好友,通过Redis的set能够很方便得出。
- 最新列表
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这 样
列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
- 消息队列
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用 于业
务解耦、流量削峰及异步处理实时性低的业务。
Redis提供了发布/订阅及阻塞队列功能,能实现一 个简单的消息队列系统。
另外,这个不能和专业的消息中间件相比。