
KEYS pattern 查找所有符合给定模式 pattern 的 key,其中 * 匹配零个或多个字符,? 匹配一个字符。DEL key [key ...] 删除给定的一个或多个 key。EXISTS key 检查给定 key 是否存在。EXPIRE key seconds 为给定 key 设置生存时间,生存时间为 0 时该 key 过期,它会被自动删除。TTL key 以秒为单位,返回给定 key 的剩余生存时间。SET key value [EX seconds] [PX milliseconds] [NX|XX] 将字符串值 value 关联到 key。如果 key 已经持有其他值,SET 就覆写旧值,无视类型。对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。GET key 返回 key 所关联的字符串值。如果 key 不存在那么返回特殊值 nil。如果 key 储存的值不是字符串类型,返回一个错误,因为 GET 只能用于处理字符串值。MSET key value [key value ...] 同时设置一个或多个 key-value 对。MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置。MGET key [key ...] 返回一个或多个给定 key 的值,如果给定的 key 里面有某个 key 不存在,那么这个 key 返回 nil。INCR key 将 key 中储存的字符串解释为十进制 64 位有符号整数并加一。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行 INCR 操作。如果值包含错误的类型,或字符串类型的值不能解释为数字,那么返回一个错误。INCRBY key increment 将 key 所储存的值加上增量 increment,具体实现与 INCR 类似。HINCRBYFLOAT key field increment 将 key 所储存的值加上浮点数增量 increment,INCRBYFLOAT 的计算结果最多只能表示小数点的后十七位。SETNX key value 即 set if not exists,当且仅当 key 不存在时将 key 的值设为 value,若给定的 key 已经存在,则 SETNX 不做任何动作。SETEX key seconds value 即 set with expire,将值 value 关联到 key,并将 key 的生存时间设为 seconds。如果 key 已经存在, SETEX 命令将覆写旧值。HSET key field value 将哈希表 key 中的域 field 的值设为 value。如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。如果域 field 已经存在于哈希表中,旧值将被覆盖。HGET key field 返回哈希表 key 中给定域 field 的值。HMSET key field value [field value ...] 同时将多个 field-value 对设置到哈希表 key 中。HMGET key field [field ...] 返回哈希表 key 中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个 nil 值。HGETALL key 返回哈希表 key 中所有的域和值。HKEYS key 返回哈希表 key 中的所有域。HVALS key 返回哈希表 key 中所有域的值。HINCRBY key field increment 为哈希表 key 中的域 field 的值加上增量 increment。增量也可以为负数,相当于对给定域进行减法操作。HSETNX key field value 将哈希表 key 中的域 field 的值设置为 value,当且仅当域 field 不存在。LPUSH key value [value ...] 将一个或多个值 value 依次插入到列表 key 的表头(左侧)。LPOP key 移除并返回列表 key 的头元素。RPUSH key value [value ...] 将一个或多个值 value 依次插入到列表 key 的表尾(右侧)。RPOP key 移除并返回列表 key 的尾元素。LRANGE key start stop 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。BLPOP key [key ...] timeout 是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。BRPOP key [key ...] timeout 是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。SADD key member [member ...] 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。SREM key member [member ...] 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。SCARD key 返回集合 key 中元素的数量。SISMEMBER key member 判断 member 元素是否集合 key 的成员。SMEMBERS key 返回集合 key 中的所有成员,不存在的 key 被视为空集合。SINTER key [key ...] 返回一个集合的全部成员,该集合是所有给定集合的交集。SDIFF key [key ...] 返回一个集合的全部成员,该集合是所有给定集合之间的差集。SUNION key [key ...] 返回一个集合的全部成员,该集合是所有给定集合的并集。ZADD key score member [[score member] [score member] ...] 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。score 值可以是整数值或双精度浮点数。ZREM key member [member ...] 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。ZSCORE key member 返回有序集 key 中,成员 member 的 score 值。如果 member 元素不是有序集 key 的成员或 key 不存在,返回 nil。ZRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。ZCARD key 返回有序集 key 的基数。ZCOUNT key min max 返回有序集 key 中,score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max)的成员的数量。ZINCRBY key increment member 为有序集 key 的成员 member 的 score 值加上增量 increment,increment 可以为负。ZRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。其中成员的位置按 score 值递增来排序。ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间的成员。在并发场景下,如果只有读操作并不会发生数据不一致,但如果存在写操作,不管是先更新数据库还是先更新缓存,只要两次更新操作中夹杂了其他线程的完整更新操作都会导致最终的数据不一致。举两个例子来说就是 线程 A 更新缓存 -> 线程 B 更新缓存 -> 线程 B 更新数据库 -> 线程 A 更新数据库 和 线程 A 更新数据库 -> 线程 B 更新数据库 -> 线程 B 更新缓存 -> 线程 A 更新缓存,以上两种情况下,线程 B 的完整更新均使得线程 A 的更新成为了部分更新,因此发生了最终的数据不一致。
旁路缓存策略(Cache Aside)能在一定程度上解决数据不一致的问题,该策略主要分为读策略和写策略两个部分:
对于写策略,主要有两个注意事项:
线程 A 更新数据库 -> 线程 B 更新数据库 -> 线程 B 更新缓存(删除) -> 线程 A 更新缓存(删除) 的数据不一致问题。
基于旁路缓存策略,可以通过加锁将更新数据库和删除缓存整合为一个原子操作从而保证强一致性,但很明显这会影响性能。同时,也可以在写入缓存时指定 TTL,这样哪怕发生数据不一致也能在缓存失效后得到解决。
此外,如果采用先删除缓存再更新数据库,通过延迟双删(在更新数据库后延迟一定时间再删除一次缓存)也可以在一定程度上解决数据不一致的问题。因此,任何一种缓存更新方案都不能说是绝对的最优解,具体用哪种方案就要看对性能和一致性的取舍了。
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,所有的请求都会访问数据库。
常见的解决方案有三种:
缓存雪崩是指在同一时段大量的缓存数据同时失效或者 Redis 故障宕机,导致大量请求到达数据库,为数据库带来巨大压力。
解决大量的缓存数据同时失效:
解决 Redis 故障宕机:
缓存击穿也叫热点 key 问题,是指一个被高并发访问并且缓存重建业务较为复杂的 key 突然失效了,无数的请求同时访问数据库并重建缓存。
常见的解决方案有三种:
RDB(Redis Database Backup)全称 Redis 数据库备份,也被称为 Redis 数据快照。它的主要作用是将内存中的所有数据保存到磁盘上的 RDB 文件中。当 Redis 实例发生故障并需要重启时,可以从磁盘上的快照文件中读取数据并进行恢复。这种机制保证了数据的持久性和可靠性。
SAVE 和 BGSAVE 命令用于执行 RDB 操作。在 Redis 服务停止时,会自动执行一次 SAVE 操作。另一方面,BGSAVE 会根据配置文件中的 save 定时在后台执行。具体来说,BGSAVE 命令会创建一个子进程,从而实现后台的异步执行持久化操作。同时通过 fork 的写时复制策略,它可以避免父进程读操作与子进程写操作之间的冲突,确保数据的一致性。最终,子进程会用生成的新的 RDB 文件替换旧的 RDB 文件,实现持久化。
通过 RDB 实现持久化主要有两个缺点:
可以在 redis.conf 文件中进行 RDB 的相关配置:
# save 表示如果在 seconds 秒内发生了 changes 次修改就进行一次 RDB
save 900 1
save 300 10
save 60 10000
# 是否开启压缩,压缩能减少磁盘占用,但是会消耗 CPU
rdbcompression yes
# RDB 文件名称
dbfilename dump.rdb
# RDB 文件保存路径
dir /var/lib/redis
AOF(Append-Only File)全称为追加文件,是 Redis 的另一种持久化机制。与 RDB 不同,AOF 并不是将整个数据集保存到磁盘,而是记录所有写操作命令,以日志形式追加到文件中。AOF 文件包含了一系列 Redis 命令,这些命令按顺序记录了数据集的变化过程。当 Redis 重启时,它会重新执行 AOF 文件中的命令,以恢复数据到之前的状态。这种方式保证了数据的持久性,并且可以提供更精确的数据恢复,因为它记录了每个写操作。
因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。因此 Redis 提供了 BGREWRITEAOF 命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果。
AOF 默认是关闭的,可以在 redis.conf 文件中进行相关设置:
# 是否开启 AOF 功能,默认是 no
appendonly yes
# AOF 文件的名称
appendfilename "appendonly.aof"
# AOF 的记录频率
# 每执行一次写命令立即记录到 AOF 文件
appendfsync always
# 写命令执行完先放入 AOF 缓冲区,然后表示每隔 1 秒将缓冲区数据写到 AOF 文件,是默认方案
appendfsync everysec
# 写命令执行完先放入 AOF 缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
# AOF 文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF 文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
Redis 主从同步主要分为全量同步和增量同步。
全量同步一般发生在从节点第一次连接主节点时,它用于建立主节点与从节点之间的初始数据关系。全量同步期间,主节点会通过 RDB 文件发送完整的数据集给从节点,后续命令则记录在 repl_baklog,依次发送给从节点。
增量同步一般发生在从节点断线重连后,从节点会提交自己的 offset 到主节点,主节点会获取 repl_baklog 中 offset 之后的命令给从节点进行同步。
同步过程中,主节点主要通过比较各自的 replid 和 offset 以确定是进行全量同步还是增量同步,以及从哪个数据状态开始同步:
replid(replication id):replid 是主节点的一个标识符,id 一致则说明是同一数据集。每一个主都有唯一的 replid,从节点会继承主节点的 replid。
offset:offset 用于表示数据偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。从完成同步时也会记录当前同步的 offset。如果从节点的 offset 小于主的 offset,说明从节点数据落后于主节点,需要更新。

