数据类型:是用户在使用 Redis 命令时看到的逻辑类型(如 String, List, Set 等)。

数据结构:是 Redis 内部为了实现这些数据类型而采用的底层物理存储方式(如 SDS, ZipList, SkipList 等)。

数据结构

底层结构 描述 对应上层类型
SDS (Simple Dynamic String) 动态字符串,预分配空间,避免缓冲区溢出,获取长度 O(1)。 String
LinkedList 双向链表,节点分散,内存不连续。 List (旧版)
ZipList 压缩列表,内存连续,节省空间,但修改复杂度高。 List, Hash, ZSet (数据少时)
QuickList 双向链表 + 压缩列表的结合体。链表节点是 ZipList。 List (3.2+)
Dict 哈希表,渐进式 Rehash。 Hash, Set, ZSet, 全局字典
IntSet 整数集合,数组结构,自动排序去重。 Set (仅整数且少)
SkipList (跳表) 多层索引链表,查找效率 O(logN),实现简单。 ZSet (数据多时)
Trie / Radix Tree 前缀树,用于 Stream 的 ID 存储。 Stream

注:Redis 7.0 引入了 ListPack 逐渐替代 ZipList,以解决 ZipList 在极端情况下的性能问题。

动态字符串(SDS)

Redis 的字符串类型(String)底层使用 SDS。

  • 兼容 C 字符串,但更高效。

  • 记录长度(len)和可用空间(alloc),避免频繁重分配。

  • 二进制安全(可存储任意字节数据)。

  • 以 O(1) 时间复杂度获取字符串长度。

双向链表(linkedlist)

早期用于 List 类型(现已在小数据量时优先使用 quicklist)。

  • 每个节点包含前驱和后继指针。

  • 支持 O(1) 头尾插入/删除。

  • 内存开销较大(每个节点有额外指针)。

哈希表(hash / dict)

当元素较多时,Hash 类型,无序集合使用Set 类型。

  • 平均 O(1) 的查找、插入、删除。

  • 使用拉链法解决冲突。

  • 支持渐进式 rehash(避免阻塞)。

压缩列表(ziplist)

用于小规模的 List、Hash 和 Sorted Set。

  • 连续内存存储,节省内存。

  • 适合存储少量、小体积的元素。

  • 插入/删除可能需要内存重分配(O(N))。

触发条件(默认配置):

  • List 元素数量 ≤ 512 且每个元素大小 ≤ 64 字节。

  • Hash 元素数量 ≤ 512 且所有 field/value 长度 ≤ 64 字节。

快速列表(quicklist)

Redis 3.2+ 中 List 的底层实现。由 ziplist(或 Redis 7.0+ 的 listpack)组成的双向链表。

  • 结合了链表的灵活性和 ziplist 的内存效率。

  • 避免单个 ziplist 过大导致的内存重分配开销。

紧凑列表(listpack)

替代 ziplist,用于 List、Hash、ZSet 等的小数据编码。

  • 比 ziplist 更节省内存。

  • 解决 ziplist 的连锁更新问题(修改一个 entry 可能导致后续所有 entry 偏移变化)。

  • 设计更简单、安全。

跳跃表(skiplist)

Sorted Set(ZSet)的底层实现之一(与 dict 配合)。

  • 有序结构,支持范围查询(如 ZRANGE)。

  • 平均 O(log N) 的查找、插入、删除。

  • 比平衡树实现简单,且支持并发优化。

  • 配合 dict:ZSet 同时维护一个 dict 用于 O(1) 查找 score。

整数集合(intset)

当 Set 中所有元素都是整数且数量较少时使用。

  • 连续内存存储,按升序排列。

  • 节省内存,支持二分查找(O(log N))。

  • 升级机制:插入非整数或超出当前整数类型范围时,自动转为 hashtable。

数据类型

五种常用基本数据类型

  • String: 字符串(二进制安全(可以存图片、序列化对象等),一个key最大存512MB)

  • Hash: 散列键值对,对象序列化后存储
    ZipList (数据少时) 或 Dict (哈希表,数据多时)

  • List: 列表,按插入顺序排序,支持两端 push/pop。(有序双向链表、可重复,增删快查询慢)

    Redis 3.2 之前是 LinkedListZipList;3.2 之后是 QuickList (双向链表 + 压缩列表)。【排行榜、消息队列、栈】

    • lpush list:1 0 1 2 3 4 5 6 添加7个数到列表

    • rpop list:1 从右边弹出一个(队列)

    • lpop list:1 从左边弹出一个 (栈)

  • Set: 集合(hash实现,无序、不可重复,支持交集、并集和差集操作)

    IntSet (整数且数量少) 或 Dict (哈希表)。【共同好友、点赞签到打卡、统计访问IP、好友推荐(交集)】

    • sadd sign:2020-01-16 1 2 3 4 5 6 7 8 添加某天签到用户的ID,8个

    • smembers sign:2020-01-16 列出某天所有签到用户的ID

    • spop sign:2020-01-16 随机列出一个某天签到用户的ID

  • Sorted Set: 有序集合(有序的set,底层结构为跳跃表和hash)
    ZipList (数据少时) 或 SkipList (跳表) + Dict (数据多时)。

