写点什么

Redis 套路,一网打尽

发布于: 19 小时前


几乎涵盖了 Redis 常见知识点,希望对大家有帮助

本文内容提要

  1. Redis 为什么这么快

1.1. 数据结构 SDS 的妙用

1.2. 性能优良的事件模型驱动

1.3. 基于内存的操作

  1. Redis 为什么这么靠谱

2.1. AOF 持久化

2.2. RDB 持久化

2.3. Sentinel 高可用

  1. Redis6.x 多线程一览

  2. Redis 最佳实践

Part1 Redis 为什么这么快

1.1 数据结构 SDS 的妙用

我们知道 redis 的底层是用 c 语言来编写的,但是,数据结构确没有直接套用 C 的结构,而是根据 redis 的定位自建了一套数据结构。

C 语言中的字符串结构:


SDS 定义下的字符串结构:


可以看到,相比于 C 语言来说,也就多了几个字段,分别用来标识空闲空间和当前数据长度,但简直是神来之笔:

  • 可以 O(1)复杂度获取字符串长度;有 len 字段的存在,无需像 C 结构一样遍历计数。

  • 杜绝缓存区溢出;C 字符串不记录已占用的长度,所以需要提前分配足够空间,一旦空间不够则会溢出。而有 free 字段的存在,让 SDS 在执行前可以判断并分配足够空间给程序

  • 减少字符串修改带来的内存重分配次数;有 free 字段的存在,使 SDS 有了空间预分配和惰性释放的能力。

  • 对二进制是安全的;二进制可能会有字符和 C 字符串结尾符 '\0' 冲突,在遍历和获取数据时产生截断异常,而 SDS 有了 len 字段,准确了标识了数据长度,不需担心被中间的 '\0' 截断。

上面的内容以字符串来说明 SDS 和 C 语言数据结构的差异和优势。顺便来看看链表、hash 表、跳表分别被 Redis 设计成了什么样的数据结构:





<<< 左右滑动见更多 >>>

可以看到,Redis 在设计数据结构的时候出发点是一致的。总结起来就是一句话:空间换时间。

用牺牲存储空间和微小的计算代价,来换取数据的快速操作

1.2 性能优良的事件驱动模式

redis6.x 之前,一直在说单线程如何如之何的好。

那么,具体单线程体现在哪里,又是怎么完成数据读写工作的呢?

$ 单线程

关于新版本的多线程模型在后面小节单独说,这里先说单线程。

所谓单线程是指对数据的所有操作都是由一个线程按顺序挨个执行的,使用单线程可以:

  • 避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU;

  • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

然而,使用了单线程的处理方式,就意味着到达服务端的请求不可能被立即处理。

那么怎么来保证单线程的资源利用率和处理效率呢?

$ IO 多路复用和事件驱动

Redis 服务端,从整体上来看,其实是一个事件驱动的程序,所有的操作都以事件的方式来进行。



如图所示,Redis 的事件驱动架构由套接字、I/O 多路复用、文件事件分派器、事件处理器四个部分组成:

套接字(Socket),是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。

I/O 多路复用,通过监视多个描述符,当描述符就绪,则通知程序进行相应的操作,来帮助单个线程高效的处理多个连接请求。

Redis 为每个 IO 多路复用函数都实现了相同的 API,因此,底层实现是可以互换的。

Reids 默认的 IO 多路复用机制是 epoll,和 select/poll 等其他多路复用机制相比,epoll 具有诸多优点:

|| 并发连接限制 | 内存拷贝 | 活跃连接感知 | | --- | --- | --- | --- | | epoll | 没有最大并发连接的限制 | 共享内存,无需内存拷贝 | 基于 event callback 方式,只感知活跃连接 | | select | 受 fd 限制,32 位机默认 1024 个/64 位机默认 2048 个 | 把 fd 集合从用户态拷贝到内核态 | 只能感知有 fd 就绪,但无法定位,需要遍历+轮询 | | poll | 采用链表存储 fd 无最大并发连接数限制 | 同 select | 同 select,需遍历+轮询 |

事件驱动,Redis 设计的事件分为两种,文件事件和时间事件,文件事件是对套接字操作的抽象,而时间事件则是对一些定时操作的抽象。

文件事件:

  • 客户端连接请求(AE_READABLE 事件)

  • 客户端命令请求(AE_READABLE 事件)和事

  • 服务端命令回复(AE_WRITABLE 事件)

