0%

redis数据结构

Redis 数据结构详解(基于 6.0.10 版本)

Redis 作为高性能键值数据库,支持多种丰富的数据结构,每种结构都有独特的底层实现和适用场景。本文从底层编码、核心命令到应用场景,全面解析 Redis 的 8 种核心数据结构:String、List、Set、Hash、Zset、Bitmaps、HyperLogLogs 和 GEO。

Redis 对象模型(redisObject)

Redis 中所有值都会被包装为 redisObject 结构体,通过 type 标识数据类型,encoding 指定底层存储方式,实现灵活的内存优化:

1
2
3
4
5
6
7
typedef struct redisObject {
unsigned type:4; // 数据类型(String/List/Set等)
unsigned encoding:4; // 编码方式(如int/ziplist/skiplist等)
unsigned lru:LRU_BITS; // 最后访问时间(用于LRU淘汰)
int refcount; // 引用计数(内存回收)
void *ptr; // 指向底层数据结构的指针
} robj;
  • type:通过 type key 命令查看,如 stringlist 等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // string
    #define OBJ_STRING 0 /* String object. */
    // list
    #define OBJ_LIST 1 /* List object. */
    // set
    #define OBJ_SET 2 /* Set object. */
    //zset
    #define OBJ_ZSET 3 /* Sorted set object. */
    //hash
    #define OBJ_HASH 4 /* Hash object. */
    // module
    #define OBJ_MODULE 5 /* Module object. */
    // stream
    #define OBJ_STREAM 6 /* Stream object. */
  • encoding:通过 object encoding key 命令查看,决定底层实现(如 String 的 int 编码 vs raw 编码)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 简单动态字符串  raw
    #define OBJ_ENCODING_RAW 0 /* Raw representation */
    // long类型的整数 int
    #define OBJ_ENCODING_INT 1 /* Encoded as integer */
    // 哈希表 hashtable
    #define OBJ_ENCODING_HT 2 /* Encoded as hash table */
    // 压缩 不再使用
    #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
    // 双向链表 不再使用该结构
    #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
    // 压缩列表 ziplist
    #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
    // 整数集合 intset
    #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
    // 跳表 skiplist
    #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
    // embstr编码的简单动态字符串 embstr
    #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
    // 快表 quicklist
    #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
    // 流 stream
    #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

核心数据结构详解

1. String(字符串)

特点:单键单值,可变字节数组,支持字符串、整数、二进制数据(最大 512MB)。
底层编码

  • int:存储 64 位整数(如 set num 123)。
  • embstr:存储短字符串(≤44 字节),连续内存分配(redisObject + SDS 同块内存)。
  • raw:存储长字符串(>44 字节),独立内存分配(redisObject 和 SDS 分开)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
127.0.0.1:6379> set test_int 1
OK
127.0.0.1:6379> object encoding test_int
"int"
127.0.0.1:6379> set test_raw zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
OK
127.0.0.1:6379> strlen test_raw
(integer) 44
127.0.0.1:6379> object encoding test_raw
"embstr"
127.0.0.1:6379> set test_raw zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
OK
127.0.0.1:6379> object encoding test_raw
"raw"
127.0.0.1:6379> strlen test_raw
(integer) 45

# 查看key的内部结构和编码等信息
>debug object k4
Value at:0x7ffc97c044f0 refcount:1 encoding:embstr serializedlength:3 lru:11351195 lru_seconds_idle:5363

embstr和raw两种编码的区别

embstr编码用于保存短字符串,与raw编码一样,都是使用redisObject结构和sdshdr结构来表示字符串对象,但是raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,两个对象在内存地址上是不连续的;而embstr编码则通过调用一次内存分配函数来分配一块连续的内存空间,空间中依次包含redisObject和sdshdr两个结构

底层实现:基于 SDS(简单动态字符串),记录长度(len)和容量(alloc),避免 C 字符串的缺陷(如 O (n) 长度计算):

