Redis内存模型原理
字符串
Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。
它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为
Redis的默认字符串表示。
SDS 定义:
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
- len 保存了SDS保存字符串的长度
- buf[] 数组用来保存字符串的每个元素
- free j记录了 buf 数组中未使用的字节数量
SDS 在 C 字符串的基础上加入了 free 和 len 字段,带来了很多好处:
- 获取字符串长度:SDS 是 O(1),C 字符串是 O(n)。
缓冲区溢出:使用 C 字符串的 API 时,如果字符串长度增加(如 strcat 操作)而忘记重新分配内存,很容易造成缓冲区的溢出。 - 而 SDS 由于记录了长度,相应的 API 在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
- 修改字符串时内存的重分配:对于 C 字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
而对于 SDS,由于可以记录 len 和 free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化。 - 空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
- 存取二进制数据:SDS 可以,C 字符串不可以。因为 C 字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等)。
- 内容可能包括空字符串,因此 C 字符串无法正确存取;而 SDS 以字符串长度 len 来作为字符串结束标识,因此没有这个问题。
- 此外,由于 SDS 中的 buf 仍然使用了 C 字符串(即以’\0’结尾),因此 SDS 可以使用 C 字符串库中的部分函数。
- 但是需要注意的是,只有当 SDS 用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)。
链表
链表在Redis中的应用非常广泛,列表(List)的底层实现之一就是双向链表。此外发布与订阅、慢查询、
监视器等功能也用到了链表。
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
} listNode
typedef struct list {
//表头节点
listNode.head;
//表尾节点
listNode.tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void *(*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
} list;
Redis链表优势:
- 双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
与传统链表(单链表)相比,Redis链表结构的优势有:
普通链表(单链表):节点类保留下一节点的引用。链表类只保留头节点的引用,只能从头节点插入删
除 - 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL
结束。 - 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
- 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
字典
字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。
字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。
Redis 的字典使用哈希表作为底层实现。
哈希(作为一种数据结构),不仅是 Redis 对外提供的 5 种对象类型的一种(hash),也是 Redis 作
为 Key-Value 数据库所使用的数据结构。
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht
/*哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,
dictEntry 结构定义如下:
*/
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry
跳表(zset)
普通单链表查询一个元素的时间复杂度为O(n),即使该单链表是有序的。
查找46 : 55—21—55–37–55–46
typedef struct zskiplistNode {
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode
//链表
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
} zskiplist;
- 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下
找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节
点,如果找到则返回,反之则返回空。 - 插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反
面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层
到k层。 - 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩
下头尾两个节点,则删除这一层。
缓存淘汰策略
最大缓存
- 在 redis 中,允许用户设置最大使用内存大小maxmemory,默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。
- redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
淘汰策略
- redis淘汰策略配置:maxmemory-policy voltile-lru,支持热配置
redis 提供 6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
LRU原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心
思想是“如果数据最近被访问过,那么将来被访问的机率也更高”。
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
在Java中可以使用LinkHashMap去实现LRU
事务
事务中命令按顺序执行,中途有命令出错后续命令仍执行,如果是语法错误则事务无法提交。
-
Redis事务没有隔离级别:
Redis事务执行命令会放入队列中,事务未提交时不会被执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。 -
Redis不保证原子性:
Redis中单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Hello World"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "java" "go" "c"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Hello World"
3) (integer) 3
4) 1) "c"
2) "go"
3) "java"
WATCH机制(乐观锁)
watch变量,并开启事务,如果该变量被修改那么事务无法执行,否则成功执行。
-
初始化信用卡可用余额和欠额
-
用watch监控,进行数据监控,事务成功执行
-
监控过程中,他人纂改,事务无法执行