Redis为什么快?
- Redis是基于内存操作,不需要从外存中读取数据
- Redis拥有各种高效的数据结构
- Redis是单线程模型,从而避开了多线程中上下文频繁切换的操作
- Redis使用多路I/O复用模型,非阻塞I/O
1.1基于内存操作
Redis基于内存操作,不论读写操作都是在内存上完成的,速度远超磁盘数据库。并且采用k-v的方式高效获取数据,是一种NoSQL非关系型数据库。
除了增删改查,Redis还进行了一些维护性操作
- 命中率统计:在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数。
- LRU时间更新:在读取一个键之后,服务器会更新键的LRU时间,这个值可以用于计算键的闲置时间。
- 惰性删除:如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作
- 键的dirty标识:如果有客户端使用WATCH命令监视了该键,服务器会将这个键标记为dirty,让事务程序注意到这个键已经被修改过。每次修改都会对dirty加一,用于触发持久化和复制。
内存资源是非常宝贵的,那么Redis是如何维护数据的呢?
Redis内存回收-过期key处理
Reids中数据过期策略采用定期删除+惰性删除策略。
内存和CPU资源都是宝贵的,Redis的过期键删除通过定期删除设定合理的执行时长和执行频率,配合惰性删除兜底的方式,来达到CPU时间占用和内存浪费之间的平衡。
- 定期删除策略:Redis启用一个定时器定时监视所有key,判断key是否过期,过期的话就删除。这种策略可以保证过期的key最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗CPU资源,并且当key已过期,但是定时器还处于未唤起状态,这段时间内已过期的数据为无效数据且还占用内存。
- 惰性删除策略在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
Redis采用定期删除+惰性删除策略
这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了未检查到的key,基本上满足了所有要求。
但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用触发惰性删除,是否会把内存撑爆?回答是不会,当内存不够用时,内存淘汰机制就会上场。
Redis内存回收-内存淘汰策略
内存淘汰:就是当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。
Redis支持8种不同策略来选择要删除的key:
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选
- volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
- allkeys-lru: 对全体key,基于LRU算法进行淘汰
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu: 对全体key,基于LFU算法进行淘汰
- volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
其中:
- LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
- LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
RedisObject数据结构:
1.2高效的数据结构
在 Redis 中存储value,常用的 5 种数据类型和应用场景如下:
- String: 缓存、计数器、分布式锁等。
- List: 链表、队列、微博关注人时间轴列表等。
- Hash: 用户信息、Hash 表等。
- Set: 去重、赞、踩、共同好友等。
- Zset: 访问量排行榜、点击量排行榜等。
上面的应该叫做 Redis 支持的数据类型,也就是数据的保存形式。针对这 5 种数据类型,Redis底层又有7种数据结构组成上述5种数据类型。
1.2.1 简单动态字符串SDS
我们都知道Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。
不过Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题: 获取字符串长度的需要通过运算 非二进制安全 不可修改 Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。
SDS的优点:
- 可以O(1)复杂度获取字符串长度:有len字段的存在,无需像C结构一样遍历计数。
- 支持动态扩容:C字符串是不支持修改的只能通过复制到一个新的字符串数组中完成修改,而SDS可以直接追加到原字符后(如果申请的内存空间足够)
- 减少内存分配次数:有free字段的存在,使SDS有了空间预分配和惰性释放的能力。
- 对二进制是安全的:二进制可能会有字符和C字符串结尾符 '\0' 冲突,在遍历和获取数据时产生截断异常,而SDS有了len字段,准确了标识了数据长度,不需担心被中间的 '\0' 截断。
3 单线程模型
(1)单线程模型
我们要明确的是:Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。
所谓单线程是指对数据的所有操作都是由一个线程按顺序挨个执行的,使用单线程好处:
- 不会因为线程创建导致的性能消耗;
- 避免上多线程上下文切换引起的 CPU开销;
- 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁的问题。
- 代码更清晰,处理逻辑简单。
单线程是否没有充分利用 CPU 资源呢?
因为 Redis 是基于内存的操作,使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于服务器内存和网络。
然而,使用了单线程的处理方式,就意味着到达服务端的请求不可能被立即处理。那么怎么来保证单线程的资源利用率和处理效率呢?
Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间。
4 IO多路复用
Redis的事件驱动架构由套接字、I/O多路复用、文件事件分派器、事件处理器四个部分组成:
- 套接字(Socket),是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
- I/O多路复用,通过监视多个描述符,当描述符就绪,则通知程序进行相应的操作,来帮助单个线程高效的处理多个连接请求。
- Redis为每个IO多路复用函数都实现了相同的API,因此,底层实现是可以互换的。
- Reids默认的IO多路复用机制是epoll,和select/poll等其他多路复用机制相比,epoll具有诸多优点:
- 事件驱动:Redis设计的事件分为两种,文件事件和时间事件,文件事件是对套接字操作的抽象,而时间事件则是对一些定时操作的抽象。
- Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求
- 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联
- 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与AE_READABLE 事件关联
- 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与AE_WRITABLE 事件关联
- 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器
IO多路复用模型
IO多路复用的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符。当客户端运行时,它将生成具有不同事件类型的套接字。在服务器端,I / O 多路复用程序(I / O 多路复用模块)会将消息放入队列(也就是 下图的 I/O 多路复用程序的 socket 队列),然后通过文件事件分派器将其转发到不同的事件处理器。
简单来说:Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升 Redis 的响应性能。
Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性
评论