1
2
3
4
5
6
struct sdshdr8 {  // 8位长度示例
uint8_t len; // 已使用长度
uint8_t alloc; // 总容量(不含头部和终止符)
unsigned char flags; // 类型标识
char buf[]; // 字符串数据
};

扩容:当字符串长度小于1M时,扩容是现有空间进行加倍;当字符串长度超过1M,扩容只会多扩1M的空间。且字符串最长为512M

核心命令

  • 设值:set key valuesetex key 3600 val(过期时间)、setnx key val(不存在时设值)。
  • 取值:get keymget key1 key2(批量获取)。
  • 修改:append key suffix(追加)、setrange key 0 "new"(替换部分字符)。
  • 计数:incr key(自增)、decrby key 5(减 5)。

值拼接

1
2
3
#给key对应的value进行拼接,返回的是拼接之后的字符长度
append k1 qwe
(integer) 5

值长度

1
strlen k1

数字运算操作

1
2
3
4
5
6
7
8
#加一
INCR k2
#减一
DECR k2
#加3,第二个参数是步长
INCRBY k2 3
#减2,第二个参数是步长
DECRBY k2 2

注意:这几个命令只能对数字进行操作,范围是Long.MIN~Long.MAX

获取值

1
2
3
4
get k1

#只获取该key所对应value的部分字符
getrange k1 0 2

判断值是否存在

1
2
# 返回0则为不存在
exists test

设置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# set key value
set test 123

# setrange覆盖部分值 其中0为从下标为0的字符开始替换
setrange k1 0 zx
(integer) 5

#设置值时设置过期时间
#setex key seconds value
setex k3 20 b

#如果key不存在设置值,防止覆盖(set if not exists)
#setnx key value
setnx k1 1

# 设置key的值,并返回key的旧值
getset k1 vv1

多值操作

1
2
3
4
5
6
7
8
9
#同时set多个键值
#mset key value [key value ...]
mset k4 v4 k5 v5
#同时获取多个键的值
#mget key [key ...]
mget k4 k5
#同时set多个键值(全部不存在时才会设置值)
#msetnx key value [key value ...]
msetnx k6 v6 k7 k8

应用场景

  • 缓存(如用户信息 set user:1000 "{name: 'Alice'}")。
  • 计数器(如文章阅读量 incr post:100:views)。
  • 分布式锁(setnx lock:order 1 + 过期时间)。
  • 分布式全局id 使用INCR来生成id INCR id
  • pv/uv统计 可以使用incr来统计文章的访问量,key设置为post:文章ID:view
  • 分布式session

2. List(列表)

特点:单键多值,有序可重复,支持两端操作(栈 / 队列)。
底层编码(6.0.10 版本):

  • quicklist:3.2 版本后替代 ziplist + linkedlist,是 ziplist 组成的双向链表,平衡内存和性能。

    在3.2版本之前使用的是ziplist+linkedlist