其他数据类型

  • bitmap(位图)

    • bitop and continue:site site:2020-01-18 site:2020-01-17 计算某段时间内,都签到的用户数量
    • bitcount site:2020-01-17 0 100 统计某一天网站的签到数量
  • hyperloglog(基数统计)

  • Stream(消息队列)

  • bitfield(位域,批量的bit位操作)

  • geo(地理空间)

各种数据类型对应的数据结构:

img

Redis 数据类型与底层结构映射

Redis 类型 编码(encoding) 底层数据结构
String raw / int / embstr SDS / long / embstr(优化的 SDS)
List quicklist quicklist(listpack + 双向链表)
Hash ziplist / listpack / hashtable listpack(小数据)或 dict
Set intset / hashtable intset(整数小集合)或 dict
Sorted Set ziplist / listpack / skiplist+dict listpack(小数据)或 skiplist+dict

String(SDS)

SDS是Redis中的一种字符串类型,它是一种二进制安全的字符串,由简单动态字符串(SDS)实现。SDS支持多种数据结构。

  • SDS可以动态增加内存空间,避免了静态数组的大小限制。

  • SDS会检查内存是否足够,避免了缓冲区溢出的问题。

  • SDS可以避免C字符串常见的问题,比如缓冲区溢出和内存泄露等。

  • SDS的常数复杂度获取字符串长度和杜绝缓冲区溢出可以避免使用strlen和strcat函数时的一些问题。

  • SDS的空间预分配和惰性空间释放两种策略可以减少修改字符串的内存重新分配次数。

  • SDS是二进制安全的,因为它不是以空字符串来判断字符串是否结束,而是以len属性表示的长度来判断字符串是否结束。

1
2
3
4
5
6
7
8
struct sdshdr{
//int 记录buf数组中未使用字节的数量 如上图free为0代表未使用字节的数量为0
int free;
//int 记录buf数组中已使用字节的数量即sds的长度 如上图len为5代表未使用字节的数量为5
int len;
//字节数组用于保存字符串 sds遵循了c字符串以空字符结尾的惯例目的是为了重用c字符串函数库里的函数
char buf[];
}

List(快速列表)

quicklist 实际上是 zipList 和 linkedList 的混合体:将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

相对于链表它压缩了内存

