Redis 的原理和使用(2)

二、常见数据类型

Redis 对外提供 string、hash、list、set、zset 五种数据结构。

理解每种结构的特点与常见命令,是使用 Redis 的基础。

正式展开之前,先补充三块预备知识:全局命令、内部编码与单线程模型。

2.1 预备知识

(1)全局命令

无论哪种数据类型,都可以使用一组通用命令管理键,常见几个命令的语法与复杂度汇总在下表:

命令 作用 复杂度
KEYS pattern 按模式匹配全部键,会阻塞服务,生产环境谨慎使用。 O(n)
EXISTS key 判断键是否存在。 O(1)
DEL key key ... 删除一个或多个键,返回删除个数。 O(1)
EXPIRE key seconds 为键设置过期时间,到期后由 Redis 淘汰。 O(1)
TTL key 返回剩余过期时间,-1 表示未设置过期时间,-2 表示键不存在。 O(1)
TYPE key 返回键对应的数据类型。 O(1)

过期与淘汰的配合可以用一条时间轴理解:键在 n 秒内可以正常读取,n 秒过后被 Redis 淘汰,再次读取会得到不存在的提示:

EXPIRE 与 TTL 还提供毫秒级版本 PEXPIRE 与 PTTL,用于精度要求更高的场景。

以命令示例感受一下执行效果:

复制代码
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10

(2)内部编码

对外是五种数据结构,对内每种结构还有多种底层实现,Redis 会根据数据规模与形态动态选择。

两个好处:一是改进内部编码时对外命令无感知,二是不同编码可以在各自场景发挥优势。五种结构与内部编码的对应关系见表:

数据结构 内部编码
string int、embstr、raw
hash ziplist、hashtable
list ziplist、linkedlist
set intset、hashtable
zset ziplist、skiplist

内部编码可以通过 object encoding 命令查看:

复制代码
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> rpush mylist e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding mylist
"ziplist"

单线程能跑到每秒万级别,原因可以归结为三点:

  • 纯内存访问。内存响应时长大约 100 纳秒,这是高吞吐的重要基础。

  • 非阻塞 IO。Redis 使用 epoll 作为 IO 多路复用实现,把连接、读写、关闭都注册为事件,不在网络 IO 上浪费过多时间。

  • 单线程避免了线程切换与竞态消耗,也让数据结构和算法实现更简单。

单线程也有代价:单条命令执行时间过长,会拖住后面所有命令的响应。

因此 Redis 面向的是快速执行的场景,耗时操作需要谨慎设计。

2.2 String 字符串

字符串是最基础的类型。

两点留意:Redis 中所有键的类型都是字符串,其他数据结构建立在字符串之上;

字符串的值可以是普通字符串、JSON / XML 文本、数字,甚至图片、音频等二进制流,单个值上限 512 MB。

由于内部按二进制流保存,Redis 不处理编码问题,客户端用什么编码写入,就按什么编码存储。

(1)常用命令

字符串命令按功能可以分成几组,下表摘录高频命令:

命令 作用
SET key value 设置值,键已存在时覆盖并清除原有过期时间。
GET key 获取值。
MSET / MGET 批量设置 / 批量获取,一次往返完成多个键的操作。
INCR / DECR 值自增 / 自减 1,返回新值,可延伸出 INCRBY、DECRBY。
SETNX key value 键不存在时才设置成功,返回 1;已存在返回 0。
SETEX key seconds value 设置值的同时指定过期时间,等于 SET + EXPIRE。

批量命令的价值可以从数据上直观感受。

假设网络往返耗时 1 毫秒、命令执行耗时 0.1 毫秒,1000 次 get 与一次 mget 1000 个键的时间差距明显:

操作 时间
1000 次 get 1000 × 1 + 1000 × 0.1 = 1100 毫秒
1 次 mget 1000 个键 1 × 1 + 1000 × 0.1 = 101 毫秒

批量操作能显著提升吞吐,但单次批量携带的键数量也要控制,避免单条命令执行时间过长拖累整个服务。

(2)内部编码

字符串有三种内部编码,Redis 根据值的类型和长度动态选择:

  • int:值是整数时使用,节省内存。

  • embstr:值长度不超过 44 字节时使用,连续分配内存,读取更快。

  • raw:长度较长的字符串使用。

    127.0.0.1:6379> set number 10086
    OK
    127.0.0.1:6379> object encoding number
    "int"
    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> object encoding hello
    "embstr"
    127.0.0.1:6379> set long_str "a very very long string ..."
    OK
    127.0.0.1:6379> object encoding long_str
    "raw"

(3)使用场景