底层实现quicklist 由多个 quicklistNode 组成,每个节点包含一个 ziplist(压缩列表,连续内存存储短数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 总元素数
unsigned long len; // 节点数
int fill : QL_FILL_BITS; // ziplist 填充因子
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;

typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
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;

核心命令

  • 插入:lpush list 1 2 3(左插,栈)、rpush list 4 5(右插,队列)。
  • 弹出:lpop list(左弹)、rpop list(右弹)。
  • 取值:lrange list 0 -1(获取所有元素)、lindex list 2(索引取值)。
  • 修剪:ltrim list 0 2(保留前 3 个元素)。

设置值**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#lpush(指从左边进,头部,先进后出,像栈)
#lpush key element [element ...]
lpush list1 1 2 3 4 5

#rpush(指从右边进,尾部,先进先出,像队列)
#rpush key element [element ...]
rpush list2 1 2 3 4 5

#给某个索引位置赋值
#lset key index element
lset list1 0 q

#在某个元素之前或之后插入一个元素
#linsert key before|after pivot element
linsert list1 before q zz

# 获取指定索引的元素,如果使用负数,则从右边开始计算
lindex list1 0

取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#弹出栈顶元素(从左边开始弹出,弹出之后,list中就不存在该元素了)
#lpop key
lpop list1
#弹出栈顶元素(从右边开始弹出,弹出之后,list中就不存在该元素了)
#rpop key
rpop list1
# 阻塞读 可以使用blpop、brpop,如果队列没有数据的时候就会进入休眠状态(这里要注意阻塞的话,客户端连接就变成了闲置连接,闲置过长服务器会断开连接的,所以使用的时候客户端要捕获异常进行重试)

#按照索引位置取值
#lindex key index
# 获取头元素
lindex list1 0
# 获取尾部元素
lindex list1 -1

#取值,从头部到尾部
#lrange key start end (负下标表示的是从后往前,如-1表示倒数第一)
lrange list1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"

# blpop和brpop是阻塞版本
# blpop key timeout
# timeout表示 列表为空时会等3秒后返回,如果timeout=0,则会一直阻塞下去,直到有数据为止
# 如果多个客户端对同一个键执行blpop,那么先执行的blpop命令的客户端可以获取到弹出的值
blpop list1 3

列表长度

1
llen list1

删除值

1
2
3
4
5
6
7
8
9
10
11
12
#删除几个某个元素  count>0时,从左边开始删除;count=0时,删除所有值为element的元素;count<0时,从右边开始删除
#lrem key count element
lrem list1 1 "hi"

#截取指定范围的值赋给key(范围是索引)
#ltrim key start stop
ltrim list1 0 0

# 从头部删除元素 出栈
lpop list1
# 从尾部删除元素
rpop list1

出栈入栈

1
2
3
#从源列表右出栈压入左目标列表,将一个列表移至另一个列表
#rpoplpush source dest
rpoplpush list2 list1

由于使用的链表,所以在使用index取值的时候需要对链表进行遍历,性能会随着index增大而变差

应用场景

  • 消息队列( LPUSH list item + brpop list来实现阻塞队列)。
  • 评论列表(lpush 新增评论,lrange 分页查询)。
  • 热点列表 如微博话题 LRANGE list 0 10

lpush+lpop = 栈

lpush+rpop = 队列

lpush+ltrim = 有限集合

lpush+brpop = 消息队列

3. Set(集合)

特点:单键多值,无序不重复,支持交集、并集等集合运算。类似于HashSet,通过hash table实现的
底层编码

  • intset:元素为整数且数量 ≤512(set-max-intset-entries 配置),紧凑数组存储。
  • hashtable:元素为字符串或数量超限,基于哈希表(键为元素,值为 NULL)。
1
2
3
4
5
6
7
8
typedef struct intset {
// 每个整数的类型
uint32_t encoding;
// intset的长度
uint32_t length;
// 整数数组
int8_t contents[];
} intset;

核心命令

  • 增删:sadd set a b c(添加)、srem set a(删除)。
  • 查询:smembers set(所有元素)、sismember set a(判断存在)。
  • 运算:sinter set1 set2(交集)、sunion set1 set2(并集)、sdiff set1 set2(差集)。

设置值**

1
2
3
4
#设置值
#sadd key member [member ...]
sadd set1 vv vc vb vv

取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#取值,集合中的所有元素
#smembers key
smembers set1
1) "vc"
2) "vb"
3) "vv"

#判断该元素是否在set
#sismember key member
sismember set1 vv

#获取随机的元素,不会删除元素(count表示获取的数量,默认为1)
#srandmember key [count]
srandmember set1 2

查看集合元素个数

1
2
#scard key
scard set1

删除

1
2
3
4
5
6
#srem key member [member ...]
srem set1 vb

#随机出栈(默认数量为1)
#spop key [count]
spop set1

随机抽取

1
2
3
#随机从集合中抽取2个(默认1个)
#srandmember key [count]
srandmember set1 2
1
2
3
#将源集合中的值移至目标集合
#smove source dest member
smove set1 set2 test