img
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct quicklistNode {
struct quicklistNode *prev; //上一个node节点
struct quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

Hash(字典+压缩列表)

哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable。

当hash对象可以同时满足一下两个条件时,哈希对象使用ziplist编码。

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

通过挂链解决冲突

img

Set(字典)

zSet(跳跃表)

跳跃表是一种有序的数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

链表加多级索引的结构,就是跳跃表,每个拉链节点都有指针指向别的槽

下面为原始单链表,上面为多个级别的索引

img

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现。

img

bitmap(sds)

即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。保存为二进制的字节数组,可以把 Bitmap 看作是一个 bit 数组。BitMap 占用的空间,就是底层字符串占用的空间

1
2
3
4
5
6
7
8
9
10
11
# 设置值,其中 value 只能是 0 和 1     SETBIT key offset value
127.0.0.1:6379> SETBIT sid10t 0 1
(integer) 0

# 获取值 GETBIT key offset
127.0.0.1:6379> GETBIT sid10t 1
(integer) 1

# 获取指定范围内值为 1 的个数 start 和 end 以字节为单位 BITCOUNT key [start end [BYTE|BIT]]
127.0.0.1:6379> BITCOUNT sid10t 0 8
(integer) 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BitMapsExample {
public void run() {
UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379");

boolean res1 = jedis.setbit("pings:2024-01-01-00:00", 123, true);
System.out.println(res1); // >>> false

boolean res2 = jedis.getbit("pings:2024-01-01-00:00", 123);
System.out.println(res2); // >>> true

boolean res3 = jedis.getbit("pings:2024-01-01-00:00", 456);
System.out.println(res3); // >>> false

long res4 = jedis.bitcount("pings:2024-01-01-00:00");
System.out.println(res4); // >>> 1
}
}

基础命令

  1. SETBIT
    • 用途SETBIT用于设置Bitmap中指定位置的位值(0或1)。
    • 语法SETBIT key offset value
    • 示例:假设我们要在位置5设置位为1,命令将是:SETBIT mybitmap 5 1
    • 工作方式SETBIT会将键mybitmap在偏移offset处的位设置为value。如果该键不存在,Redis会自动创建一个新的Bitmap。该命令返回位被设置之前的旧值。
  1. GETBIT

    • 用途GETBIT用于获取Bitmap中指定位置的位值。
    • 语法GETBIT key offset
    • 示例:要获取位置5的位值,命令是:GETBIT mybitmap 5
    • 工作方式GETBIT返回键mybitmap在偏移offset处的位值。如果偏移量超出了字符串的长度,它会假定超出范围的位都是0。
  1. BITCOUNT

    • 用途BITCOUNT用于计算Bitmap中设置为1的位的数量。
    • 语法BITCOUNT key [start end]
    • 示例:计算整个Bitmap的位数:BITCOUNT mybitmap
    • 工作方式BITCOUNT会返回指定范围内值为1的位的数量。如果不指定范围,它将默认计算整个Bitmap。
  1. BITPOS

    • 用途BITPOS用于找到Bitmap中第一个设置为0或1的位的位置。
    • 语法BITPOS key bit [start] [end]
    • 示例:找到第一个设置为1的位:BITPOS mybitmap 1
    • 工作方式BITPOS返回位值为bit的第一个位的位置。你可以指定一个可选的范围来限制搜索。如果没有找到,会返回特定的值。

高级命令

  1. BITOP

    • 用途BITOP用于对一个或多个Bitmap进行AND、OR、XOR和NOT操作。
    • 语法BITOP operation destkey key [key ...]
    • 示例:对两个Bitmap进行AND操作:BITOP AND destkey bitmap1 bitmap2
    • 工作方式BITOP会将指定的操作应用于提供的所有Bitmap,并将结果存储在destkey中。这对于组合多个Bitmap或对Bitmap进行逻辑操作非常有用。
  1. BITFIELD

    • 用途BITFIELD用于对Bitmap进行复杂的操作,如设置或获取指定范围内的位值。
    • 语法BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]
    • 示例:在位置5设置3位长的无符号整数:BITFIELD mybitmap SET u3 5 6
    • 工作方式BITFIELD可以在Bitmap上执行多个操作,包括获取、设置和自增特定范围的位。它支持不同的数据类型和位操作,使得它成为最灵活的Bitmap操作命令。

应用场景:签到统计、登录状态、用户数、黑名单、去重、

布隆过滤器(bloom)

RoaringBitmap是高效压缩位图,简称RBM。可以用来告诉你 “某样东西一定不存在或者可能存在”

布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询

bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中,结果是概率性的,而不是确切的。

特点: 相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

原理:布隆过滤器是一个 bit 向量或者说 bit 数组。如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成**多个哈希值,**并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,则图为:

img

4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

结论:随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 这个值存在。为了较少这种碰撞的误判,可以增加哈希函数计算(比如以前是三个,现在增加五个,只有五个都为1才算存在,但是效率低)

使用场景:利用布隆过滤器减少磁盘 IO 或网络请求,当一个值必定不存在,可以不用进行后续昂贵的查询请求。

存在问题: 布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生成环境中建议对体积庞大的布隆过滤器进行拆分。应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。

使用指南

选型指南:

需求场景 推荐类型 理由
普通缓存、计数器、锁 String 最简单,性能最高
对象存储、部分更新 Hash 结构化,支持字段级操作
消息队列、最新列表 List 支持 Push/Pop,有序
去重、交集并集运算 Set 自动去重,集合运算丰富
排行榜、排序、延时队列 ZSet 自带分数排序
海量 UV 统计 HyperLogLog 空间换精度,极省内存
签到、状态位 Bitmap 位操作,空间利用率极致
附近的人、距离计算 Geo 封装了经纬度计算
可靠消息队列 Stream 支持消费组确认机制

注意事项

  1. 内存优化:尽量用 Hash 代替多个 String 存储对象(减少 Key 内存开销);小数据量时 Redis 会自动使用 ZipList 结构。

  2. 大 Key 问题:避免 Value 过大(如 String > 10KB, Hash/List/Set/ZSet 元素过多),会导致网络阻塞和删除时主线程卡顿。

  3. 热 Key 问题:避免某个 Key 被极高频率访问,导致单分片负载过高。

  4. 过期策略:Redis 使用惰性删除 + 定期删除,不要完全依赖过期时间处理业务逻辑。