哨兵(Sentinel)的作用如下:
哨兵基于心跳机制监测服务状态,会每隔 1 秒向集群的每个实例发送 PING 命令:
quorum 的哨兵实例都认为该实例主观下线,则该实例客观下线。其中 quorum 的值最好超过哨兵实例数量的一半。一旦发现主节点故障,哨兵需要选定一个从节点作为新的主节点,选择依据如下:
down-after-milliseconds * 10)则会排除该从节点。slave-priority 值,越小优先级越高,如果是 0 则永不参与选举。slave-prority 一样,则判断从节点的 offset 值,越大说明数据越新,优先级越高。当选出一个新的主节点后,切换流程如下:
SLAVEOF NO ONE命令,让该节点成为主节点。SLAVEOF 主节点地址 主节点端口 命令,让这些从节点成为新的主节点的从节点,并开始从新的主节点上同步数据。SDS(Simple Dynamic Strings)是 Redis 中的一种字符串表示方式,它是 Redis 自定义的字符串类型,用于代替 C 语言中的原生字符串。
以下为 8 位版本的 SDS 结构体定义,开头 8 位 len 保存实际存储的字节数,接下来 8 位 alloc 保存实际申请的总的字节数,这二者都不包括头部及结束标识。接下来是 flags 用于标识 SDS 类型,因为 SDS 除了 8 位版本还有 5、16、32、64 几种类型,之所以要进行分类也是为了更合理地使用内存空间,同时由于总共只有 5 类,因此只有低 3 位是有效位。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
__attribute__ ((__packed__)) 是 GCC 编译器的扩展属性,它告诉编译器不要进行字节对齐。之所以不进行内存对齐是因为 SDS 结构体的指针不指向结构体开头的 len,而是位于 flags 和 buf[] 之间(会在 SDS 初始化时由 SDS 内部完成偏移),这样一方面指针减 1 即能取到 flags 的值,另一方面也使得 SDS 能够兼容 C 语言中的部分字符串函数。如果进行内存对齐,flags 后面会被进行内存填充,不仅浪费了内存,还导致无法直接访问 flags。
SDS 主要具有以下特点:
\0 在内任意二进制数据,这意味着它不仅可以保存字符串,还可以存储图片、序列化对象等各种数据。IntSet 是 Redis 中的一种专门用于存储整数值的数据结构,它是 Redis 的一个内部数据类型,能够保证元素唯一、有序。
以下为 IntSet 的结构体定义,其中 encoding 指定了编码方式,IntSet 支持存放 16 位、32 位、64 位三种整数编码方式,且支持升级操作,即当新的整数值无法用当前编码方式表示时,它会将整个数据集转换成更高位数的编码方式,这保证了 IntSet 的内部数据始终以最紧凑的方式存储。而为了方便查找,Redis 会将 IntSet 中所有的整数按照升序依次保存在 contents[] 中,并通过二分查找提高查询效率。
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
Dict 是 Redis 中用于存储键值对的数据结构,也被称为字典或哈希表,实现了键值对的快速存储和检索。
struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; /* Next entry in the same hash bucket. */
};
struct dict {
dictType *type; // 内置的不同哈希函数
dictEntry **ht_table[2]; // 两个二维 dictEntry 数组(指针指针数组),一个用于存储当前数据,另一个一般为空,只在 rehash 时用作临时存储
unsigned long ht_used[2]; // 标识每个二维 dictEntry 数组中的 dictEntry 个数
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* Keep small vars at end for optimal (minimal) struct padding */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
signed char ht_size_exp[2]; /* exponent of size. (size = 1<
};
Dict 还会根据负载因子动态扩容和收缩,并通过渐进式 rehash 来保证性能。渐进式 rehash 的流程如下:
ht_used[0] + 1 的
2
n
2^n
2n。ht_used[0] 的
2
n
2^n
2n,但不能小于 4。dict.ht_table[1] 申请内存空间。dict.rehashidx = 0,标识开始 rehash。dict.rehashidx 是否大于 -1,如果是则将 dict.ht_table[0][rehashidx] 的 entry 链表 rehash 到 dict.ht_table[1],并且将 rehashidx++。直至 dict.ht_table[0] 的所有数据都 rehash 到 dict.ht_table[1]。dict.ht_table[1] 赋值给 dict.ht_table[0],dict.ht_table[1] 置为空哈希表,释放原来的 dict.ht_table[0] 的内存。rehashidx 赋值为 -1,代表 rehash 结束。dict.ht_table[1],查询、修改和删除则会在 dict.ht_table[0] 和 dict.ht_table[1] 中依次查找并执行。这样可以确保 dict.ht_table[0] 的数据只减不增,随着 rehash 最终为空。ZipList 是一种特殊的双端链表,由一系列特殊编码的连续内存块组成。不支持随机存取,但是可以在任意一端进行压入与弹出操作。不过与普通的链表不同,ZipList 节点之间不是通过指针连接,而是通过在每个 entry 中记录上一节点和本节点的长度来寻址,因此内存占用较低,但如果列表数据过多,链表过长,可能会影响查询性能。此外,增或删较大数据时有可能发生连续更新问题。
ZipList 虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。因此,Redis 在 3.2 版本引入了新的数据结构 QuickList,它是一个双端链表,但链表中的每个节点都是一个 ZipList。
ZipList 结构如下:

| 属性 | 类型 | 长度 | 用途 |
|---|---|---|---|
| zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数。 |
| zltail | uint32_t | 4字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。 |
| zllen | uint16_t | 2字节 | 记录了压缩列表包含的节点数量。最大值为 UINT16_MAX(65534),如果超过这个值,此处会记录为 65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。 |
| entry | 列表节点 | 不固定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。 |
| zlend | uint8_t | 1字节 | 特殊值 0xFF(十进制 255),用于标记压缩列表的末端。 |
Redis 中的 SkipList(跳表)首先是双向链表,但所有元素按照 score 值升序存储,同时每个节点可能包含多个指针,每个指针跨度不同。
typedef struct zskiplistNode {
sds ele; // 节点存储的值
double score; // 节点的分数,主要用来排序和查找
struct zskiplistNode *backward; // 前一个节点的指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 下一个节点的指针
unsigned long span; // 索引跨度
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾节点指针
unsigned long length; // 节点数量
int level; // 最大的索引层级
} zskiplist;
SkipList 结构如下:

Redis 中所有的键和值等数据对象都会被封装为一个 RedisObject,它里面包含了数据类型、编码方式、值以及一些其他信息,以便 Redis 能够正确地管理和操作这些数据对象。
struct redisObject {
unsigned type:4; // 对象类型,分别是 string、hash、list、set 和 zset,占 4 位
unsigned encoding:4; // 底层编码方式,共有 11 种,占 4 位
unsigned lru:LRU_BITS; // 该对象最后一次被访问的时间,便于淘汰 key,低 8 位表示频率,高 16 位表示访问时间
int refcount; // 对象引用计数器,计数器为 0 表明无人引用,可以被回收
void *ptr; // 指向实际数据的指针
};
Redis 会根据存储的数据类型不同,选择不同的编码方式,累计 11 种:
| 编号 | 编码方式 | 说明 |
|---|---|---|
| 0 | OBJ_ENCODING_RAW | raw 编码动态字符串 |
| 1 | OBJ_ENCODING_INT | long 类型的整数的字符串 |
| 2 | OBJ_ENCODING_HT | hash 表(字典 dict) |
| 3 | OBJ_ENCODING_ZIPMAP | 已废弃 |
| 4 | OBJ_ENCODING_LINKEDLIST | 双端链表 |
| 5 | OBJ_ENCODING_ZIPLIST | 压缩列表 |
| 6 | OBJ_ENCODING_INTSET | 整数集合 |
| 7 | OBJ_ENCODING_SKIPLIST | 跳表 |
| 8 | OBJ_ENCODING_EMBSTR | embstr 的动态字符串 |
| 9 | OBJ_ENCODING_QUICKLIST | 快速列表 |
| 10 | OBJ_ENCODING_STREAM | stream 流 |
五大数据类型与编码方式的对应关系如下:
| 数据类型 | 编码方式 |
|---|---|
OBJ_STRING | INT、EMBSTR 和 RAW |
OBJ_LIST | LinkedList 和 QuickList(3.2以后) |
OBJ_SET | IntSet 和 HT |
OBJ_ZSET | ZipList、HT 和 SkipList |
OBJ_HASH | ZipList 和 HT |
String 的底层实现主要分以下三种情况:
LONG_MAX 范围内,则会采用 INT 编码,直接将数据保存在 RedisObject 的 ptr 指针位置(刚好 8 字节),不再需要 SDS 了。
在 3.2 版本之前,Redis 采用 ZipList 和 LinkedList 来实现 List,当元素数量小于 512 并且元素大小小于 64 字节时采用 ZipList 编码,超过则采用 LinkedList 编码。在3.2版本之后,Redis 统一采用 QuickList 来实现 List。

Set 不保证元素有序,但保证元素唯一,查询效率要求极高。为此,Set 默认采用 HT 编码,Dict 中的 key 用来存储元素,value 统一为 null。不过 Dict 内存并不连续,因此当存储的所有数据都是整数,并且元素数量不超过 set-max-intset-entries 时,Set 会采用 IntSet 编码以节省内存,并通过二分查找保证元素唯一。
ZSet 底层数据结构必须满足键值存储、键必须唯一以及可排序,因此 Redis 默认通过 SkipList + HT 的方式实现 ZSet(见下图)。

不过 SkipList + HT 的编码方式相当于存储了两份数据,比较耗费内存,因此在满足以下两个条件时 ZSet 会采用 ZipList 实现:
zset_max_ziplist_entries,默认值 128。zset_max_ziplist_value 字节,默认值 64。不过 ZipList 本身没有排序功能,而且没有键值对的概念,因此需要额外进行以下处理:
score 和 element 是一前一后紧挨在一起的两个 entry。score 越小越接近队首,score 越大越接近队尾,按照 score 值升序排列。Hash 的实现方式与有序集合 ZSet 基本相同。对于较小的数据集,为了节省内存,可以采用 ZipList 的方式存储。而对于较大的数据集或元素较大的情况,会直接采用哈希表 Dict 的方式存储。不过,由于哈希表不需要对元素进行排序,因此无需使用 SkipList。
简单的分布式锁的加锁操作通过 SET 实现,解锁操作通过以下 Lua 脚本实现:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
需要注意的是,这里的 value 必需保证唯一,并在删除前进行判断,从而保证加锁和解锁的同源性,一般情况下可以采用 UUID + 线程标识。
上述简单分布式锁存在不可重入、不可重试与锁超时自动释放的问题,Redisson 的 RedissonLock 能够在单 Redis 实例的情况下有效解决这些问题。
RedissonLock 的主要特点及实现原理如下:
tryLock() 函数 leaseTime 参数使用默认值时,利用 watch dog 机制,通过定时任务每隔 releaseTime / 3 时间重置超时时间,并在锁释放时取消定时任务。普通 RedissonLock 的局限性在于仅适用于单实例 Redis。对于 Redis 集群,在主节点上加锁后,由于 Redis 主从同步是异步的,主节点如果在将加锁信息转移到从节点之前宕机,就可能会导致并发问题。
RedissonMultiLock 联锁可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。这样可以保证多个锁的一致性。
RedissonRedLock 继承自联锁,主要区别在于它认为获取到 n / 2 + 1 个锁即为获取成功,从而提高了 Redis 集群的可用性。不过解锁时仍然会在每一个节点上执行解锁操作,以保证所有的锁都能够被释放。
参考:
http://doc.redisfans.com/index.html
https://www.xiaolincoding.com/redis
