Redis 介绍
键值存储 | 内存数据库 | 支持多种数据结构(字符串、列表、集合、有序集合、哈希表) | 高性能
发布/订阅模式 | 事务支持 | 分布式缓存 | 实时分析和计数
Redis 应用
1. Redis 锁
2. Redis缓存
a. 限制短信通知次数
使用Redis记录用户短信通知次数,有效期 24 小时,以控制和限制通知次数
b. 请求幂等性:
通过Redis记录重复的请求数据,确保请求的幂等性,同时可用于削峰处理
c. 记录任务信息 + 优先级消息队列
使用Redis记录任务信息,并结合优先级消息队列实现任务调度和处理
d. 延时队列
通过Redis实现延时队列,用于处理需要延迟执行的任务
c. 高频热点资源的记录
缓存高频热点资源,如合同导出结果,以减少性能损耗和提高响应速度
d. 埋点
使用Redis来记录用户行为,如用户请求接口和用户 24 小时内短信次数统计,以方便后台统计数据
Redis 原理
IO 模型 | 持久化 |
一. Redis IO模型
1. IO模型:
BIO Blocking I/O 阻塞| NIO Non-Blocking 非阻塞| AIO Asynchronous I/O 异步
二.持久化
Redis支持数据持久化,将内存中的数据保存到硬盘上以便在重启时恢复数据.
RDB持久化
1. RDB持久化的触发
-
RDB是一种快照持久化方式,它将Redis的数据以二进制形式保存到磁盘上。
-
使用
SAVE
命令进行 RDB 持久化,它会阻塞 Redis 服务器进程,直到 RDB 文件创建完成。Redis服务器进程在执行SAVE
命令期间被阻塞,不能处理其他命令请求。 -
使用
BGSAVE
命令触发 BGSAVE 持久化,它会创建一个子进程负责生成 RDB 文件,父进程继续处理命令请求。BGSAVE 操作是在后台进行的,允许 Redis 服务器在持久化操作期间继续处理其他命令请求,提高了并发性。 -
SAVE
命令和BGSAVE
命令都是针对全数据库的持久化.
2. RDB自动间隔性保存
因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
save选项示例:
save
选项允许设置多个保存条件,只要满足其中任意一个条件,服务器就会执行BGSAVE命令。
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
❑服务器在900秒之内,对数据库进行了至少1次修改。
❑服务器在300秒之内,对数据库进行了至少10次修改。
❑服务器在60秒之内,对数据库进行了至少10000次修改。
2.1 设置保存条件
当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项.
在企业级项目中,通常会在Redis服务器的配置文件(如redis.conf
)中配置这些选项.
public class RedisServer {
// 其他属性...
private SaveParameter[] saveparams;
}
public class SaveParameter {
// 秒数
public long seconds;
// 修改数
public int changes;
}
2.2 dirty计数器和lastsave属性
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性:
❑dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,
服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)
❑lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间
public class RedisServer {
// 修改计数器
private long dirty;
// 上一次执行保存的时间
private Date lastsave;
// 其他属性...
private SaveParameter[] saveparams;
}
- 服务器维护一个
dirty
计数器,记录距离上一次成功执行SAVE或BGSAVE命令之后,数据库状态(所有数据库)发生了多少次修改。 lastsave
属性记录了服务器上一次成功执行SAVE或BGSAVE命令的时间戳。
2.3 保存条件检查
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
public class RedisServer {
// ...
// 其他成员变量和方法...
public void serverCron() {
// ...
// 遍历所有保存条件
List<SaveParam> saveParams = server.getSaveParams();
long currentTime = System.currentTimeMillis() / 1000; // 获取当前时间的秒数
for (SaveParam saveParam : saveParams) {
// 计算距离上次执行保存操作有多少秒
long saveInterval = currentTime - server.getLastSaveTime();
// 如果数据库状态的修改次数超过条件所设置的次数
// 并且距离上次保存的时间超过条件所设置的时间
// 那么执行保存操作
if (server.getDirty() >= saveParam.getChanges() &&
saveInterval > saveParam.getSeconds()) {
bgSave();
}
}
// ...
}
// ...
}
public class SaveParam {
private int seconds;
private int changes;
}
程序会遍历并检查saveparams数组中的所有保存条件,只要有任意一个条件被满足,那么服务器就会执行BGSAVE命令。
BGSAVE在执行之后完成,其中dirty计数器已经被重置为0,而lastsave属性也被更新为BGSAVE完成的时间.
2.4 RDB文件结构
📢: 上图中, 全大写单词标示常量,用全小写单词标示变量和数据
a. RDB是二进制文件
b. RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着“REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。
c. db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号,比如"0006"就代表RDB文件的版本为第六版。本章只介绍第六版RDB文件的结构。
d. databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:
❑如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字 节。
❑如果服务器的数据库状态为非空(有至少一个数据库非空), 那么这个部分也为非空,根据 数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。
e. EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了
f. check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
DATABASE部分:
一个RDB文件的databases部分可以保存任意多个非空数据库。例如,如果服务器的0号数据库和3号数据库非空,那么服务器将创建一个如下图所示的RDB文件,图中的database 0代表0号数据库中的所有键值对数据,而database 3则代表3号数据库中的所有键值对数据。
每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分:
AOF持久化
Redis数据结构
简单动态字符串 | 链表 | 字典 | 跳跃表 | 对象
一. 简单动态字符串: SDS Simple Dynamic Strings
1. 基本组成部分
public class SDS {
private int len; // 保存字符串的长度
private int free; // 保存未使用字节的数量
private char[] buf; // 字节数组,用于保存字符串
}
C字符串(字符数组)结尾的惯例:SDS遵循了C字符串(以Null字符 \0
结尾的字符串)的惯例。这意味着SDS的存储方式与C字符串类似,字符串的结尾会有一个Null字符,表示字符串的结束。但需要注意的是,这个Null字符不计入SDS的len
属性(字符串的长度)中。
2. "未使用空间在SDS中的作用"
-
C字符串的长度计算复杂度是O(N):C字符串是以空字符 '\0' 结尾的,要获取其长度,程序必须遍历整个字符串直到遇到空字符为止。这种操作的时间复杂度是O(N),其中N是字符串的长度。
-
SDS的长度计算复杂度是O(1):SDS内部记录了字符串的长度(len属性),因此无论字符串有多长,获取长度的操作复杂度都是O(1)。这是SDS的一个显著优势。
-
性能优势:Redis中采用SDS作为字符串的内部表示方式,这降低了获取字符串长度的复杂度,从O(N) 降低到了O(1)。这确保了Redis在处理字符串键时的高性能,即使字符串非常长也不会影响系统性能。
3. 杜绝缓冲区溢出
-
C字符串的缓冲区管理:C字符串操作(例如
strcat
)假定用户已经为目标字符串(dest
)分配了足够的内存,以容纳要追加的内容。如果这个假定不成立,就会发生缓冲区溢出,导致数据覆盖相邻内存。 -
SDS的自动空间管理:与C字符串不同,SDS的API会自动管理字符串的内存分配和扩展。在执行修改操作之前,SDS的API会检查是否有足够的空间来容纳修改后的内容,如果不足,API会自动扩展SDS的内存,然后再执行修改操作。这杜绝了缓冲区溢出的可能性。
4. 内存重分配次数
-
C字符串的长度与底层数组长度关联:C字符串的底层实现总是一个N+1个字符长的数组,其中N是字符串的长度。因此,每次增长或缩短C字符串都需要进行内存重分配,这可能会导致缓冲区溢出或内存泄漏。
-
SDS的解耦长度和底层数组长度:SDS通过未使用的空间来解除字符串长度和底层数组长度之间的关联。SDS的buf数组可以包含未使用的字节,这些字节的数量由SDS的free属性记录。SDS的长度和底层数组的长度不一定相等。
-
空间预分配策略:SDS通过空间预分配策略优化了字符串的增长操作。当SDS需要进行空间扩展时,程序会为SDS分配额外的未使用空间,其数量取决于SDS的长度变化。如果长度变化较小,程序分配的未使用空间也较小;如果长度变化较大,程序会分配1MB的未使用空间。这减少了连续执行字符串增长操作所需的内存重分配次数.
SDS(Simple Dynamic Strings)中空间预分配策略的原则
📢其根据字符串长度的变化来确定额外分配的未使用空间数量
长度小于1MB的情况:如果对SDS进行修改后,SDS的长度(len属性的值)将小于1MB,那么程序将分配的未使用空间与len属性的值相同。例如,如果修改后的长度为13字节,程序会分配额外的13字节未使用空间。此时,SDS的buf数组的实际长度将变为原长度加上额外分配的未使用空间再加上1字节用于保存空字符,即 len + 13 + 1 字节。
长度大于等于1MB的情况:如果对SDS进行修改后,SDS的长度将大于等于1MB,程序会分配1MB的未使用空间。例如,如果修改后的长度为30MB,程序会分配1MB的未使用空间。此时,SDS的buf数组的实际长度将变为原长度加上1MB的未使用空间再加上1字节用于保存空字符,即 len + 1MB + 1 字节。
5. 惰性空间释放
-
惰性空间释放的原理:当SDS的API需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收多出来的字节,而是使用free属性记录这些多余字节的数量,并将其保留在SDS内部。这些字节并不立即释放,而是等待将来的使用。
-
优化未来增长操作:通过惰性空间释放策略,SDS避免了在缩短字符串时不必要的内存重分配操作。而当将来需要执行增长操作时,SDS可以利用这些未使用空间,从而减少内存开销。
-
手动释放未使用空间:SDS还提供了相应的API,允许开发者在有需要时手动释放SDS的未使用空间,以确保不会浪费内存资源。这意味着程序可以在适当的时候释放那些不再需要的空间。
6. 二进制安全
SDS的二进制安全性使其更加灵活,能够处理各种类型的数据,包括文本和二进制数据。这使得Redis可以适用于广泛的使用场景,不仅仅局限于文本数据的存储和处理.
SDS不管存储什么数据, 都会以空字符结尾, 以此来兼容C语言的字符串相关的库.
-
C字符串的限制:C字符串必须符合某种编码(如ASCII),并且字符串中除了末尾的空字符外,不能包含其他空字符。这些限制使得C字符串只能用于保存文本数据,无法保存二进制数据。如果特殊数据格式使用空字符分割单词,C字符串函数会误认为空字符为字符串结尾。
-
SDS的二进制安全:为了适应不同的使用场景,SDS的API都是二进制安全的。SDS API以处理二进制数据的方式来操作SDS的buf数组,不对数据进行限制、过滤或假设。SDS的buf属性被称为字节数组,用于存储一系列二进制数据。
-
灵活性:SDS的二进制安全特性使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。特殊数据格式,如使用空字符分割的单词,也可以轻松地保存在SDS中,因为SDS使用len属性而不是空字符来确定字符串是否结束。
二. 链表
1. 基本组成部分
a. 链表节点基本组成部分
public class ListNode<T> {
// 前置节点
public ListNode<T> prev;
// 后置节点
public ListNode<T> next;
// 节点的值
public T value;
public ListNode(ListNode<T> prev, ListNode<T> next, T value) {
this.prev = prev;
this.next = next;
this.value = value;
}
}
b.链表组成部分
public class ListNode<T> {
// 表头节点
public ListNode<T> head;
// 表尾节点
public ListNode<T> tail;
// 链表所包含的节点数量
public long len;
// 节点值复制函数
public DupFunction<T> dup;
// 节点值释放函数
public FreeFunction<T> free;
// 节点值对比函数
public MatchFunction<T> match;
public ListNode(ListNode<T> head, ListNode<T> tail, long len, DupFunction<T> dup, FreeFunction<T> free, MatchFunction<T> match) {
this.head = head;
this.tail = tail;
this.len = len;
this.dup = dup;
this.free = free;
this.match = match;
}
}
// 定义函数式接口用于节点值复制、释放和对比函数
@FunctionalInterface
public interface DupFunction<T> {
T duplicate(T ptr);
}
@FunctionalInterface
public interface FreeFunction<T> {
void free(T ptr);
}
@FunctionalInterface
public interface MatchFunction<T> {
int match(T ptr, T key);
}
-
双端链表:Redis链表是双向链表,每个节点都有指向前一个节点(prev)和后一个节点(next)的指针,因此获取某个节点的前置节点和后置节点的复杂度都是O(1)。
-
无环链表:Redis链表没有环,表头节点的prev指针和表尾节点的next指针都指向NULL,确保对链表的访问以NULL为终点。
-
带表头和表尾指针:链表结构中包含head指针和tail指针,这使得程序能够以O(1)的复杂度获取链表的表头节点和表尾节点。
-
带长度计数器:Redis链表使用len属性来追踪链表中节点的数量,这使得程序能够以O(1)的复杂度获取链表的节点数量。
-
多态:链表节点使用
void*
指针来保存节点值,而且可以为节点值设置类型特定的函数,包括dup(复制)、free(释放)、match(对比)。这使得Redis链表可以用于保存各种不同类型的值,实现多态的数据结构。
三. 字典
字典是一种数据结构,它可以存储和组织键值对(key-value pair),其中每个键(key)都唯一地对应一个值(value)。这允许通过键来快速查找、插入、更新或删除对应的值。字典通常用于存储和管理各种类型的数据,使其易于检索和操作。
1. 基本组成部分
a. 哈希表
public class DictHt {
// 哈希表数组
private DictEntry[][] table;
// 哈希表大小
private long size;
// 哈希表大小掩码,用于计算索引值,总是等于 size - 1
private long sizemask;
// 该哈希表已有节点的数量
private long used;
}
-
table
属性是一个数组,其中每个元素都是一个指向dictEntry
结构的指针。每个dictEntry
结构保存着一个键值对。 -
size
属性记录了哈希表的大小,即table
数组的大小。它表示哈希表可以容纳多少个键值对。 -
used
属性记录了哈希表目前已有节点(键值对)的数量。它表示当前哈希表中存储的键值对数量。 -
sizemask
属性的值总是等于size - 1
。这个属性和哈希值一起决定了一个键应该被放到table
数组的哪个索引位置上。哈希值与sizemask
进行按位与操作,以确定键在数组中的位置
b. 哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对.
public class DictEntry {
// 键
public Object key;
// 值,使用 Object 类型来兼容不同数据类型
public Object value;
// 指向下一个哈希表节点,形成链表
public DictEntry next;
}
key
属性保存键值对中的键。v
属性用一个联合体 (union
) 存储键值对中的值,这个值可以是指针,也可以是uint64_t
整数,或者是int64_t
整数。next
属性是一个指向下一个哈希表节点的指针,它用于解决键冲突,将多个哈希值相同的键值对连接在一起,形成链表结构。
这个结构体允许哈希表存储不同数据类型的值,同时解决了哈希冲突的问题,确保具有相同哈希值的键值对能够正确地存储和检索。
c. 字典
public class Dict {
// 类型特定函数
public DictType type;
// 私有数据
public Object privdata;
// 哈希表数组
public DictHt[] ht;
// rehash 索引
// 当 rehash 不在进行时,值为 -1
public int rehashidx;
}
public class DictType {
// 计算哈希值的函数
public interface HashFunction {
int hashFunction(Object key);
}
// 复制键的函数
public interface KeyDup {
Object keyDup(Object privdata, Object key);
}
// 复制值的函数
public interface ValDup {
Object valDup(Object privdata, Object obj);
}
// 对比键的函数
public interface KeyCompare {
int keyCompare(Object privdata, Object key1, Object key2);
}
// 销毁键的函数
public interface KeyDestructor {
void keyDestructor(Object privdata, Object key);
}
// 销毁值的函数
public interface ValDestructor {
void valDestructor(Object privdata, Object obj);
}
// 计算哈希值的函数
public HashFunction hashFunction;
// 复制键的函数
public KeyDup keyDup;
// 复制值的函数
public ValDup valDup;
// 对比键的函数
public KeyCompare keyCompare;
// 销毁键的函数
public KeyDestructor keyDestructor;
// 销毁值的函数
public ValDestructor valDestructor;
}
-
type
属性是一个指向dictType
结构的指针,用于定义针对特定类型键值对的操作函数。这允许 Redis 创建多态字典,以便应对不同类型的数据。 -
privdata
属性保存了传递给类型特定函数的可选参数,这些函数在处理键值对时可能需要使用。 -
ht
属性是一个包含两个dictht
哈希表的数组,通常只使用ht[0]
哈希表。ht[1]
哈希表主要用于在进行哈希表的rehash
操作时暂时存储数据。 -
rehashidx
属性用于跟踪哈希表的rehash
进度,如果没有进行rehash
,其值为 -1。
2. 哈希算法
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
3. 键冲突
-
当多个键被分配到哈希表数组的相同索引位置时,发生了键冲突。
-
Redis使用链地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个
next
指针,多个节点可以用next
指针构成一个单向链表。 -
处理冲突的方法是将新的节点添加到链表的表头位置,通过
next
指针将多个节点连接成一个链表。这样,最新添加的节点总是位于链表的开头。 -
这种方式使得在解决冲突时的操作复杂度为 O(1),因为无论链表有多长,都可以直接将新节点插入到表头位置,而不需要遍历整个链表。
四. 跳跃表
跳跃表(Skip List)是一种数据结构,用于存储一组有序的元素,支持快速的插入、删除和查找操作。跳跃表的特点在于它通过多层次的链表结构来实现快速查找,类似于多层索引,这使得在跳跃表中进行查找操作的时间复杂度保持在 O(log n) 级别。
跳跃表的核心思想是通过添加多级索引来加速查找。具体而言,跳跃表由多个层级组成,每个层级都是一个有序的链表,其中最底层包含所有的元素。上层的链表是下层链表的子集,用于跳过一些元素,从而快速定位到目标元素。