目录
目录
目录
5. Redis 为什么是单线程?真的是单线程的吗?单线程的Redis为什么快?Redis 6.0为何引入多线程?
8. 为啥redis zset使用跳跃链表而不用红黑树实现?
11. Redis中Sorted Set的实际应用场景有哪些?
18. 为什么使用Redis跳跃表(Skip List)实现有序集合(sorted set)?
2. Redis支持的 java 客户端都有哪些?有什么区别?官网推荐使用哪一个?
3. Redis和Redisson有什么关系?对比有什么优缺点?
(第一炮)一、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提供了发布/订阅及阻塞队列功能,能实现一 个简单的消息队列系统。
另外,这个不能和专业的消息中间件相比。
(第二炮)二、Redis客户端问题?
1. Redis 官方为什么不提供 Windows 版本?
因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。
2. Redis支持的 java 客户端都有哪些?有什么区别?官网推荐使用哪一个?
Redis 支持的 java 客户端如下:
- Jedis
Jedis是Redis官方推荐的Java客户端之一,提供了完整的Redis命令操作和功能支持。
它是一个轻量级、易于使用的库,具有良好的性能和稳定性。
- Lettuce
Lettuce是另一个流行的Redis Java客户端,与Jedis相比,它基于Netty框架实现,提供了异步和响应式的编
程模型。Lettuce支持Redis的高级功能,如集群、哨兵和Redis Streams。
- Redisson
Redisson是一个功能丰富的Redis Java客户端和分布式对象框架。
除了基本的Redis操作外,Redisson还提供了分布式锁、分布式集合、分布式对象等功能,使得在Java应用中
使用Redis更加方便。
也可以这样描述:Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些
Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map,
ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore,
Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。
- Jedisson
Jedisson是另一个基于Jedis开发的Redis Java客户端,提供了对Redis的基本操作以及一些额外的功能,如连
接池管理、对象映射等。
- RedisTemplate
RedisTemplate是Spring Framework提供的一个Redis客户端,封装了对Redis的常见操作和功能。
它与Spring集成紧密,可以与Spring的事务管理和缓存机制无缝配合使用。
除了以上列举的客户端库,还有其他一些Redis的Java客户端可供选择,
如JedisCluster、JRediSearch、Redis-Java-Client等。
每个客户端库都有其特点和使用方式,可以根据具体的需求和偏好选择适合的客户端。
各 Redis 支持的 java 客户端区别如下:
- jedis 和 redisson
Jedis 和 Redisson 都是Java中对 Redis 操作的封装。
Jedis 只是简单的封装了 Redis 的 API 库,可以看作是 Redis 客户端,它的方法和 Redis 的命令很类似。
Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更加大。
但 Jedis 相比于 Redisson 更原生一些,更灵活。
得出结论,看出它们的优缺点:Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支
持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不
支持排序、事务、管道、分区等Redis特性。
Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
官网推荐的 Redis 客户端如下:
- Redisson
3. Redis和Redisson有什么关系?对比有什么优缺点?
Redis和Redisson有什么关系?
Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom
filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap,
Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong,
CountDownLatch, Publish / Subscribe, HyperLogLog)。
4. 通信协议
RESP(Redis Serialization Protocol),一种直观的文本协议,Redis 协议将传输的结构数据分为5种最小单元类
型,单元结束时统一加上回车换行符号\r\n。
- 单行字符串 以 + 符号开头。
- 多行字符串 以 $ 符号开头,后跟字符串长度。
- 整数值 以 : 符号开头,后跟整数的字符串形式。
- 错误消息 以 - 符号开头。
- 数组 以 * 号开头,后跟数组的长度。
客户端 -> 服务器:客户端向服务器发送的指令只有一种格式,多行字符串数组。
set author codehole *3\r\n3\r\nset\r\n6\r\nauthor\r\n$8\r\ncodehole\r\n
(第三炮)三、淘汰策略 & 定期删除 & 过期删除
1. Redis有哪几种数据淘汰策略?
背景:
Redis 是个基于内存的缓存数据库,既然是基于内存的,那肯定就会有存满的时候,再有新的数据就存不
进去了。
此时 Redis 会执行已经定义好的一些淘汰策略,本文大概讲一下 Redis 的 8 种数据淘汰策略。
8 种数据淘汰策略:
讲解内存淘汰策略前,先来一个简单理解
Redis的过期策略,是有定期删除+惰性删除两种。
定期好理解,默认100s就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了
Redis的淘汰策略是指在内存空间不足时,Redis选择哪些数据可以被清除出内存,以便为新的数据腾出
空间。
当容量超过100时,开始执行LRU策略:将最近最少未使用的 TimeoutInfoHolder 对象 evict 掉
当 Redis 达到最大内存限制时,Redis会确切地使用配置好的最大内存策略指令来执行。
相关策略如下:
- noeviction(默认策略)
不会删除任何数据,拒绝所有写入操作并返回客户端错误消息(error)OOM command not
allowed when used memory,此时 Redis 只响应删和读操作;
- allkeys-lru
从所有 key 中使用 LRU 算法进行淘汰(LRU 算法:最近最少使用算法);
- allkeys-lfu
从所有 key 中使用 LFU 算法进行淘汰
(LFU 算法:最不常用算法,根据使用频率计算,4.0 版本新增);
- volatile-lru
从设置了过期时间的 key 中使用 LRU 算法进行淘汰;
- volatile-lfu
从设置了过期时间的 key 中使用 LFU 算法进行淘汰;
- allkeys-random
从所有 key 中随机淘汰数据;
- volatile-random
从设置了过期时间的 key 中随机淘汰数据;
- volatile-ttl
在设置了过期时间的key中,淘汰过期时间剩余最短的。
注意:
当使用 volatile-lru、volatile-lfu、volatile-random、volatile-ttl 这四种淘汰策略时,
如果没有 key 可以淘汰,则和 neoviction 一样返回错误。
那么我们该如何选择使用哪种淘汰策略?
根据应用程序的访问模式,选择正确的淘汰策略很重要,但是你可以在程序运行时重新配置策略,并使
用 Redis 的 info 命令 输出来监控缓存未命中和命中的数量,以调整设置。
- keyspace_hits:缓存命中的次数
- keyspace_misses:没有命中的次数
- 缓存命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)
一般根据经验来说:
- 使用 allkeys-lru 策略场景
-
- 当你期望元素的子集将比其他元素更频繁地被访问时,比如幂律分布,20%的数据占有80%的使用次数;
- 当你不确定使用哪种策略时。
- 使用 allkeys-random 策略场景
-
- 当你有一个循环访问,其中所有 key 进行会被连续地访问;
- 当你希望所有 key 的分布比较均匀。
- 使用 volatile-ttl 策略场景
-
- 当你大部分缓存都设有不同的 ttl 值,向 Redis 提供过期候选的提示时。
注意:
给 key 设置过期时间会占用内存,因此使用 allkeys-lru 这样的策略更节省内存,因为在内存压力下不需
要过期配置就可以收回密钥。
其中,volatile-lru、volatile-ttl和volatile-random是针对带有过期时间的key的淘汰策略,而allkeys-
lru和allkeys-random则是针对全部数据集的淘汰策略。
还需要注意的是,Redis并不保证所有符合淘汰条件的数据都会被清除出内存,因为在清除过程中可能会
发生一些问题(比如数据正在被使用等),这时Redis会放弃清除该数据,并尝试清除下一个符合条件的
数据。因此,淘汰策略只是一种参考,具体清除哪些数据还需要根据实际情况来判断。
那么我们该如何查询、设置内存淘汰策略?
1:查询当前内存淘汰策略
127.0.0.1:6379> config get maxmemory-policy
2:获取Redis能使用的最大内存大小
127.0.0.1> config get maxmemory
说明:
- 如果不设置最大内存大小或设置最大内存大小为 0,
- 在 64 为操作系统下不设置内存大小,
- 在 32 为操作系统下最多使用 3GB 内存。32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器默认限制最大 3GB 的可用内存。
3:设置淘汰策略
方法一:通过配置文件设置淘汰策略(修改 redis.conf 文件):
maxmemory-policy allkeys-lru
方法二:通过命令修改淘汰策略(临时生效,重启恢复):
127.0.0.1:6379> config set maxmemory-policy allkeys-lru
4:设置 Redis 最大占用内存大小
设置 Redis 最大占用内存大小为 100M(临时生效,重启恢复)
127.0.0.1:6379> config set maxmemory 100mb
2. Redis 的数据过期策略是什么?
Redis的数据过期策略可以通过两种方式来实现:
- 定时过期
Redis提供了过期时间的设置,可以通过EXPIRE或者EXPIREAT命令来设置键值对的过期时间。
Redis会在键值对过期时自动将其删除。
- 惰性过期
当客户端尝试访问某个键值对时,Redis会检查该键值对是否已经过期,如果已经过期,则会将其删
除。这种过期策略可以减少Redis的内存占用,因为只有在需要访问某个键值对时才会检查其是否过
期。另外,Redis还提供了一种被动过期的策略,即当内存使用达到一定阈值时,Redis会优先删除
过期键值对来释放内存空间。
需要注意的是,Redis的数据过期并不是实时的,而是通过定时和惰性过期策略来实现的。
因此,如果需要实时删除过期数据,可以考虑使用Redis的发布/订阅模式和Lua脚本来实现。
3. 定期删除和惰性删除的区别和优缺点是什么?
定期删除和惰性删除是两种常见的缓存淘汰策略,它们的区别和优缺点如下:
- 定期删除
定期删除是指在缓存中设置一个定时器,每隔一段时间就扫描一次缓存中的数据,将其中过期的数
据进行删除。定期删除的优点是实现简单,对于一些访问频率比较低的数据来说比较适用。缺点是
可能会出现缓存清理不及时的情况,因为定期删除是按照时间来清理缓存的,如果在清理时间点之
前访问了一个过期的数据,那么这个数据就会一直存在于缓存中,直到下一次定期删除操作执行时
才会被清理。
- 惰性删除
惰性删除是指在获取缓存数据的同时判断其是否过期,如果过期了就将其删除。惰性删除的优点是
可以保证过期数据的及时删除,因为每次访问缓存的时候都会进行过期判断;缺点是实现比较复
杂,因为需要在每次访问缓存时都进行过期判断,这会对系统性能产生一定的影响。
综合来看,定期删除适用于一些不需要严格控制数据过期时间的场景,惰性删除则适用于需要保证数据
及时删除的场景。在实际应用中,也可以将两种策略结合使用,以达到更好的效果。
4. Redis的LRU具体实现是怎样的?
Redis的LRU(Least Recently Used,最近最少使用)是通过维护一个链表来实的。
链表的头部是最老的素,尾部是最新的元素。
一个新元素插入时,如果缓存已满,则将头部的元素删除;
当一个元素被访问时,将它移动到链表尾部。
具体实现如下:
- 维护一个双向链表,链表头表示最老的元素,链表尾表示最新的元素。
- 每个节点包括三个属性:key(元素的键值)、value(元素的值)和指向前一个节点的指针 prev,
以及指向后一个节点的指针 next。
- 当一个新元素被插入时,先检查链表是否已满。如果已满,则将链表头部的元素删除。
- 如果链表未满,则将新元素插入链表尾部。
- 当一个元素被访问时,将它移动到链表尾部。具体实现是先将该节点从链表中删除,然后将它插入
到链表尾部。
- 当需要删除一个元素时,直接删除链表头部的元素即可。
通过这种方式,Redis的LRU算法可以保证缓存中的数据最近被使用的元素总是被保留下来,而最老的元
素总是被删除掉。
5. 过期key删除策略
Redis过期key删除策略为:定期删除和惰性删除两种策略配合使用。
定期删除
redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的
key。Redis默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简
单的贪心策略。
1、从过期字典中随机20个key;
2、删除这20个key中已经过期的key;
3、如果过期的key比率超过1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认
不会超过25ms。
所以业务开发人员一定要注意过期时间,如果有大批量的key过期,要给过期时间设置一个随机范围,而
不能全部在同一时间过期,否则
客户端请求会等待很久。(与读写请求共用一个线程)
惰性删除
除了定期遍历之外,它还会使用惰性策略来删除过期的key,在客户端访问这个key的时候,redis对key
的过期时间进行检查,如果过期了就立即删除。定期删除是集中处理,惰性删除是零散处理。
三种删除策略概述:
惰性删除
不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回
null 给客户端,如果没有过期则返回正常信息给客户端。
优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进
行过期检查。
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果
数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存
泄漏。
定期删除
周期性的随机检查一批设置了过期时间的key并进行处理,检查到的已过期的key将被删除。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有
效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一
样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。 另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就
会返回这个键的值,这是业务不能忍受的错误。
定时删除
在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其
进行删除的操作。
优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。
缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应
时间和吞吐量造成影响。
从库的过期策略(包括持久化文件)
从库不会进行过期扫描,从库对过期的处理是被动的。主库在key到期时,会显示地向所有从服务器发送
一条del指令,从库通过执行这条del指令来删除过期的key。
因为指令同步是异步进行的,所以主库过期的key的del指令没有及时同步到从库的话,会出现主从数据
的不一致,主库没有的数据在从库里还存在。
AOF:
当过期key被惰性删除或定期删除后,程序会向AOF文件追加(append)一条del命令,来显式地记录该
key已被删除。
RDB:
- 生成 ———— key检查,过期key不保存到RDB
- 载入(主)———— key检查,过期key不载入
- 载入(从)———— 不检查,全部载入(不影响)
6. 内存淘汰策略
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap)。交换会让Redis的
性能急剧下降,在生产环境中是不允许Redis出现交换行为的,为了限制最大使用内存,Redis提供了配
置参数maxmemory,当实际内存超出maxmemory时,Redis提供了几种可选策略来让用户自己决
定。
Redis.conf对应的配置项是maxmemory-policy
- noeviction:不会继续服务写请求(DEL请求可以继续服务),读请求可以继续进行。这样可以保证不
会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
- volatile-lru:利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key,这个是最常
用的)
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
(LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到)
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
内存淘汰策略可以通过配置文件来修改, 修改对应的值就行,默认是noeviction。
如果你只是拿Redis做缓存,那应该使用allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同
时使用Redis的持久化功能,那就使用volatile-xxx策略,这样可以保留没有设置过期时间的key,它们是
永久的key不会被LRU算法淘汰。
7. 近似LRU算法
Redis使用的是一种近似LRU算法,无需消耗大量额外内存,在现有数据结构的基础上使用随机采样法来
淘汰元素,能达到和LRU算法非常近似的效果。给每个key增加了一个额外的小字段,就是最后一次被访
问的时间戳。
处理key过期方式分为集中处理和懒惰处理,LRU( maxmemory-policy)的处理方式只有懒惰处理。
当Redis执行写操作时,发现内存超出maxmemory,就会执行一次LRU淘汰算法。
这个算法也很简单,就是随机采样出5(可以配置)个key,然后淘汰掉最旧的key,如果淘汰后内存还是超
出maxmemory,那就继续随机采样淘汰,直到内存低于maxmemory为止。
如何采样就是看maxmemory-policy的配置,如果是allkeys就是从所有的key字典中随机,如果是
volatile就从带过期时间的key字典中随机。
每次采样多少个key看的是maxmemory-samples的配置,默认为5。
另外,Redis在算法中增加了淘汰池,进一步提升了近似LRU算法的效果。
淘汰池是一个数组,它的大小是maxmemory-samples,在每一次淘汰循环中,新随机出来的key列表
会和淘汰池中的key列表进行融合,淘汰掉最旧的一个key之后,保留剩余较旧的key列表放入淘汰池中留
待下一个循环。