
非关系型基于内存的KV数据库
Redis对外主要提供了5种数据结构,分别是string、hash、list、set、sorted set
并且对于每一种数据结构redis都提供了至少2种相应的内部编码实现以应对不同的使用场景。

零、Redis中几种数据结构
- redisDb: redisDb 是 redis 数据库的抽象,定义在 server.h 中,比较关键的属性如下
typedef struct redisDb {
dict *dict; /* 键值对字典,保存数据库中所有的键值对 */
dict *expires; /* 过期字典,保存着设置过期的键和键的过期时间*/
dict *blocking_keys; /*保存着 所有造成客户端阻塞的键和被阻塞的客户端 (BLPOP) */
dict *ready_keys; /* 保存着 处于阻塞状态的键,value为NULL*/
dict *watched_keys; /* 事物模块,用于保存被WATCH命令所监控的键 */
// 当内存不足时,Redis会根据LRU算法回收一部分键所占的空间,而该eviction_pool是一个长为16数组,保存可能被回收的键
// eviction_pool中所有键按照idle空转时间,从小到大排序,每次回收空转时间最长的键
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* 数据库ID */
long long avg_ttl; /* 键的平均过期时间 */
} redisDb;
- dict 是 redis 中的字典,定义在 dict.h 文件中,其主要的属性如下
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2]; //方便渐进的rehash扩容,dict的hashtable
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
- dictht 是哈希表结构,定义在 dict.h 文件中,其重要的属性如下:
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
- dictEntry 是 redis 中的** key-value 键值对节点,是实际存储数据的节点**,定义在 dict.h 文件中,其重要的属性如下: *key: 键对象,总是一个字符串类型的对象 SDS *val: 值对象,可能是任意类型的对象。对应常见的5种数据类型:string,hash,list,set,zset *next: 尾指针,指向下一个节点
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
- redisObject: Redis 数据库中所有数据都以 key-value 节点 dictEntry 存储,其中 key 和 value 都是一个 redisObject 结构体对象,只不过 key 总是一个字符串类型的对象(SDS),value 则可能是任意一种数据类型的对象。 redisObject 结构体定义在 server.h 中如下所示
typedef struct redisObject {
unsigned type:4; //占用4bit
unsigned encoding:4; //占用4bit
unsigned lru:LRU_BITS; /*占用24bit LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount; //占用4byte
void *ptr; //占用8byte 总空间:4bit+4bit+24bit+4byte+8byte = 16byte
} robj;
参考文章 https://www.jianshu.com/p/e667a9b36566
一、redis支持的数据结构
-
STRING 字符串 Simple dynamic string(SDS)的数据结构
image.png
- 二进制安全数据结构 可以存储二进制数据,不用管字符串是否包含/0字符
- 动态扩容机制 避免添加字符时多次分配空间(如果空闲空间小于要添加的字符串t的长度,则需要扩容。如果目前的字符串小于1M,则直接扩容双倍,如果目前的字符串大于1M,则直接添加1M。)
- 兼容C语言函数库
- LIST 链表
Redis中的列表list,在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的,但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。
- List是一个有序(按加入时顺序排序)的数据结构,quickList和zipList作为底层实现
- 可以通过每个zipList最大容量,quickList数据压缩范围,提升数据存取效率。
- lpush alist a b c d
- lrange alist 0 -1 会返回 d c b a
- ziplist是一个特殊的双向链表,没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方,牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度 更费内存。这是典型的“时间换空间”,但是存在连锁更新问题

- quickList,是ziplist和linkedlist二者的结合,quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据

- SET 集合
当set中的元素value全是整数值时,且set中数据个数不超过512时使用intset来存储,其他情况都是用字典dict来存储。
- set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。
- 设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。
- ZSET 有序集合
//操作
ZADD price 8.5 apple 5.0 banana 6.0 cherry
- 有序集合的编码可以是 ziplist 或者 skiplist
- ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。
编码转换: 当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:保存的元素数量小于128;保存的所有元素长度都小于64字节。
typedef struct zset{
//跳跃表
zskiplist *zsl;
//字典
dict *dice;
} zset;

- Redis Zrank 返回有序集中指定成员的排名。 zrank key member
- zrange key 0 9 返回指定索引区间元素
- HASH 散列
-
编码可以是 ziplist 或者 dict字典,所有键值对的键和值的字符串长度都小于64,键值对数量小于512采用ziplist。
image.png dict 字典,哈希对象中的每个 key-value 对都使用一个字典键值对dictEntry来保存,字典的键和值都是字符串对象
二、应用场景
- 分布式锁
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
EX seconds − 设置指定的到期时间(以秒为单位)。PX milliseconds - 设置指定的到期时间(以毫秒为单位)。NX - 仅在键不存在时设置键。XX - 只有在键已存在时才设置。
- 加锁:set lock mid-pid ex 5 nx
如果不加过期时间,获得锁的线程在解锁之前挂掉了,锁就永远释放不掉了
setnex key value
expire key expireTime - 解锁
解锁前要判断自己是不是获得了锁,有可能在业务执行期间锁已经过期了,误删除了别的线程的锁 使用Lua脚本,保证操作的原子性
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
三、redis持久化和高可用
- redis复制
SYNC完整重同步和PSYNC部分重同步(用于断线重连场景)
过程: 同步 + 命令传播
- 从节点向主节点发送sync命令
- 主节点执行BGSAVE命令生成rdb文件,并记录写命令到缓冲区
- rdb文件生成发给从节点,从节点载入rdb文件,
- 主节点发送缓冲区写命令,从节点接收并执行
- 进入命令传播阶段,主服务器会向从服务器发送写命令
部分重同步:
主服务器复制偏移量、从服务器复制偏移量、主服务器复制积压缓冲区(由主服务器维护的固定长度的FIFO队列)
- 从服务器断线重连上主服务器之后,会通过PSYNC命令将自己的复制偏移量offset传给主服务器,若offset已不再复制积压缓冲区,则执行完整重同步,否则执行部分重同步
心跳检测:检测网络连接状态,检测命令丢失(复制积压缓冲区)
在命令传播阶段,从服务器会以每秒一次的频率,向主服务器发送命令:
REPLICATION ACK <replication_offset>
- 持久化
RDB持久化:通过保存内存快照来记录数据库状态;SAVE(阻塞) BGSAVE(会创建子进程生成rdb文件)
(1)可手动执行、可根据服务器配置定期执行(SAVE 900 1) 900s内有一次写命令
(2)可用rdb文件还原数据库(比AOF快)AOF持久化:
通过保存Redis服务器执行的写命令来记录数据库状态
同步频率(效率和安全性平衡):(1)每条写命令 (2)每秒 (3)由操作系统控制(缓冲区写到磁盘)-
AOF文件重写:通过记录一条写指令来记录数据库key状态,来减小AOF文件大小
image.png 不管是RDB还是AOF都是先写入一个临时文件,然后通过 rename 完成文件的替换工作。
如果开启了AOF持久化则优先使用AOF文件来恢复数据库状态

- 哨兵机制
- Redis高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主从服务器,在主服务器进入下线状态时,从该主服务器的从服务器中选择一个没有断开连接的、优先级高的、复制偏移量最大的作为主服务器
- 主观下线:某一个Sentinel实例认为某一个redis服务器下线
- 客观下线:超过一定数量的Sentinel实例认为某一个redis服务器下线
-
故障转移过程:当一个主服务器被判断为客观下线时,各个Sentinel实例会协商选出一个领头Sentinel,来对下线主服务器进行故障转移过程。
image.png
- 集群方案
Redis提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能
节点:
一个Redis集群有多个节点组成,一个节点就是运行在Redis集群模式下的Redis服务器
槽:
- Redis集群通过分片的方式来保存数据库中的键值对,集群数据库被分为16384个槽,数据库中每个键都属于这个槽中的一个,集群中的每个节点可以处理0~16384个槽。
- 当每个槽都被处理时,集群处于上线状态,任何一个槽不被处理时,节点处于下线状态。
- 在集群中执行命令:客户端向集群发送与数据库键相关的命令时,收到命令的节点首先会CRC16(key) & (16384-1) 计算出key属于哪个槽,如果这个槽指派给了自己,当前节点直接执行命令,否则向客户端返回一个MOVED指令,指引客户端转向正确的节点
- 重新分片:将任意数量已经指派给某个节点的槽改为指派给另外一个节点,相关槽所属的键值对也会从源节点迁移到目标节点,且重新分片过程可以在线进行
ASK错误:用户访问槽迁移过程中且已被迁移到目标节点的数据库键 -
故障转移:主节点下线时,主节点对应的从节点广播FAILOVER_AUTH_REQUEST信息,集群中其他主节点投票选择新的主节点,超过半数后变成新Master。
image.png
四、参考文章
https://juejin.im/post/6844904198019137550#heading-7
五、REDIS为什么这么快
- IO多路复用
- 单线程处理,避免上下问切换带来的资源损耗
- 纯内存操作
六、REDIS线程模型

七、Redis和Memcached对比
- Redis支持更丰富的数据结构,能支持更丰富的操作
- 在 redis3.x 版本中,便能支持 Cluster 模式,而 Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
- 由于 Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis
八、常用命令
- keys pattern 查找所有符合给定模式pattern(正则表达式)的 key
九、过期策略
- 惰性删除
- 定期删除
十、内存淘汰策略(由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除)
- noeviction: 返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
- allkeys-lru: 全部键空间LRU
- volatile-lru: 过期监控件LRU
- allkeys-random: 全部键空间随机回收
- volatile-random:过期键空间随机回收
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键
十一、REDIS事务 - 原子性 (多个命令要么全部执行,要么都不执行)
- MULTI(开启事务)、EXEC(提交事务)
- WATCH 在exec之前,watch的key发生变化,事务就会abort
- Redis事务提供了将一个或多个命令打包执行的能够力,且命令执行时不会被打断
十二、Pipeline
- Redis Pipeline(管道)是一种用于优化多个命令批量执行的机制。通过使用管道,客户端可以将多个命令一次性发送给Redis服务器,而无需等待每个命令的响应,从而减少了网络通信的延迟和开销。
- 当需要进行大批量的写操作或读取大量数据时,使用管道可以极大地提升性能。
十三、script - 原子性
- REDIS事务可以做的事情,script也可以实现
- EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3




