文章目录
-
- 1 Redis 基础
-
- 1.1 说一下 Redis 和 Memcached 的区别和共同点
- 1.2 为什么要用 Redis?
- 1.3 Redis 常用的数据类型有哪些?🔥
- 1.4 String 还是 Hash 存储对象数据更好呢?
- 1.5 Redis 基本数据类型底层实现原理🔥
- 1.6 压缩列表是怎么实现的?
- 1.7 跳表是怎么实现的?🔥
- 1.8 介绍一下 Redis 中的 listpack
- 1.9 哈希表是怎么扩容的?
- 1.10 购物车信息用 String 还是 Hash 存储更好呢?
- 1.11 Redis 的有序集合底层为什么要用跳表?🔥
- 1.12 Redis 如何实现数据不丢失(持久化)🔥
- 1.13 AOF 为什么是在执行完命令之后再记录日志?
- 1.14 AOF 重写
- 2 Redis 线程模型
- 3 Redis 内存管理
- 4 Redis 性能优化
- 5 Redis 缓存问题
- 6 Redis 集群
- 7 Redis 应用
本文作者:夏日。主要参考:JavaGuide、小林coding和二哥的Java进阶之路,同时加上网上搜索整理和个人理解总结。
1 Redis 基础
1.1 说一下 Redis 和 Memcached 的区别和共同点
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别:
- 数据类型:Redis 支持更丰富的数据类型。Memcached 只支持最简单的 k/v 数据类型。Redis 还提供 list,set,zset,hash 等数据结构。
- 数据持久化:Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用了;Memcached 数据只存在内存之中,重启后就没了。
- 集群模式支持:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 自 3.0 版本起是原生支持集群模式的。
- 线程模型:Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
- 特性支持:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
- 过期数据删除:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
1.2 为什么要用 Redis?
**1、Redis 具备高性能
传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。
2、Redis 具备高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍。所以可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库,提高了系统整体的并发。
3、功能全面
Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
1.3 Redis 常用的数据类型有哪些?🔥
基本数据类型:
- String:字符串
- 值可以是字符串、数字或者二进制,但值最大不能超过512MB。
- 应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
- Hash:哈希
- Hash 是一个键值对集合。
- 应用场景:缓存对象、购物车等。
- Set:集和
- 无序去重的集合。
- 应用场景:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- List:列表
- 有序可重复的集合,底层是依赖双向链表实现的。
- 应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
- SortedSet(Zset):有序集和
- 增加了一个权重参数
score
,使得集合中的元素能够按score
进行有序排列 - 应用场景:适用于排行榜和带权重的消息队列等场景。
- 增加了一个权重参数
特殊的数据类型:
- Bitmap:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在 Bitmap 中叫做偏移量。Bitmap 的长度与集合中元素个数无关,而是与基数的上限有关。
- 适用场景:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等。
- Hyperloglog。HyperLogLog 是用来做海量数据基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
- 应用场景:比如百万级网页 UV 计数,如统计独立访客。
- Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作
- 应用场景:适用场景如定位、附近的人等。
- Stream :Redis Stream 是 Redis 5.0 版本新增的专门为消息队列设计的数据类型,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
1.4 String 还是 Hash 存储对象数据更好呢?
- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
在绝大部分情况,我们建议使用 String 来存储对象数据即可!
1.5 Redis 基本数据类型底层实现原理🔥
1.5.1 String 的底层实现
Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 \0
结尾的字符数组),而是自己编写了 SDS(Simple Dynamic String,简单动态字符串) 来作为底层实现。
SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。
- 获取字符串长度的复杂度较低:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
- 可以保存二进制数据:C 语言中的字符串以空字符
\0
作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。 - 减少内存分配次数:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
1.5.2 List 的底层实现
List 类型的底层数据结构是由双向链表或压缩列表实现的:
- 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值),Redis 会使用压缩列表作为 List 类型的底层数据结构;
- 如果不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了。
1.5.3 Hash 的底层实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
- 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
- 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
在 Redis 7.0 中,压缩列表数据结构已经废弃,交由 listpack 数据结构来实现。
1.5.4 Set 的底层实现
Set 类型的底层数据结构是由哈希表或整数集合实现的:
- 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
- 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
1.5.5 Zset 的底层实现
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现。
1.6 压缩列表是怎么实现的?
压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。当我们往压缩列表中插入数据时,会根据数据大小和类型进行不同的空间大小分配,以此来节省内存。如果我们要查找第一个元素和最后一个元素,可以通过表头直接定位,复杂度是 O(1)。而查找其他元素就只能逐个查找,此时的复杂度是 O(N) ,因此压缩列表不适合保存过多的元素。
1.7 跳表是怎么实现的?🔥
跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表。实现这一特性就是靠跳表节点中的zskiplistLevel 结构体类型的 level 数组。level 数组中的每一个元素代表跳表的一层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。
跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
1.8 介绍一下 Redis 中的 listpack
listpack 中每个节点不再像压缩列表一样包含前一个节点的长度,但还是用一块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采用不同的编码方式保存不同大小的数据。当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
1.9 哈希表是怎么扩容的?
为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
- 给「哈希表 2」 分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
- 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
1.10 购物车信息用 String 还是 Hash 存储更好呢?
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
那用户购物车信息的维护具体应该怎么操作呢?
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的 key 即可。
1.11 Redis 的有序集合底层为什么要用跳表?🔥
-
平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的算法简单得多,速度也快得多。
-
红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
-
B+树 vs 跳表:B+树更适合作为数据库中的索引结构,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。
1.12 Redis 如何实现数据不丢失(持久化)🔥
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。
Redis支持三种方式的持久化: RDB
快照、 AOF
日志,Redis 4.0 新增了RDB 和 AOF 的混合持久化。
1.RDB 是一个快照文件,它会根据指定的规则定时将 redis 内存中的数据以二进制的方式写入磁盘。
- 优点:
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式。
- 使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。
- RDB文件存储的是压缩的二进制文件,适用于备份、全量复制,可用于灾难备份
- 缺点:
- RDB是间隔一段时间进行持久化,频率太低会丢失大量数据,频率太高就会很影响性能。
- RDB方式数据无法做到实时持久化。因为
BGSAVE
每次运行都要执行fork
操作创建子进程,属于重量级操作。 - RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版 RDB 格式的问题。
2.AOF 的含义是追加文件,当 redis 执行完命令的时候,都会把命令记录到日志文件中。AOF的主要作用是解决了数据持久化的实时性。
- 优点:
- AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次同步操作,如果Redis进程挂掉,最多丢失1秒的数据。
- AOF以
append-only
的模式写入,所以没有磁盘寻址的开销,写入性能非常高。
- 缺点:对于同一份数据,AOF文件比RDB快照要大,数据恢复比较慢。
3.RDB 和 AOF 的混合持久化:AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头,之后的数据再以AOF的格式追加到文件的末尾。
- 好处是可以结合 RDB 和 AOF 的优点,快速加载同时避免丢失过多的数据。
- 缺点是AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,