1. 什么是Redis
Redis 是互联网技术领域使用最为广泛的存储中间件,它是Remote Dictionary Service的首字母缩写,也就是远程字典服务。Redis 以其超高的性能、完美的文档、简洁易懂的源码和丰富的客户端库支持在开源中间件领域广受好评。对 Redis 的了解和应用实践已成为当下中高级后端开发者绕不开的必备技能。
2. Redis可以做什么
- 数据缓存
- 消息队列
- 分布式锁
- 会话缓存
- 时效性数据
- 访问频率
- 计数器
- 社交列表
- 记录用户判定信息
- 交集、并集和差集
- 热门列表与排行榜
- 最新动态
- 大数据去重
3. Redis的优点
- 速度快:内存存储,查找和操作的时间复杂度O(1)。
- 支持丰富的数据类型:提供了String、List、Hash、Set、Sorted Set 5种基础数据结构,并扩展了Bitmap、HyperLogLog、GEO等高级数据结构。
- 丰富的特性:订阅发布Pub/Sub、Key过期策略、事务、计数、Stream等。
- 持久化存储:提供了RDB和AOF两种数据的持久化存储方案,解决宕机数据丢失的问题。
- 高可用:提供Redis Sentinel高可用方案和Redis Cluster集群方案。
4. Redis和Memcached区别
- 数据结构对比:Redis支持更复杂的数据结构,能支持更丰富的数据操作;Memcached 仅提供简单的字符串。
- 性能对比:Redis 只使用单核(Redis 6开始支持多核),而 Memcached 可以使用多核,所以平均每一个核上 Redis在存储小数据时比 Memcached 性能更高。
- 持久化对比:Redis 支持数据持久化和数据恢复,允许单点故障,但是同时也会付出性能的代价。Memcached不支持数据持久化,断电或重启后数据消失,但其稳定性是有保证的。
- 集群对比:Redis3.x+ 版本中,官方便能支持 Cluster 集群模式。Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。Memcached的分布式不是在服务器端实现的,而是在客户端应用中实现的,即通过内置算法制定目标数据的节点。
-
内存利用率对比:简单的 Key-Value 存储的话,Memcached 的内存利用率更高,可以使用类似内存池。
如果 Redis 采用 hash 结构来做 key-value 存储,由于其组合式的压缩, 其内存利用率会高于 Memcached 。 - 内存管理对比:Redis 采用的是包装的 malloc/free ,相较于 Memcached 的内存管理方法 tcmalloc / jmalloc 来说,要简单很多。
- 网络IO模型对比:Memcached 是多线程,非阻塞 IO 复用的网络模型,原型上接近 Nignx 。Redis 使用单线程的 IO 复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现了 epoll, kqueue 和 select ,更接近 Apache 早期的模式。
5. 为什么 Redis 这么快?
- C语言实现。
- 纯内存操作。
- 核心是基于非阻塞的IO多路复用机制。
- 单线程反而避免了多线程的频繁上下文切换问题。Redis 利用队列技术,将并发访问变为串行访问,消除了传统数据库串行控制的开销。第一,单线程可以简化数据结构和算法的实现。第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
- Redis 使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表对短数据进行压缩存储;再如跳表使用有序的数据结构加快读取的速度。
6. Redis线程模型
关键字:非阻塞 IO、多路复用。
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
- 客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
- 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
7. 如何提高Redis多核 CPU 的利用率
可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的,所以,如果想使用多个 CPU ,可以考虑一下分区。
升级到Redis 6,可以利用多核CPU。
8. Redis数据结构及应用场景
Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。
8.1 Redis 5种基础数据结构:
string(字符串):string表示的是一个可变的字节数组,Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
-
list (列表):list存储结构用的是双向链表而不是数组,随机定位性能较弱( O(n)),首尾插入删除性能较优(O(1))。Redis底层存储是称之为快速链表quicklist的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
hash (哈希):哈希等价于Java语言的HashMap,在实现结构上它使用二维结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值,链表的作用就是用来将产生了「hash碰撞」的元素串起来。
set (集合):set相当于 Java 语言里面的 HashSet,使用hash结构,所有的value都指向同一个内部值。
当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。zset(Sorted Set) (有序集合):zset底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。
zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。
zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
8.2 Redis 4种特殊数据结构:
- HyperLogLog
- Bitmaps
- GEO
- Stream
Redis Module
- BloomFilter
- RedisSearch
- Redis-ML
- JSON
Redis 5.0新增
- Stream
9. Redis数据“过期”策略
在Redis中,同时使用了3种过期策略:
- 被动删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key 。
- 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的 key 。
- 当前已用内存超过 maxmemory 限定时,触发主动清理策略,即 「数据“淘汰”策略」 。
10. Redis数据“淘汰”策略
Redis 在生产环境中,采用配置参数 maxmemory 的方式来限制内存大小。当 Redis 内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使 Redis 性能急剧下降。因此如何淘汰无用数据释放空间,存储新数据变得尤为重要。
10.1 四种淘汰机制:
- LRU(Least recently used,最近最少使用):
- 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- 在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据server.unixtime 计算出来进行排序的,然后选择最近使用时间最久的数据进行删除。每一个 redis 对象都会设置相应的 lru。每一次访问数据,会更新对应lru。
- LRU淘汰策略并不是面向所有最久未使用的键值对,而只是随机挑选的几个键值对。
TTL:
Redis 数据集数据结构中保存了键值对过期时间的表,TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl 最大的键值对淘汰。TTL淘汰策略并不是面向所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对。RANDOM:
在随机淘汰的场景下获取待删除的键值对,随机找hash桶再次hash指定位置的dictEntry。LFU(Least Frequently Used,最久未使用)
- 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”。
10.2 8种淘汰策略及应用场景
Redis 提供了 8 种数据淘汰策略,其中volatile-lfu、allkeys-lfu 2种是Redis 4.0新增。
- volatile-lru
- 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- redis并不是保证取得所有数据集中最近最少使用的键值对,而只是随机挑选的几个键值对中的,当内存达到限制的时候无法写入非过期时间的数据集。
- 没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
- volatile-ttl
- 从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- redis 并不是保证取得所有数据集中最近将要过期的键值对,而只是随机挑选的几个键值对中的, 当内存达到限制的时候无法写入非过期时间的数据集。
- ttl值越大越优先被淘汰。
- volatile-random
- 从已设置过期时间的数据集中任意选择数据淘汰。
- 当内存达到限制无法写入非过期时间的数据集时。
- allkeys-lru
- 从数据集中挑选最近最少使用的数据淘汰。
- 当内存达到限制的时候,对所有数据集挑选最近最少使用的数据淘汰,可写入新的数据集。
- 适用场景:如果我们的应用对缓存的访问符合幂律分布,也就是存在相对热点数据,或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
- allkeys-random
- 从数据集中任意选择数据淘汰。
- 当内存达到限制的时候,对所有数据集挑选随机淘汰,可写入新的数据集。
- 适用场景:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
- no-enviction(默认策略)
- 当内存达到限制的时候,不淘汰任何数据,不可写入任何数据集,所有引起申请内存的命令会报错。
- volatile-lfu
- 从所有配置了过期时间的键中淘汰使用频率最少的键。
- allkeys-lfu
- 从所有键中淘汰使用频率最少的键。
10.3 Redis 回收进程如何工作的?
- 一个客户端运行了新的写命令,发起需要更多内存的申请。
- Redis 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
- Redis 执行新命令。