主要内容出自:
Java知识体系最强总结(2020版)
CS-Notes
一、Redis与Memcached对比
对比项 | Redis | Memcached |
---|---|---|
数据类型 | 5种 | 只支持最简单的 k/v 数据类型 |
持久化 | RDB 快照和 AOF 日志 | 不支持 |
集群模式 | 原生支持 cluster 模式 | 没有原生的集群模式,需要依靠客户端来实现往集群中分片读写数据(hash取模) |
网络IO模型 | 单线程IO多路复用模型 | 多线程非阻塞IO模式 |
附加功能 | 1. 发布/订阅模式 2.主从分区 3. 序列化支持 4. LUA脚本支持 |
多线程服务支持 |
内存管理机制 | 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的数据交换到磁盘 | Memcached 的数据则会一直在内存中,Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 |
过期数据删除 | 惰性删除+定期删除 | 惰性删除 |
二、Redis数据类型及应用场景
数据类型 | 可以存储的值 | 操作 | 底层数据结构 |
---|---|---|---|
string | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
整数值、SDS |
list | 列表 | 从两端压入或者弹出元素 对单个或者多个元素进行修剪,只保留一个范围内的元素 |
双向链表、压缩列表 |
set | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
字典、整数集合 |
hash | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 |
字典、压缩列表 |
zset | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
跳跃表、字典、压缩列表 |
计数器
可以对 String 进行自增自减运算,从而实现计数器功能。
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
消息队列(发布/订阅功能)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息
不过最好使用 Kafka、RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
可以使用 Redis 自带的 SETNX
命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock
分布式锁实现。
其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
ZSet 可以实现有序性操作,从而实现排行榜等功能。
底层数据结构
Redis 原理及应用(1)–数据类型及底层实现方式
1、简单动态字符串(SDS)
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
SDS的优点:
- 获取字符串长度(SDS O(1))
- 防止缓冲区溢出
- 预分配策略减少扩展或收缩字符串带来的内存重分配次数
- 二进制安全:根据len判断长度,可以存空字符
2、跳跃表
Redis 只在两个地方用到了跳跃表,一个是实现有序集合键(sorted Sets),另外一个是在集群节点中用作内部数据结构。
其实跳表主要是来替代平衡二叉树的,比起平衡树来说,跳表的实现要简单直观的多。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速查找访问节点的目的。
跳跃表是查找、删除、添加等操作都可以在O(logn)期望时间下完成。
typedef struct zskiplist {
//表头节点和表尾节点
structz skiplistNode *header,*tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}
1、层: level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。level数组的每个元素都包含:前进指针:用于指向表尾方向的前进指针,跨度:用于记录两个节点之间的距离
2、后退指针:用于从表尾向表头方向访问节点
3、分值和成员:跳跃表中的所有节点都按分值从小到大排序(按照这个进行排序的,也就是平衡二叉树(搜索树的)的节点大小)。成员对象指向一个字符串,这个字符串对象保存着一个SDS值(实际存储的值)
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
3、整数集合
《Redis 设计与实现》 中这样定义整数集合:“整数集合是集合建(sets)的底层实现之一,当一个集合中只包含整数,且这个集合中的元素数量不多时,redis就会使用整数集合intset作为集合的底层实现。”
可以这样理解整数集合,他其实就是一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大。
typedef struct intset{
//编码方式
uint32_t enconding;
// 集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}
整数集合的底层实现为数组,这个数组以有序,无重复的范式保存集合元素,在有需要时,程序会根据新添加的元素类型改变这个数组的类型.
4、压缩列表
压缩列表用于元素个数少、元素长度小的场景。其优势在于集中存储,节省空间。
- zlbytes:用于记录整个压缩列表占用的内存字节数
- zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节
- zllen:记录了压缩列表包含的节点数量。
- entryX:要说列表包含的各个节点
- zlend:用于标记压缩列表的末端
压缩列表是一种为了节约内存而开发的顺序型数据结构
压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
添加新节点到压缩列表,可能会引发连锁更新操作。
因为普通链表节点的内存是随机分配的, 占用的内存是零星的,如果是大量数据的话使用这个好, 但是如果是少量数据的话这样比较浪费空间, 而压缩列表使用的内存是连续的, 在少量数据的时候使用压缩列表节约了一定的内存
三、过期键的删除策略
对于散列表这种容器,只能为整个散列表设置过期时间,而不能为散列表里面的单个元素设置过期时间。
1、立即删除
在设置键的过期时间时,创建一个回调事件,当过期时间达到时,由时间处理器自动执行键的删除操作。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。
但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。
2、惰性删除
键过期之后不管它。每次从dict字典中按key取值时,先检查此key是否已经过期,如果过期了就删除它,并返回nil,如果没过期,就返回键值。
惰性删除的缺点:浪费内存。dict字典和expires字典都要保存这个键值的信息。且对于一些后续不经常访问到的数据不能够及时删除。
3、定期删除
每隔一段时间,对expires字典进行检查,删除里面的过期键。
- 通过限制删除操作执行的时长和频率,来减少删除操作对cpu的影响。
- 定时删除也有效的减少了因惰性删除带来的内存浪费。
redis使用的过期键值删除策略是:惰性删除加上定期删除,两者配合使用。
4、对应的数据结构
Redis 通过一个过期字典(expires
,可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
过期字典是存储在 redisDb 这个结构里的:
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
dictht 是一个散列表结构,使用拉链法解决哈希冲突。
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。
四、数据淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
策略 | 描述 | 应用场景 |
---|---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | 如果设置了过期时间,且分热数据与冷数据,推荐使用 volatile-lru 策略。 |
volatile-ttl(time to live) | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | 如果让 Redis 根据过期时间来筛选需要删除的key,请使用 volatile-ttl 策略。 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | 很少使用 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。最常用 值得一提的是,设置 expire 会消耗额外的内存,所以使用 allkeys-lru 策略,可以更高效地利用内存,因为这样就可以不再设置过期时间了。 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 | 如果需要循环读写所有的key,或者各个key的访问频率差不多,可以使用 allkeys-random 策略 |
no-eviction | 不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。大多数写命令都会导致占用更多的内存 | 很少使用 |
volatile-lfu | LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰 | Redis 4.0 引入 |
allkeys-lfu | LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰 | Redis 4.0 引入 |
五、Redis持久化
Redis 提供了RDB和AOF两种持久化方式。默认是只开启RDB,当Redis重启时,它会优先使用AOF文件来还原数据集。
1、RDB持久化(快照持久化)
RDB 持久化:将某个时间点的所有数据都存放到硬盘上。
快照持久化是Redis默认采用的持久化方式
- 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。如果数据量很大,保存快照的时间会很长。
- 如果系统发生故障,将会丢失最后一次创建快照之后的数据。
- 快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。
创建快照的方式:
-
BGSAVE命令 :客户端向Redis发送 BGSAVE命令 来创建一个快照。对于支持BGSAVE命令的平台来说(基本上所有平台支持,除了Windows平台),Redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求。
-
SAVE命令 :客户端还可以向Redis发送 SAVE命令 来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常用,我们通常只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。
-
save选项 :如果用户设置了save选项(一般会默认设置),比如 save 60 10000,那么从Redis最近一次创建快照之后开始算起,当“60秒之内有10000次写入”这个条件被满足时,Redis就会自动触发BGSAVE命令。
-
SHUTDOWN命令 :当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在SAVE命令执行完毕之后关闭服务器。
-
一个Redis服务器连接到另一个Redis服务器:当一个Redis服务器连接到另一个Redis服务器,并向对方发送PSYNC命令来开始一次复制操作的时候,如果主服务器目前没有执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令
2、AOF持久化(Append Only File)
AOF持久化:将写命令添加到 AOF 文件(Append Only File)的末尾
与快照持久化相比,AOF持久化的实时性