惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
博客园 - 【当耐特】
博客园 - 三生石上(FineUI控件)
量子位
aimingoo的专栏
aimingoo的专栏
V
V2EX
Vercel News
Vercel News
B
Blog
M
MIT News - Artificial intelligence
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
TaoSecurity Blog
TaoSecurity Blog
N
News and Events Feed by Topic
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
S
Secure Thoughts
U
Unit 42
博客园 - 叶小钗
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
News | PayPal Newsroom
Help Net Security
Help Net Security
S
Security Affairs
Microsoft Security Blog
Microsoft Security Blog
W
WeLiveSecurity
博客园 - Franky
Forbes - Security
Forbes - Security
Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
Schneier on Security
Schneier on Security
I
InfoQ
B
Blog RSS Feed
大猫的无限游戏
大猫的无限游戏
A
About on SuperTechFans
Webroot Blog
Webroot Blog
AWS News Blog
AWS News Blog
Last Week in AI
Last Week in AI
Security Archives - TechRepublic
Security Archives - TechRepublic
C
CERT Recently Published Vulnerability Notes
N
News and Events Feed by Topic
阮一峰的网络日志
阮一峰的网络日志
L
Lohrmann on Cybersecurity
SecWiki News
SecWiki News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
J
Java Code Geeks

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 手动实现docker容器bridge网络模型 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 go中如何处理error pod中将代码与运行环境分离 ddt源码分析 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment 容器的本质 docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
redis之五种基本数据类型
zhengwenfeng · 2022-12-01 · via 郑文峰的博客

# 0. 前言

本文主要讲解 redis 的五种基本数据类型:String、List、Set、Sorted Set、Hash。学习如何使用它们,并且了解它们的底层数据结构实现,这样我们才能在适当的应用场景选择最适合的数据类型来解决我们的需求。

# 1. String

# 1.1 简单使用

String 是 redis 最简单的且最常用的数据类型,可以用来存储字符串、整型以及浮点型。

  • 字符串
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> get name
"zhangsan"

1
2
3
4

  • 整型

虽然 get age 时返回的是字符串,但是可以通过 object encoding age 看到其真正的类型。

127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> object encoding age
"int"

# 自增
127.0.0.1:6379> incr age
(integer) 19

# 自减
127.0.0.1:6379> decr age
(integer) 18

1
2
3
4
5
6
7
8
9
10
11
12
13
14

  • 浮点型

写入的浮点型,但还是通过字符串来保存的,但是在使用的时候,会将字符串转换成浮点数。

127.0.0.1:6379> set height 1.77
OK
127.0.0.1:6379> get height
"1.77"
127.0.0.1:6379> object encoding height
"embstr"

# 浮点数操作
127.0.0.1:6379> incrbyfloat height 1
"2.77"

1
2
3
4
5
6
7
8
9
10

更多的操作请参考官网 (opens new window)

# 1.2 数据编码

String 支持的编码类型有 int、embstr 和 raw,在不同条件时,会转换成对应的编码。

  • int 当存储的数据为整型时,会使用 int 编码。其实现是,会直接将整型值存储在 redisObject 的 ptr(将 void* 转换成 long) 中,并且将字符串的对象的编码设置为 int。

  • raw 该种编码是使用一种简单动态字符串(SDS)的数据结构存储,当字符串的长度大于 44 时,会使用该种数据编码方式。

  • embstr 也是使用 简单动态字符串(SDS)的数据结构存储,是当字符串长度小于等于 44 时使用。

embstr 和 raw 的区别是什么呢?

创建 string 使用 raw 编码时,会调用两次内存分配来创建 redisObject 和 sds 的数据结构,而 embstr 只会调用一次来创建连续的内存空间来存储 redisObject 和 sds 。

当字符串较小时,embstr 少调用一次内存分配,释放也只需要一次,明显更快,并且连续的内存空间可以更好的利用 cpu 缓存。

# 1.3 简单动态字符串(SDS)

在数据编码中提到 embstr 和 raw 都是使用 SDS 数据结构,那么这种数据结构的是怎么样的,有什么好处呢?

数据结构如下图所示:

struct sdshdr {
    //记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
    int len;
 
    //记录buf 数组中未使用字节的数量
    int free;
 
    //字节数组,用于保存字符串
    char buf[];
};

1
2
3
4
5
6
7
8
9
10

  • free: 表示还有多少空余空间
  • len: 已使用多少空间
  • buf: 存储字符串的数组

问题:String 为什么使用简单动态字符串来实现,而不是使用 C 传统字符串来实现呢?

  • 防止缓存区溢出

在扩充字符串时,需要考虑缓冲区是否足够,SDS 提供 API 来帮助我们判断并扩充空间。

  • 常数获取字符串长度

C 传统字符串需要遍历才能知道具体的长度,时间复杂度为 O(n),而 SDS 可以直接读 len 属性获取长度,时间复杂度为 O(1)

  • 减少内存分配的次数

