Redis
Table of Contents
1 Redis 特性
- 速度快
- 内存数据库
- I/O 多路复用
- 协议简单
- C 语言开发
- 键值对的数据结构服务器
- key 永远是 String
- value 可以是多种类型, 可以通过
object encoding <key>
来查看
- 丰富的功能
- 简单稳定
- 主线程是单线程
- 持久化
- RDB (Redis DataBase)
- AOF (Append-Only File)
- 主从复制
- 高可用和分布式转移
- 客户端语言多
2 Redis 使用场景
- 缓存数据库
- 排行榜
- 计数器
- 社交网络
- 消息队列
- 分布式锁
3 安装及使用
下载并安装
wget https://download.redis.io/releases/redis-6.2.5.tar.gz
tar xzf redis-6.2.5.tar.gz
cd redis-6.2.5
make
启动 Redis Server
src/redis-server
客户端连接 Redis
$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"
4 内部数据结构
4.1 简单动态字符串 (Simple Dynamic String)
sds 是对 C 语言字符串的扩展, 除了记录底层字符串,还缓存了当前长度
// sds 的实现结构体不止一个,但是定义的方式类似,只是操作系统位数不一致 // 下面以 sdshdr64 为例展示 struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
- Redis 的字符串是 二进制安全
- 对比 C 语言字符串,提供了如下的特性
- 高效计算字符串长度
strlen
- 高效追加字符串
append
- 高效计算字符串长度
- 代码实现在
sds.h
和sds.c
两个文件中
4.2 双端链表 (Doubly Linked List)
- 代码实现在
adlist.h
和adlist.c
两个文件中 该数据结构的定义如下
typedef struct listNode { // 记录链表节点 struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct listIter { // 记录链表的迭代器 listNode *next; int direction; } listIter; typedef struct list { // 链表的数据结构 listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); // 记录链表的空闲指针 int (*match)(void *ptr, void *key); // 匹配的函数指针 unsigned long len; // 缓存链表长度 } list;
4.3 字典 (Hash Table)
- 代码实现在
dict.h
和dict.c
两个文件中 - 字典的实现在 Redis 中有两个作用
- 实现数据库键空间 (Key Space), 例如:
DBSIZE
,FLUSHDB
,RANDOMKEY
- 用作 Hash 类型键的底层实现之一
- 实现数据库键空间 (Key Space), 例如:
该数据结构的定义如下
typedef struct dictEntry { // 字典项定义 void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; // 指向相应数据项 } dictEntry; typedef struct dictType { uint64_t (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); int (*expandAllowed)(size_t moreMem, double usedRatio); } dictType; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ // 每个字典的 entry 有两个 dictht, 主要目的是可以快速 rehash typedef struct dictht { dictEntry **table; // 哈希桶 bucket unsigned long size; // 指针数组大小 bucket size unsigned long sizemask; // 指针数组长度掩码,用于计算索引值 unsigned long used; // 哈希表现有节点数量 } dictht; typedef struct dict { // 字典数据类型的定义 dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */ } dict; /* If safe is set to 1 this is a safe iterator, that means, you can call * dictAdd, dictFind, and other functions against the dictionary even while * iterating. Otherwise it is a non safe iterator, and only dictNext() * should be called while iterating. */ typedef struct dictIterator { dict *d; long index; // 正在迭代的数组索引 int table, safe; // table 表示正在迭代的号码(0或1), safe=1, 表示迭代过程中修改哈希表 dictEntry *entry, *nextEntry; /* unsafe iterator fingerprint for misuse detection. */ long long fingerprint; } dictIterator; typedef void (dictScanFunction)(void *privdata, const dictEntry *de); typedef void (dictScanBucketFunction)(void *privdata, dictEntry **bucketref); /* This is the initial size of every hash table */ #define DICT_HT_INITIAL_SIZE 4
- 字典的每个项记录两个
dictht
ht[0]
是字典主要使用的哈希表,ht[1]
则只有在程序对ht[0]
进行 rehash 时才使用
dictCreate()
创建新的字典dictCreate()
创建了dict
和两个dictht
dictCreate()
并没有给dictht
的table
分配空间
dictAdd()
表示向字典中添加dictEntry
数据- 该过程可能触发 rehash
- rehash 可以是扩展数据,也可能是收缩数据 (shrink)
- rehash 使用
ht[1]
作为中转节点 - 为了 hash 效率, Redis 采用渐进式哈希
_dictRehashStep
用于对数据库字典、以及哈希键的字典进行被动 rehashdictRehashMilliseconds
则由 Redis 服务器常规任务程序执行,用于对数据 库字典进行主动 rehash
- 该过程可能触发 rehash
- 字典使用 链地址 解决哈希冲突
4.4 跳跃表 (Skip List)
- 跳跃表主要使用场景就是 有序集合 zset
- 包含的元素数量比较多
- 元素成员 (member) 是比较长的字符串
- 跳跃表以有序的方式在 层次化 的链表中保存元素, 效率和平衡树媲美
- 查找、删除、添加等操作都可以在 O(logN) 期望时间下完成
- 比起平衡树来说, 跳跃表的实现要简单直观得多
- 跳跃表的构成部分
- 表头 (head): 负责维护跳跃表的节点指针
- 跳跃表节点: 保存着元素值,以及多个层
- 层: 保存着指向其他元素的指针
- 高层的指针越过的元素数量大于等于低层的指针
- 为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小, 慢慢降低层次
- 表尾: 全部由 NULL 组成,表示跳跃表的末尾
跳跃表的定义位于
server.h
中/* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward; // 后退指针 struct zskiplistLevel { // 层 struct zskiplistNode *forward; // 前进指针 unsigned long span; // 该层跨域的节点数量 } level[]; // level[] 在 Redis 中是 1 到 32 的随机数 } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; // 头节点和尾节点 unsigned long length; // 节点数量 int level; // 目前表内节点的最大层数 } zskiplist;
- Redis 在 William Pugh 关于 Skip List 的设计中进行修改,满足自身查询的需求
- score 值可重复
- 对比一个元素需要同时检查它的 score 和 member
- 每个节点带有高度为 1 层的后退指针,用于从表尾方向向表头方向迭代
5 内存映射数据结构
5.1 整数集合 (intset)
- 用于有序、无重复地保存多个整数值,根据元素的值,自动选择该用什么长度的整数 类型来保存元素
- intset 的应用范围如下
- 只保存着整数元素
- 元素的数量不多
intset 的实习见
intset.c
和intset.h
中typedef struct intset { uint32_t encoding; // 保存所使用类型的长度 uint32_t length; // 元素个数 int8_t contents[]; // 元素 } intset;
contents
数组中的整数位数提升时,需要对整数进行扩充操作,这个扩充过程叫 做 升级 (upgrade)contents
数组不支持降级操作,一旦升级过后即使最大位数的数字被删除了,依 旧使用保持最大位数
5.2 压缩列表 (ziplist)
- ziplist 是由一系列特殊编码的内存块构成的列表
- 一个 ziplist 可以包含多个节点 (entry)
- 每个节点可以保存一个长度受限的字符数组 (不以
\0
结尾的 char 数组) 或者 整数
- 哈希键、列表键和有序集合键初始化的底层实现皆采用 ziplist
- 添加和删除 ziplist 节点有可能会引起连锁更新,因此,添加和删除操作的最坏复
杂度为
O(N^2)
ziplist 的典型结构如下,其中存储数据的是 entries
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->| size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte +---------+--------+-------+--------+--------+--------+--------+-------+ component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend | +---------+--------+-------+--------+--------+--------+--------+-------+ ^ ^ ^ address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END | ZIPLIST_ENTRY_TAIL
ziplist 的 entry 如下
area |<------------------- entry -------------------->| +------------------+----------+--------+---------+ component | pre_entry_length | encoding | length | content | +------------------+----------+--------+---------+
6 数据类型
6.1 对象处理机制
对象的类型定义位于
redisObject
结构体typedef struct redisObject { unsigned type:4; // 类型:字符串,哈希表,列表等等 unsigned encoding:4; // 编码格式:sds, int, hashtable, zipmap, list, ziplist, skiplist unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; // 引用计数 void *ptr; } robj;
- 对象的内存释放方式是使用引用计数
- 对象初始创建时
refcount
的值被设置成 1 - 当
refcount
降至 0 后,该redisObject
将会被释放
- 对象初始创建时
6.2 字符串 String
- 字符串默认有如下两种类型
REDIS_ENCODING_INT
使用long
类型来保存long
类型值REDIS_ENCODING_RAW
则使用sdshdr
结构来如下类型的值sds
(也即是char*
)long long
double
long double
- 二进制安全, 可以存储图片和序列化对象, 最多可存储 512M 的数据
GET
,SET
,DEL
, 操作单个字符串键的指令MSET
同时设置多个键INCR
和DECR
STRLEN
,APPEND
6.3 哈希表 Hash
- 哈希表使用如下两种编码格式
REDIS_ENCODING_ZIPLIST
- 键和值一同推入压缩列表
- 形成保存哈希表所需的键-值对结构
REDIS_ENCODING_HT
当出现如下条件时, ziplist 转 hashtable- 哈希表中某个键或某个值的长度大于
server.hash_max_ziplist_value
, 默认 值为 64 - 压缩列表中的节点数量大于
server.hash_max_ziplist_entries
, 默认值为 512
- 哈希表中某个键或某个值的长度大于
- 操作哈希表的指令
HMSET
,HGET
和HGETALL
- 物理存储会在 ziplist 和 hashtable 两种类型中转换
- ziplist 比 hashtable 节约空间,默认使用 ziplist
- 当有需要时,Redis 会自动将 ziplist 转成 hashtable
6.4 列表 List
- 列表使用如下两种编码格式
REDIS_ENCODING_ZIPLIST
- 将值添加到压缩列表中
REDIS_ENCODING_LINKEDLIST
当出现如下条件时, ziplist 转 linkedlist- 哈希表中某个键或某个值的长度大于
server.list_max_ziplist_value
, 默认 值为 64 - 压缩列表中的节点数量大于
server.list_max_ziplist_entries
, 默认值为 512
- 哈希表中某个键或某个值的长度大于
- 列表是一个双向链表, 最多存储
2^32-1
个元素 - 常见列表操作
LPUSH
,LPOP
,RPUSH
和RPOP
- 范围查找
LRANGE key 0 10
- 计算列表长度
LLEN
- 列表可能会发生阻塞,
BLPOP
,BRPOP
和BRPOPLPUSH
6.5 集合 Set
- 集合使用如下两种编码格式
REDIS_ENCODING_INTSET
- 如果集合的第一个元素是
long long
类型,则初始化成该类型
- 如果集合的第一个元素是
REDIS_ENCODING_HT
当出现如下条件时, intset 转 hashtable- 哈希表中某个键或某个值的长度大于
server.set_max_ziplist_value
, 默认 值为 64 - 压缩列表中的节点数量大于
server.set_max_ziplist_entries
, 默认值为 512 - 字典编码时,对应的
dictEntry
只保存key
,value
和next
都为空
- 哈希表中某个键或某个值的长度大于
- 集合数据操作
SADD
,SREM
和SMEMBERS
- 随机获取成员
SRANDMEMBERS
- 集合交集和并集操作
SINTER
和SUNION
- 求差集
SDIFF
6.6 有序集合 Zset
- 有序集合使用如下两种编码格式
REDIS_ENCODING_ZIPLIST
- 每个有序集元素以两个相邻的 ziplist 节点表示
- 第一个节点保存元素的 member 域
- 第二个元素保存元素的 score 域
REDIS_ENCODING_SKIPLIST
当出现如下条件时, ziplist 转 skiplist- 服务器属性
server.zset_max_ziplist_entries
的值大于 0, 默认为 128 - 元素的 member 长度小于服务器属性
server.zset_max_ziplist_value
的值, 默认为 64 zset 同时使用 字典 和 跳跃表 两个数据结构来保存有序集元素
typedef struct zset { dict *dict; zskiplist *zsl; } zset;
- 字典和跳跃表两个结构通过将指针共同指向这两个值来节约空间
- 服务器属性
- 关于 zset 的两种定义的有序
- 字典结构 dict
- 检查给定 member 是否存在于有序集(被很多底层函数使用)
- 取出 member 对应的 score 值 (实现
ZSCORE
命令)
- 跳跃表结构 skiplist
- O(logN) 时间复杂度内根据 score 对 member 进行定位(被很多底层函数使用)
- 范围查找和处理操作,例如
ZRANGE
,ZRANK
和ZINTERSTORE
- 字典结构 dict
- 有序的集合类型, 键是不可重复的
ZADD
和ZCOUNT
7 使用示例
7.1 整数操作
Redis 的整数和字符串都是存在 string 类型中
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set hello 'world' OK 127.0.0.1:6379> keys * 1) "hello" 127.0.0.1:6379> get hello "world" 127.0.0.1:6379> append hello '!' (integer) 6 127.0.0.1:6379> get hello "world!"
7.2 计数器
使用一个整数 string 类型的 count 变量值来计数
127.0.0.1:6379> set count 1 OK 127.0.0.1:6379> incr count (integer) 2 127.0.0.1:6379> get count "2" 127.0.0.1:6379> incr count (integer) 3 127.0.0.1:6379>
7.3 哈希使用
常见哈希表的操作如下
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> hmset user name zhang age 12 OK 127.0.0.1:6379> hgetall user 1) "name" 2) "zhang" 3) "age" 4) "12"
7.4 集合操作
集合的操作如下,包括对集合的基本操作
127.0.0.1:6379> sadd course math (integer) 1 127.0.0.1:6379> sadd course chinese (integer) 1 127.0.0.1:6379> object encoding course "hashtable" 127.0.0.1:6379> smembers course 1) "math" 2) "chinese"
7.5 获取对象存储类型
- 使用
object encoding
查看对象的底层数据类型 - 使用
type
来查看 key 对应的数据结构类型
127.0.0.1:6379> keys * 1) "a" 2) "str1" 127.0.0.1:6379> object encoding a "quicklist" 127.0.0.1:6379> object encoding str1 "embstr" 127.0.0.1:6379> type str1 string
8 Redis 缓存相关问题
- 缓存穿透 指查询一个数据库中不存在的数据
- 容易产生对数据库过大的访问压力导致数据库崩溃
- 解决方案: 设置缓存过期时间, 即使未查到时设置一个空值
- 缓存雪崩 指在某一个时间段, 缓存集中失效
- 对数据库产生周期性的压力
- 解决方案: 对冷数据, 超时设置短一点, 对热数据, 超时设置长一点
- 缓存击穿 指一个非常热的 Key, 在失效的瞬间大并发量把数据库击穿
- 瞬间并发量高, 直接把数据库打崩溃掉
- 解决方案: 对于特别大的热 Key, 可以设置永不过期