二、常见数据类型
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 清空全部库。