在扩大字符串长度时,需要重新申请更大的内存空间,然后拷贝到新的内存空间中,然后再释放旧的内存空间。

在缩小时也是一样,需要申请新的空间,再释放旧的空间。

在 SDS 中可以通过以下的方式来减少内存分配和释放,提高效率。

  1. 空间预分配,在分配空间时,除了必要的空间外,可以额外的分配未使用的空间,在下次使用,可以快速使用这部分的空间,而不必重新分配。
  2. 惰性释放空间,在缩小字符串时,不需要马上释放多余的空间,可以预留给下次使用。
  • 二进制安全

C 语言中是以空字符来表示字符串的结束,而一些二进制文件内容可能是包含空字符串的,因此 C 语言字符串无法正确读取,所以 SDS 都是以二进制方式处理 buf,并且不以空字符串判断是否结束,而是用 len 来判断。

# 1.4 使用场景

  • 缓存,存储热点数据作为缓存,降低持久化数据库的读写压力。
  • 分布式锁
  • 计数器,可以使用 incr 命令

# 2. List

# 2.1 简单使用

命令 说明
lpush 将值从左边推入列表
rpush 将值从右边推入列表
lpop 将值从列表左边弹出返回
rpop 将值从列表右边弹出返回
lrange 根据索引查看列表中的数据
127.0.0.1:6379> lpush mylist 1 2 3
(integer) 3

127.0.0.1:6379> lrange mylist 0 -1
1) "3"
2) "2"
3) "1"

127.0.0.1:6379> rpush mylist 4
(integer) 4

127.0.0.1:6379> lrange mylist 0 -1
1) "3"
2) "2"
3) "1"
4) "4"

127.0.0.1:6379> lpop mylist
"3"

127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "4"

127.0.0.1:6379> rpop mylist
"4"

127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"

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

# 2.2 数据编码

在 redis3.2 之前使用的是 ziplistlinkedlist 两种编码方式。在数据量少的时候使用 ziplist,而数据多的时候使用 linkedlist。

而之后的版本使用的是 quicklist

127.0.0.1:6379> object encoding mylist
"quicklist"

1
2

# 2.3 压缩列表(ziplist)

该数据结构类似于一个数组,在数组的表头添加三个字段 zlbytes、zltail、zlen,再在表尾添加 zlend 表示列表结束。

  • zlbytes:内存字节数
  • zltail:列表尾偏移量
  • zllen:列表元素个数
  • zlend:列表结束标记

通过这几个字段可以快速定位第一个元素和最后元素,时间复杂度为 O(1)。

除了表头与表尾之外,其他元素都是节点,节点的数据结构如下图:

  • previous_entry_length : 上一个节点的长度。可以推算出上一个节点的地址
  • encoding: 该节点的数据类型及长度
  • content: 节点的值

连锁更新问题

对于 previous_entry_length:

  • 前一个节点长度小于 254 字节,previous_entry_length 长度需要用 1 字节保存长度值
  • 前一个节点长度大于 254 字节,previous_entry_length 长度需要用 5 字节保存长度值

有这一种场景,如果压缩列表中有多个连续的节点长度在 250-253 字节之间,这些节点 previous_entry_length 使用 1 个字节存储大小。

  • 当在最前方添加一个节点大于等于 254 字节的节点时,后面的节点 previous_entry_length 都需要从 1 字节扩充到 5 字节,起到连锁更新,后面的节点都需要更新。

  • 当删除其中 small 节点时,其后节点 previous_entry_length 需要保存前面的 big 节点,需要扩充节点空间,并且后面发生连锁更新

虽然连锁更新复杂度很高,但是造成的可能性很低。

  • 长度需要在 250 到 253 字节之间
  • 被更新的节点数量不多,也不会造成性能影响

优点

  • 内存地址连续,减少了内存碎片化,可以使用到缓存进行优化
  • 如果是双向链表,节点的头尾都需要指针来指向上一个或者下一个节点,而压缩列表省去了该部分内存空间的占用。

缺点

  • 对于删除和插入操作都需要移动该节点位置后面的元素,并且可能会触发连锁更新反应

# 2.4 快速列表(quicklist)

quicklist 是压缩列表和双向链表结合实现的,在宏观上它是一个双向链表,然后其每个结点上都挂了一个 ziplist。如下图所示:

ziplist 在列表数量大的时候性能会下降,很难分配一大块连续的内存空间,并且在删除和插入操作性能下降的更为厉害。

而双向链表会导致大量的碎片化空间,而且每个节点的头尾指针都会占用一定的内存空间。

而 quicklist 算是在两者之间取得了一个平衡。

# 2.5 使用场景

  • 消息队列
  • 列表及分页展示

# 3. Set

是一个无序的集合,集合元素是唯一的,会自动对添加的数据进行去重。

# 3.1 简单使用