时间事件: 分为定时事件和周期性时间;redis 的所有时间事件都存放在一个无序链表中,当时间事件执行器运行时,需要遍历链表以确保已经到达时间的事件被全部处理。

可以看到,Redis 整个执行方案是通过高效的 I/O 多路复用件驱动方式加上单线程内存操作来达到优秀的处理效率和极高的吞吐量。

1.3 基于内存的操作

上面的小节也提到了,redis 之所以可以使用单线程来处理,其中的一个原因是,内存操作对资源损耗较小,保证了处理的高效性。

如此宝贵的内存资源,Redis 是怎么维护和管理的呢?

$ 除了增删改查还有哪些维护性操作[1]

命中率统计,在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数。

LRU 时间更新,在读取一个键之后,服务器会更新键的 LRU 时间,这个值可以用于计算键的闲置时间。

惰性删除,如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作。

键的 dirty 标识,如果有客户端使用 WATCH 命令监视了该键,服务器会将这个键标记为 dirty,让事务程序注意到这个键已经被修改过。每次修改都会对 dirty 加一,用于_触发持久化和复制_。

数据库通知,“如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知”

$ Redis 何如管理内存

过期键删除,内存和 CPU 资源都是宝贵的,Redis 通过定期删除设定合理的执行时长和执行频率,配合惰性删除兜底的方式,来达到 CPU 时间占用和内存浪费之间的平衡。

数据淘汰,如果 key 生产的太快,定期删除操作跟不上新生产的速率,而这些 key 又很少被访问无法触发惰性删除,是否会把内存撑爆?回答是不会,因为 redis 有数据淘汰策略:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

  • allkeys-lru:当内存不足以容纳新写入数据时,,移除最近最少使用的 Key。

  • allkeys-random:当内存不足以容纳新写入数据时,随机移除某个 Key。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

值得一提的是,这里的 lru 和平常我们所熟知的 lru 还不完全一样,redis 使用的是采样概率的思想,省略了双向链表的内存消耗。

Redis 会在每一次处理命令的时候判断是否达到了最大限制,如果达到则使用对应的算法去删除涉及到的 Key,这时,我们前面所维护过键的 LRU 值就会派上用场了。

Part2 Redis 为什么这么靠谱

天有不测风云,服务器也有趴窝的时候,Redis 这个基于内存的存储遇到服务器宕机该怎么应对呢?

2.1RDB 持久化

持久化是一种常见的解决方案,那么,我们首先能想到的最简单的持久化方案,就是每隔一段时间把内存里的数据保存一次,来避免绝大部分数据的丢失。这也是 Redis 的 RDB 持久化得思路。

RDB 有两种方式,save 和 bgsave

save,会阻塞服务器的其他操作,直到 save 执行完成,所以,这个期间的所有命令请求都会被拒绝。对客户端影响较大。

BGSave,由子进程进行数据保存,期间 redis 仍然可以继续处理客户端请求。为了防止竞争和冲突,bgsave 被设计成和 save/bgrewriteaof 操作互斥。

Redis 服务器默认每 100 毫秒执行一次,如果数据库修改次数(dirty 计数器)大于设置的阈值,并且距离上次执行保存的时间(lastsave 属性)大于设置的阈值,则执行保存操作。

因为是统一批量的保存操作,rdb 文件有二进制存储、结构紧凑、空间消耗少、恢复速度快等特点,在持久化方案上不可或缺。

2.2AOF 持久化

然而,因为 bgsave 的周期间隔和保存触发条件等原因,在服务器宕机时,不可避免的会丢失一部分最新的数据。这就需要一些辅助手段来做持久化补充。

RDB 保存的是键值对,而 AOF 则用来保存写命令。

为什么 AOF 保存的是命令,而不是键值对呢?

Coder 的技术之路认为,一是因为 aof 刷盘,是在文件事件处理过程当中的,具体位置是在结束一个事件循环之前,调用追加函数进行,所以,使用请求命令来存储更方便;二是如果遇到追加过程中命令被破坏,也可以通过 redis-check-aof 来恢复(命令恢复起来比较方便)。

AOF 刷盘策略,由于 aof 追加动作是和客户端请求处理串行执行的,所以每次都刷盘对性能影响较大,因此都是先追加到 aof_buf 缓存区里,而是否同步到 AOF 文件中则依赖 always、everysec(默认)、no 的刷盘配置。想比 everysec ,always 对性能影响较大,而 no 则容易丢失数据。

