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 地理信息,距离计算 附近的人、地理位置推荐

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

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10