差集 交集 并集

1
2
3
4
5
6
7
8
9
10
#差集 返回的是其他key与第一个key的差集,属于A且不属于B的(A - B)
#sdiff key [key ...]
sdiff set1 set2

#交集 属于A且属于B的(A ∩ B)
#sinter key [key ...]
sinter set1 set2
#并集 属于A或属于B的(A ∪ B)
#sunion key [key ...]
sunion set1 set2

应用场景

  • 标签系统(用户标签 sadd user:100:tags "music" "sports")。推送的时候使用交集 SINTER set1 set2 set3,还有商品选择的标签筛选也可以使用并集
  • 好友关系(关注)(共同好友 sinter user:100:friends user:200:friends 、关注(添加好友) sadd user1:follow user2 sadd user2:fans user1、可能认识的人 使用差集 sdiff user2:follow user1:follow
  • 抽奖 spop 随机抽取
  • 点赞

    点赞 sadd like:文章id 用户id

    取消点赞 srem like:文章id 用户id

    是否点赞 sisrember like:文章id 用户id

    点赞用户 smembers like:文章id

    点赞数 scard like:文章id

4. Hash(哈希)

特点:键值对集合(类似字典),适合存储对象。相当于HashMap,对于hash碰撞也是采用的HashMap的处理方式,数组+链表
底层编码

  • ziplist:字段和值为短字符串(字段数 ≤512 且单个长度 ≤64 字节,由 hash-max-ziplist-entrieshash-max-ziplist-value 控制),紧凑存储。
  • hashtable:字段或值过长,基于哈希表(键为字段,值为字段值)。
1
2
3
4
5
#根据该配置项来进行编码转换的
# 当hash对象的键值对数量大于该值时使用OBJ_ENCODING_HT编码
hash-max-ziplist-entries 512
# 当hash对象中的键值对存在键或值的长度大于该值时使用OBJ_ENCODING_HT编码
hash-max-ziplist-value 64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 哈希表结构
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
unsigned long sizemask;
// 哈希表已有节点数量
unsigned long used;
} dictht;
// 哈希节点
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 下一个哈希节点,链表,可以将多个哈希值相同的键值对进行连接,解决hash冲突的问题
struct dictEntry *next;
} dictEntry;
// 字典结构
typedef struct dict {
// 类型
dictType *type;
// 私有数据
void *privdata;
// 哈希表,通常情况下只有一个hashtable是有值的。在扩容时,需要分配新的hashtable,然后进行渐进式搬迁,这时候两个hashtable存储的分别是旧的hashtable和新的hashtable,搬迁结束后,旧的hashtable被删除,新的hashtable取而代之
// ht[0]:用来存放真实的数据
// ht[1]:用于扩容
// 也是采用了数组+链表的方式
dictht ht[2];
//rehash 如果为-1表示没有进行rehash
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;


// 处处不同的数据结构,使用不同的hash算法,不同的键值比较算法,不同的析构函数
typedef struct dictType {
// hash函数
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;
哈希冲突

redis哈希冲突是使用链地址法进行解决的,将新节点添加在链表的表头

扩容

redis的字典源码中的dictht是一个容量为2的数组,其作用就是用于进行扩容的

1
2
3
4
// 哈希表
// ht[0]:用来存放真实的数据
// ht[1]:用于扩容
dictht ht[2];

最初始的数据是存放在ht[0]中的,当要进行扩展或收缩时,ht[1]会根据ht[0]的容量来计算容量,ht[1]扩展的大小是比当前ht[0].used值的两倍大的第一个2的整数幂;收缩的大小是ht[0].used的第一个大于等于的2的整数幂。将ht[0]的所有键值重新散列到ht[1]中,重新计算所有的数组下标,当数据迁移完后ht[0]就会释放,将ht[1]改为ht[0],为下一次扩展或收缩做准备

由于有时候redis中的数据比较多,不能瞬间完成扩容操作,会将rehash操作进行分步进行,其字段rehashidx就用于标识rehash过程,如果是-1表示当前没有进行rehash操作,当进行rehash操作时会将该值改为0,在进行更新、删除、查询操作时会在ht[0]和ht[1]中都进行,先更新ht[0],再更新ht[1],而进行新增操作就直接新增到ht[1]中,保证ht[0]只减不增,直到rehash完成

核心命令

  • 增改:hset user:100 name "Alice" age 25(设字段)、hmset user:100 city "Beijing"(批量设)。
  • 查询:hget user:100 name(取字段)、hgetall user:100(所有字段和值)。
  • 计数:hlen user:100(字段数)、hexists user:100 email(判断字段存在)。

设置值**

1
2
3
4
5
6
7
8
9
10
11
12
#Redis4.0之后可以设置多个值,与hmset功能相同
#hset key field value [field value ...]
# 不区分插入和更新,插入时返回1,更新时返回0
hset user id 123

#设置多个字段
#hmset key field value [field value ...]
hmset user name zhangsan age 18

#不存在则设置值
#hsetnx key field value
hsetnx user id 12

获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#hget key field
hget user id
#获取多个字段的值
#hmget key field [field ...]
hmget user id name sex age

#可以获取该key所对应的map
#hgetall key
hgetall user

#获取key对应的map有几个字段
#hlen key
hlen user

#判断该key的某个字段是否存在
#hexists key field
hexists user id

#获取该key下所有的字段
#hkeys key
hkeys user

#获取该key下所有的字段值
#hvals key
hvals user

删除字段值

1
2
3
#支持删除多个字段
#hdel key field [field ...]
hdel user age

数值操作

1
2
3
4
5
6
7
# 加2
# hincrby key field increment
hincrby user id 2

# 减2
# hdecrby key field increment
hdecrby user id 2

应用场景

  • 存储对象(用户信息 hset user:100 id 100 name "Alice")。hash和string都可以存储对象,频繁读操作时使用string,频繁写操作使用hash。hash的存储消耗要高于单个字符串
  • 购物车(hset cart:100 item:10 2 表示用户 100 的购物车中商品 10 数量为 2)。

5. Zset(有序集合)

特点:单键多值,每个元素关联 score(分数),按分数排序,元素唯一。类似于TreeSet
底层编码

  • ziplist:元素少且短(元素数 ≤128 且单个长度 ≤64 字节,由 zset-max-ziplist-entrieszset-max-ziplist-value 控制),按 score+元素 紧凑存储。
  • skiplist + hashtable:元素多或长,skiplist 按分数排序,hashtable 映射元素到分数(O (1) 查分数)。
1
2
3
#根据该配置项来进行编码转换的
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

底层实现skiplist(跳表)是核心,通过多层索引加速查找,时间复杂度接近 O (log n):

底层使用的是dict哈希表和skiplist跳表,dict用来关联value和score,保证value的唯一性,同时通过value可以获取到score。skiplist的作用在于给value排序以及根据score的范围来获取元素列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef struct zset {
dict *dict; // 元素→分数映射
zskiplist *zsl; // 按分数排序的跳表
} zset;

typedef struct zskiplistNode {
// 节点数据
sds ele;
// 分数
double score;
// 后继指针
struct zskiplistNode *backward;
// 前驱指针
struct zskiplistLevel {
struct zskiplistNode *forward;
// 调到下一个数据项需要走几步,计算rank时有用
unsigned long span;
} level[];
} zskiplistNode;

typedef struct zskiplist {
// 跳表头尾指针
struct zskiplistNode *header, *tail;
// 跳表长度
unsigned long length;
// 跳表高度
int level;
} zskiplist;

跳表skiplist相比一般的链表,有更高的查找效率,效率可以比拟二叉查找树

跳表搜索

在存储时会预先间隔地保存有序链表的节点,从而在查找时能达到类似于二分搜索的效果,可以节省查找时间,但是浪费了空间,数据只存储了一次,只是指针多次存储了

  • 由多层结构组成
  • 每一层都是一个有序链表
  • 最底层的链表包含了所有元素
  • 如果一个元素出现在leveli的链表中,则它在leveli之下的链表也都会出现
  • 每个节点包含两个指针,一个指向同链表中的下一个元素,一个指向下面一层的元素

核心命令

  • 增改:zadd rank 90 "Alice" 85 "Bob"(添加元素和分数)。
  • 查询:zrange rank 0 -1 withscores(升序取所有)、zrevrange rank 0 2(降序取前 3)。
  • 范围:zrangebyscore rank 80 100(分数 80-100 的元素)。
  • 计数:zcard rank(元素数)、zcount rank 80 90(分数范围内的元素数)。

设置值**

1
2
3
# 向zset中添加元素,并且会根据score进行排序,如果元素存在,会更新score
#zadd key score member [score member ...]
zadd zset1 1 q 2 w

取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#WITHSCORES表示查询结果是否带着score,score从小到大
#zrange key start stop [WITHSCORES]
zrange zset1 0 -1

#WITHSCORES表示查询结果是否带着score,score从大到小
#zrevrange key start stop [WITHSCORES]
zrevrange zset1 0 -1

#根据score范围查询
#zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
zrangebyscore zset2 80 100

#根据score的范围来计数
#zcount key min max
zcount zset2 0 10

#根据值来获取下标(根据score从小到大来排的序)
#zrank key member
zrank zset2 d

#根据值来获取下标(根据score从大到小来排的序)
#zrevrank key member
zrevrank zset2 d

#获取元素的score
#zscore key member
zscore zset2 c

查看集合元素个数

1
2
#该key的元素个数
zcard zset2

删除

1
2
3
4
5
6
7
8
9
10
#zrem key member
zrem zset2 s

# 根据下标区间来删除(根据score从小到大来排的序)
#zremrangebyrank key start stop
zremrangebyrank zset1 5 8

# 根据score区间来删除
# zremrangebyscore key min max
zremrangebyscore zset1 7 10

数值操作

1
2
3
# 如果key中存在该元素,则该元素的score增加increment,否则新增该元素
# zincrby key increment member
zincrby zset1 2 b

应用场景

  • 排行榜(游戏积分 zadd game:rank 1000 "user1"zrevrange 取 Top10)。
    • ZADD 添加元素
    • ZRANGE 按分数从低到高排列,返回区间内的元素
    • ZREVRANGE 按分数从高到低排列,返回区间内的元素
    • ZRANK 返回某个元素的排名
  • 带权重的消息队列(按优先级 score 消费)。

6. Bitmaps(位图)

特点:基于 String 实现的位操作,每个位存储 0/1,节省空间(1MB 可存 800 多万位)。位图不是一个真实的数据类型,是定义在String之上的面向位操作的集合,每个单元只能存储0和1。位图的最大优势在于节省空间

1
2
127.0.0.1:6379> type bittest
string

底层编码rawembstr(复用 String 的 SDS 存储)。

核心命令

  • 设位:setbit user:login 5 1(第 5 天登录标记为 1)。
  • 查位:getbit user:login 5(查询第 5 天是否登录)。
  • 计数:bitcount user:login 0 30(月登录天数)。
  • 运算:bitop AND result b1 b2(两个位图的交集)。

设置值

1
2
3
#setbit key offset value
# value只能是0或者1,存储的是二进制
setbit bittest 10 1

取值

1
2
3
4
5
6
7
8
9
10
11
12
# 零存整取  直接取全部的值
get key
#getbit key offset
# 如果offset超出范围的话返回0
getbit bittest 10

#bitcount key [start] [end]
#获取指定范围为1的个数
bitcount bittest 0 1000
#bitcount key 0/1 [start] [end]
# 查找指定范围内出现的第一个0或1
bitpos bittest 1

bitmap适用于大量数据,少量数据的话会导致大部分位都是0,占用的内存数比集合还多

应用场景

  • 签到统计(setbit 记录每日签到,bitcount 算月签到数)。
  • 用户标签(每位代表一个标签,位运算快速筛选用户)。
  • 可以用来做布隆过滤器

7. HyperLogLogs(基数统计)

特点:极小空间(~12KB)实现海量数据的基数统计(去重计数),误差率约 0.81%。HyperLogLog是一种基于String的基数算法。通过HyperLogLog可以利用极小的内存空间完成独立总数的统计

1
2
127.0.0.1:6379> type hpl
string

底层编码raw(String 存储)。

核心命令

  • 添加:pfadd uv:202408 "user1" "user2"(添加访问用户)。
  • 计数:pfcount uv:202408(统计独立访客数)。
  • 合并:pfmerge uv:202407-08 uv:202407 uv:202408(合并两个月数据)。
1
2
3
4
5
6
7
8
9
10
11
# 添加
# pfadd key element [element ...]
pfadd hpl 123

# 统计
# pfcount key [key ...]
pfcount hpl

#合并
# pfmerge destkey sourcekey [sourcekey ...]
pfmerge destkey sourcekey1 sourcekey2

HyperLogLog非常的节省空间,但是HyperLogLog统计的数据是不准确的

应用场景

  • UV 统计(无需存储具体用户,仅计数)。不可以获取用户id(而且数据存在一定的误差)
  • 搜索关键词去重计数。

8. GEO(地理信息)

特点:存储经纬度,支持距离计算、附近地点查询,底层复用 Zset(score 编码经纬度)。

1
2
127.0.0.1:6379> type key
zset

底层编码skiplist + hashtable(复用 Zset 结构)。

核心命令

  • 添加:geoadd cities 116.40 39.90 "Beijing" 121.47 31.23 "Shanghai"
  • 查坐标:geopos cities "Beijing"(获取北京经纬度)。
  • 算距离:geodist cities "Beijing" "Shanghai" km(两地距离,单位千米)。
  • 附近地点:georadius cities 116.40 39.90 100 km(北京周边 100km 内城市)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 增加地理位置信息
# longitude经度
# latitude纬度
# member成员
# geoadd key longitude latitude member [longitude latitude member ...]
geoadd key 116.28 39.55 bj

# 获取地理位置信息 返回经纬度
#geopos key member [member ...]
geopos key bj

# 获取两个地理位置的距离 unit为单位 包含 m 米、km 千米、mi 英里、ft 尺
# geodist key member1 member2 [unit]
geodist key bj sjz km

# 获取指定位置范围内的地理信息集合
# georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
# longitude latitude也可以使用成员来替换,此时使用georadiusbymember key member
# radius m|km|ft|mi 表示半径+单位
# withcoord 返回结果中包含经纬度
# withdist 返回结果中包含里节点位置的距离
# withhash 返回结果中包含geohash
# COUNT count 指定返回的数量
# asc|desc 按照距离做升序或者降序
# store key 将返回结果的地理位置信息保存到指定键
# storedist key 将返回结果距离节点距离保存到指定键
georadiusbymember key bj 250 km

# 删除地理位置信息
# zrem key member

应用场景

  • 附近的人(georadiusbymember 查用户周边的人)。
  • 地理位置推荐(如外卖店铺距离排序)。

数据结构选择指南

数据结构 核心特性 典型应用场景
String 单值,简单高效 缓存、计数器、分布式锁
List 有序可重复,两端操作 消息队列、评论列表
Set 无序不重复,集合运算 标签、好友关系、共同好友
Hash 键值对集合 存储对象(用户信息、商品属性)
Zset 有序(score),去重 排行榜、带权重的队列
Bitmaps 位操作,省空间 签到统计、用户标签
HyperLogLogs 基数统计,极小空间 UV 统计、去重计数
GEO 地理信息,距离计算 附近的人、地理位置推荐

欢迎关注我的其它发布渠道