AOF 文件重写压缩,AOF 因为保存了请求命令,自然要比 RDB 更大,并且随着程序的运行,会越来越大,然而,文件中有很多冗余的命令数据是可以压缩的,因为对于某个键值对,某一时刻只会有一个状态。



那么,在重写过程中新产生的操作该怎么办呢?



2.3Sentinel 高可用解决方案

上面两个小节,主要是在阐述单机服务器的数据稳定性保障,那么,如果是多机、多进程该怎么来保障呢?

哨兵的作用:监视服务节点的健康



当主节点宕机时,由哨兵感知,并在从节点中重新选举主节点:



同时,sentinel 还会监视宕机的 master 节点,恢复之后会将其设置为从节点加入集群。

除了主从切换的 sentinel 方案,还有 Cluster 集群模式来保障 redis 的高可用,用来解决主从复制的存储浪费问题。

Part3 Redis6.x 的多线程

之前已经阐述过了单线程模型的整体流程,这里不太赘述。

Redis 的多线程模型,不是传统意义上的多线程并发,而是把 socket 解析回写的这部分操作并行化,以解决 IO 上的时间消耗带来的系统瓶颈。



对客户端的任何请求,其实还是主线程在执行,避免了操作相同数据时线程间的竞争,把 io 部分并行化,降低了 io 对资源的损耗,从而提升了系统的吞吐量。仔细想来,感觉和 rpc 中的异步调用差不多意思,都是绑定来源,等待处理完成后给给各来源返回对应结果。

Part4 Redis 最佳实践

Redis 被当做分布式缓存的应用场景非常普遍,有关缓存穿透、缓存击穿、缓存雪崩、数据漂移、缓存踩踏、缓存污染、热点 key 等常见问题,在上一篇文章 诸多策略,缓存为王中已经有了详细阐述,这里不再重复。

这里主要给出一些日常开发中的关注点:

  • Key 的设计。尽量控制 key 的长度,一是过长会占用较多空间,二是我们知道键空间是字典类型,即时本身在查找过程中很快,过长的键也会对比较判断时间有所增加。

  • 批量命令的使用。因为 redis 操作绝大部分都耗在网络传输上,将多次传输改为一次传输,大概率会提升效果。

  • value 的大小。尽量避免大 value,原因同上,value 太大会影响网络传输效率。比如,之前的一次经历,批量获取了 200 个商品的信息(信息比较多,可以认为是大 value),发现很慢,后来把 200 拆成了 4 个 50,并行去调用,效果提升的比较明显。这个问题也可以考虑用数据压缩的方式进行优化

  • 复杂命令的使用。比如排序、聚合等等操作,应该在离线阶段就处理完毕,然后再存入缓存,而不是在线使用复杂命令去计算。

  • 善用数据结构。redis 丰富的数据结构对支撑业务有天然的优势,比如,之前曾用消息队列配合 bitmap 数据结构存储和维护商品的多个状态(库存、上下架、秒杀、黑白名单等),getbit 来直接判断该商品是否允许展示。

其实没有什么最佳实践,业务各有各的不同,都需要在实践中研究尝试,如果大家有非常好的实际案例,也欢迎补充,欢迎留言交流~

推荐阅读:

1. 高并发架构优化:细说负载均衡

2. 高并发架构优化:万亿流量下的负载均衡实战

3. 高并发架构优化:从BAT实际案例看消息中间件的妙用

4. 高并发存储优化:细说数据库索引原理及其优化策略

5. 高并发存储优化:也许是史上最详尽分库分表文章之一

6. 高并发存储优化:数据库索引优化Explain实战

7. 高并发存储优化番外:阿里数据中间件源码不完全解析

8. 高并发存储优化:诸多策略,缓存为王


欢迎关注同名公众号:Coder 的技术之路大家的每个鼓励都是我坚持的动力!


参考资料

[1]

Redis 设计与实现: 黄健宏.著

发布于: 19 小时前阅读数: 10
用户头像

还未添加个人签名 2018.03.14 加入

一个工作6年的技术人,浪过京东、支付宝, 干过电商、搞过支付链路、玩过广告系统~ ,欢迎关注同名微信公众号,有任何想法问题,欢迎大伙交流讨论

评论

发布
暂无评论
Redis套路,一网打尽