字符串类型覆盖了缓存、计数、会话、验证码等常见需求:

  • 缓存:以 uid 为键存放用户信息,命中缓存直接返回,未命中再回源存储层并回写缓存。这套读写流程可以用下图概括:
  • 计数:视频播放数、评论数等用 INCR 系列命令完成自增

  • 会话:分布式 Web 服务把 Session 分散在各服务器上,用户刷新时可能被均衡到另一台机器导致重新登录

  • 短信验证码:限制用户每分钟获取验证码不超过 5 次,用字符串配合过期时间实现频控。

  • 分布式锁:利用 SETNX 的"仅当键不存在时写入"特性,作为锁的最简实现。

2.3 Hash 哈希

哈希类型指值本身又是一个键值对结构,形如 key = { field1: value1, field2: value2, ... }。

主流语言中它被称为哈希、字典或映射。

(1)常用命令

命令 作用
HSET key field value 设置指定字段的值,字段不存在时新建。
HGET key field 获取指定字段的值。
HMSET / HMGET 批量设置 / 批量获取多个字段。
HGETALL key 获取全部字段与值。
HINCRBY key field increment 字段值按指定步长自增。
HSETNX key field value 字段不存在时才设置成功。
复制代码
127.0.0.1:6379> hset user:1 name james
(integer) 1
127.0.0.1:6379> hset user:1 age 28
(integer) 1
127.0.0.1:6379> hgetall user:1
1) "name"
2) "james"
3) "age"
4) "28"

(2)内部编码

哈希的内部编码有两种:ziplist 与 hashtable。

一旦某个 value 超过 64 字节,或者字段个数超过 512,编码会转换为 hashtable:

复制代码
127.0.0.1:6379> object encoding user:1
"ziplist"
127.0.0.1:6379> hset user:2 big_field "a value longer than 64 bytes ..."
(integer) 1
127.0.0.1:6379> object encoding user:2
"hashtable"

(3)使用场景

哈希适合存放对象类数据,例如用户信息、商品信息。

使用哈希也要注意控制 ziplist 与 hashtable 之间的转换,转换过程会带来额外的内存消耗。

2.4 List 列表

列表类型用来存储多个有序的字符串,元素允许重复。

列表按插入顺序排列,两端操作效率高:lpush 从左侧插入,rpush 从右侧插入,lpop / rpop 从两端弹出。

(1)常用命令

命令 作用
LPUSH / RPUSH 从左侧 / 右侧插入一个或多个元素。
LPOP / RPOP 从左侧 / 右侧弹出元素。
LRANGE key start stop 按索引范围获取元素,支持正数与倒数下标。
LINDEX key index 获取指定索引位置的元素。
LLEN key 返回列表长度。
LREM key count value 删除指定数量的匹配元素。
BRPOP key key ... timeout 阻塞式弹出,队列为空时等待指定时间。

列表的获取与删除是两个不同操作:lindex 只读不改,列表长度不变;lrem 会真正删除元素,长度随之变化:

(2)内部编码

列表同样有两种编码。

元素较少且没有大元素时用 ziplist;元素个数超过 512,或者某个元素长度超过 64 字节,转换为 linkedlist:

复制代码
127.0.0.1:6379> rpush listkey e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey
"ziplist"
127.0.0.1:6379> rpush listkey e1 e2 e3 ... e512 e513
(integer) 513
127.0.0.1:6379> object encoding listkey
"linkedlist"

(3)使用场景

列表的经典用法是消息队列。

生产者用 lpush 从左侧插入,消费者用 brpop 阻塞式从右侧争抢,多个消费者客户端分摊消费,兼顾负载均衡与高可用:

队列还可以按频道拆分:消费者通过 brpop 监听不同的键,实现订阅不同频道的效果,频道之间互不干扰:

微博这类业务按时间倒序展示动态,列表天然有序且支持按索引分页,配合哈希保存每条的属性,可以拼出完整的分页列表。

2.5 Set 集合

集合类型存储无序、不重复的字符串。集合提供了丰富的集合间运算,适合标签、社交类业务。

(1)常用命令

命令 作用
SADD key member member ... 添加一个或多个元素,已存在的元素被忽略。
SREM key member member ... 删除一个或多个元素,返回实际删除个数。
SMEMBERS key 返回集合全部元素。
SISMEMBER key member 判断元素是否在集合中。
SCARD key 返回元素个数。
SPOP key count 随机弹出元素,适合抽奖场景。
SINTER / SUNION / SDIFF 求交集 / 并集 / 差集。
复制代码
redis> SADD myset "one" "two" "three"
(integer) 3
redis> SREM myset "one"
(integer) 1
redis> SMEMBERS myset
1) "three"
2) "two"

(2)内部编码

集合有两种编码:元素都是整数且个数小于 set-max-intset-entries( 512)时使用 intset,节省内存;不满足条件时使用 hashtable:

复制代码
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"
127.0.0.1:6379> sadd setkey "hello"
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"

(3)使用场景

交集、并集、差集的概念可以用集合间的圆圈关系表示:

典型场景包括:

  • 标签系统:给文章打标签,用集合保存每个标签下的文章 id,支持按标签检索。

  • 社交关系:共同好友用 SINTER 求交集,关注关系用集合存粉丝 / 关注列表。

  • 抽奖:SPOP 随机弹出参与者,一次一个或多个。

2.6 Zset 有序集合

有序集合在集合"元素不重复"的基础上,为每个元素关联一个分数,元素按分数排序。用户排行榜就是它的典型应用:

列表、集合、有序集合三者容易混淆,异同点可以对比记忆:

数据结构 是否允许重复元素 是否有序 有序依据 应用场景
列表 是 是 索引下标 时间轴、消息队列
集合 否 否 无 标签、社交
有序集合 否 是 分数 排行榜、社交

(1)常用命令

命令 作用
ZADD key score member 添加元素并设置分数,元素已存在则更新分数。
ZRANGE key start stop 按分数从小到大的顺序返回指定索引范围元素。
ZREVRANGE key start stop 按分数从大到小的顺序返回元素。
ZSCORE key member 获取元素分数。
ZRANK / ZREVRANK 获取元素排名(正序 / 倒序)。
ZINCRBY key increment member 元素分数按步长自增。

(2)集合间操作

有序集合也支持交集、并集等运算,合并过程中按元素聚合分数,聚合方式有 sum、max、min,还可为各集合设置权重:

复制代码
ZINTERSTORE dest 2 user:ranking:1 user:ranking:2
# 合并后各元素分数按 sum 方式聚合
# mike 168(91 + 77),martin 875(625 + 250),tom 1139(888 + 251)

(3)内部编码

有序集合的编码同样是 ziplist 与 skiplist 的组合:元素个数少于 128 且成员长度不超过 64 字节时用 ziplist,超过后转换为 skiplist:

复制代码
127.0.0.1:6379> object encoding myzset
"ziplist"
127.0.0.1:6379> zadd myzset 1.0 a 2.0 b ... 129 个元素
(integer) 129
127.0.0.1:6379> object encoding myzset
"skiplist"

(4)使用场景

有序集合的应用集中在需要按分数排序与排名计算的场景:

  • 游戏排行:玩家分数作为 score,ZREVRANGE 取出前 N 名。

  • 热度榜单:按点击、销量等指标排序,ZINCRBY 实时更新分数。

  • 时间排序:把时间戳作为分数,实现按时间范围检索的数据结构。

2.7 键遍历

全库键遍历用 KEYS 会阻塞服务,Redis 提供了渐进式遍历命令 SCAN。

它每次返回一部分键,通过游标继续迭代,多个客户端可以分头并行扫描,过程中不会阻塞主线程:

复制代码
127.0.0.1:6379> scan 0
1) "1024"      # 下一次遍历的游标
2)  1) "key:1"
    2) "key:12"
    ...
127.0.0.1:6379> scan 1024
1) "0"         # 游标归零,遍历结束
2)  1) "key:24"

SCAN 也提供 MATCH 模式与 COUNT 提示参数,用于按模式过滤和调整单次返回数量。

数据库管理方面,Redis 默认有 16 个逻辑库(0-15),用 SELECT 切换,用 FLUSHDB 清空当前库、FLUSHALL 清空全部库。

相关推荐
Java后端的Ai之路1 小时前
19-ReactJS最佳实践
前端·react.js·前端框架
“AI国潮设计-小江”1 小时前
《【AIGC实战】Python+SDXL批量生成“英歌舞树莓香草布丁”IP,附保持角色一致性的代码逻辑》
开发语言·人工智能·python·prompt·aigc
betazhou1 小时前
Apache Seatunnel 抽取SQL server实时同步到Oracle(四)
数据库·oracle·apache·sql server·seatunnel
l1t1 小时前
DeepSeek总结的PostgreSQL 的 MVCC 很糟。其他所有的也一样。第1部分
数据库·postgresql
溪语流沙1 小时前
【Web全栈进阶】文件上传与图片展示:早报站的第一张图片
前端·vue.js·python·fastapi
川石课堂软件测试1 小时前
JMeter异步接口测试实战
数据库·python·jmeter·mysql·kubernetes·测试用例·prometheus
alwaysrun1 小时前
Web之wasm-bindgen详解
前端·wasm·bindgen
aichitang20241 小时前
Meta Muse Spark Open:816B 巨型开放权重多模态 Agent 模型架构、部署实战与社区首测全解
大数据·人工智能·python·算法·ai·架构·spark
有同事要进步1 小时前
leecode每日一题 第66题:加1
python