命令 说明
SADD 向集合添加一个或多个元素
SCARD 获取集合的数量
SMEMBERS 获取集合全部元素
SISMEMBER 判断集合中是否指定元素
127.0.0.1:6379> sadd myset zheng wen feng zheng
(integer) 3

127.0.0.1:6379> scard myset
(integer) 3

127.0.0.1:6379> smembers myset
1) "zheng"
2) "feng"
3) "wen"

127.0.0.1:6379> sismember myset feng
(integer) 1

1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 数据编码

该数据类型使用数据编码的是 intset 或者 hashtable,当 set 满足以下条件时,使用的 inset,其他情况都是 hashtable。

  • 当所有的元素都是整数值时
  • 元素个数不超过 512 个时
127.0.0.1:6379> sadd myset2 1 2 4 4
(integer) 3

127.0.0.1:6379> SMEMBERS myset2
1) "1"
2) "2"
3) "4"

127.0.0.1:6379> OBJECT ENCODING myset2
"intset"

1
2
3
4
5
6
7
8
9
10

# 3.3 整型数组(intset)

intset 数据结构如下所示:

typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;

1
2
3
4
5
6
7
8

  • encoding:contents 中存储的数据类型取决于该字段
  • length: 数组长度
  • contnets: 存储数据的整型数组,数据的数据类型取决于 encoding,从小到大排序

contents 数组中每一个元素的类型都是由 encoding 决定的,但是当原来的数据类型是 int16 时,现在要插入一个 int32 时,该如何操作呢?

升级

这个时候需要对 contents 中的每个元素都进行升级:

  • 根据新元素的类型,扩大 contents 数组的空间大小
  • 将数组的所有元素转换成新元素相同的类型并放入数组中
  • 最后改变 encoding 的值

这么做的好处是,当所有的数据都是小整型时,可以节省内存的开销。

目前不支持降级。

# 3.4 使用场景

  • 标签,给用户打上标签,以标签为 key,用户 ID 为 value 放入 set 中
  • 点赞,收藏等,利用 set 的唯一特性。

# 4. Sorted Set

是一个不允许重复元素的集合,但是每个元素会关联一个分数,可以通过分数对集合进行排序。

# 4.1 简单使用

命令 说明
ZADD 向集合添加一个或多个元素
ZRANGE 根据位置查询获取集合多个元素
ZREM 如果元素存在集合中,则进行删除
127.0.0.1:6379> zadd myzset 100 zheng 99 wen 80 feng
(integer) 3

127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "wen"
3) "zheng"

127.0.0.1:6379> zadd myzset 91 hello
(integer) 1

# 默认是根据score升序查询
127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "hello"
3) "wen"
4) "zheng"

127.0.0.1:6379> zrem myzset wen
(integer) 1

127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "hello"
3) "zheng"

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

# 4.2 数据编码

该类型的数据编码可以为 ziplist 或者 skiplist,当满足以下条件时,使用的 ziplist,其他情况是 skiplist

  • 元素数量小于 128
  • 搜索元素长度小于 64

当数据编码为 skiplist 时,redis 中使用的存储结构是 zset 数据结构,其中包含了 zskiplistdict

typedef struct zset {
    zskiplist *zs1;
    dict *dict;
}

1
2
3
4

通过 zskiplist 可以快速的范围查询,dict 可以快速定位单个元素。

# 4.3 跳表(skiplist)

在有序的链表上添加了多级索引,通过索引位置的跳转,实现数据的快速定位。如下图所示:

优点:

  1. 在通过范围查询或排序时,可以在跳表中先找到最小值,然后再在最底层链表中遍历获取。

# 4.4 使用场景

  • 排行榜,通过给视频、文章打分,然后进行排序展示

# 5. hash

用于存储 string 类型的键值对,适用于存储对象。

# 5.1 简单使用

命令 说明
hset 设置键值对
hget 获取指定 hash 的键对应的值
hgetall 获取指定 hash 中的所有键值对
hdel 删除指定 hash 中的键值对
127.0.0.1:6379> hset person name zhangsan
(integer) 1

127.0.0.1:6379> hset person age 18
(integer) 1

127.0.0.1:6379> hget persion name
"zhangsan"

127.0.0.1:6379> hgetall person
1) "name"
2) "zhangsan"
3) "age"
4) "18"

127.0.0.1:6379> hdel person age
(integer) 1

127.0.0.1:6379> hgetall person
1) "name"
2) "zhangsan"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5.2 数据编码

使用的数据编码是 ziplisthashtable,满足以下条件选择使用 ziplist,其他情况使用 hashtable

  • hash 对象保存的键和值字符串长度都小于 64 字节
  • hash 对象保存的键值对数量小于 512

# 5.3 使用场景

  • 用来存储对象信息

# 6. 总结

redis 之所以快,正是因为其有着丰富的数据结构,所以我们需要理解它们,在设计方案时,就能正确的选择数据类型来实现我们的业务需求。

# 7. 相关链接