☰
Redis 入门(二):五大数据类型与最常用命令,一次讲清
2026/10/8 8:29:40 网站建设 项目流程
个人主页: > 我不会起名字322 < (欢迎各位大佬莅临😊)
其他栏目: > 技术栈学习笔记 <
其他栏目: > 力扣Hot100题目解析 <
其他栏目: > Go项目学习笔记 <

文章目录

  • Redis 入门(二):五大数据类型与最常用命令,一次讲清
    • 一、String:最基础的类型,承担了最多的活儿
    • 二、List:两端进出的有序序列
    • 三、Hash:一个 key 装一组字段
    • 四、Set:自动去重的无序集合
    • 五、ZSet:带分数、能排序的集合
    • 六、通用命令:与类型无关的那几个
      • keys 与 scan
      • exists、type、del、rename
      • expire、ttl、persist
    • 小结:该选哪个类型

Redis 入门(二):五大数据类型与最常用命令,一次讲清

上一篇我们把 Redis 装好、也用redis-cli连上跑通了ping。连接通了之后,紧接着的问题就是:数据到底以什么形式放进去?和 MySQL 不同,Redis 不需要先建表、先定义字段,它的类型信息挂在 value 上——同一个 key,你写进去的是普通字符串还是有序集合,决定了它能用哪些命令、适合什么业务。这篇就把最常用的五种 value 类型和配套的通用命令完整过一遍,每种类型都给出可以直接粘进redis-cli的命令、真实输出,以及线上最容易踩的坑。

一、String:最基础的类型,承担了最多的活儿

是什么:一个 key 对应一个值,值可以是普通文本、JSON 串,也可以是二进制字节。它是另外几种结构的实现基础,单个 value 上限 512MB。

常用命令

命令作用
set key value写入,key 已存在会直接覆盖
set key value EX 60/PX 60000写入并带上过期时间(秒 / 毫秒),一条命令完成
set key value NX仅当 key 不存在时写入,成功返回 OK,失败返回 nil
get key取值,key 不存在返回 nil
mset/mget一次写 / 读多个 key,减少网络往返
incr/decr值加 1 / 减 1,key 不存在时按 0 起算
incrby/decrby按指定步长加减,只能作用于整数
incrbyfloat按浮点步长加减
getset key value写入新值,同时把旧值返回
append/strlen尾部追加内容 / 取长度
getrange/setrange按偏移读子串 / 按偏移覆盖

真实输出

127.0.0.1:6379>setarticle:1001:views0OK127.0.0.1:6379>incr article:1001:views(integer)1127.0.0.1:6379>incrby article:1001:views9(integer)10127.0.0.1:6379>incrbyfloat article:1001:score1.5"1.5"127.0.0.1:6379>incr article:1001:score(error)ERR value is not an integer or out of range127.0.0.1:6379>setlock:order:9001"1"NX EX30OK127.0.0.1:6379>setlock:order:9001"1"NX EX30(nil)127.0.0.1:6379>ttl lock:order:9001(integer)26127.0.0.1:6379>getset article:1001:views0"10"127.0.0.1:6379>mset user:1:name tom user:2:name jerry OK127.0.0.1:6379>mget user:1:name user:2:name1)"tom"2)"jerry"

典型场景:缓存单条 JSON、接口计数(点赞数、播放量、库存扣减计数器)、set NX EX做分布式锁的占位、把 session 或验证码存起来并自动过期。

注意点

  • incr系列要求值是纯整数。一旦用incrbyfloat把它变成"1.5",后面再incr就会直接报错,两类命令别混着用。
  • setnx之后再单独发一条expire是两次操作,中间挂掉就留下一个永不过期的锁。要加过期时间就用一条set key value NX EX。
  • getset在 Redis 6.2 之后可以用set key value GET代替,新代码优先用后者,语义更清楚。
  • value 别塞太大。单个几百 KB 的 JSON,序列化和网络传输的开销都会明显上升;如果对象字段多、还经常只改其中一个,就该考虑 Hash。
  • append、setrange是原地改内容,适合短文本拼接,用它们反复拼一个很长的字符串会不断触发内存重新分配,代价比重新set一次还高。

二、List:两端进出的有序序列

是什么:一串按插入位置排列的元素,允许重复。可以从左边或右边压入、弹出,所以既能当队列也能当栈;元素少时底层用紧凑结构存储,变大了才转成链表形态。理论上能放 40 多亿个元素。

常用命令

命令作用
lpush/rpush key v1 v2从左侧 / 右侧插入一个或多个元素
lpop/rpop key [count]从左侧 / 右侧弹出并返回
lrange key start stop按下标范围取,0 -1表示全部
llen key元素个数
lindex key i/lset key i v取 / 改指定下标的元素
linsert key before|after pivot v在某个元素前 / 后插入
lrem key count value删除匹配的元素,count 控制方向和数量
ltrim key start stop只保留区间内的元素,其余丢弃
rpoplpush/lmove从一个列表尾部弹出并压到另一个列表头部
blpop/brpop key timeout阻塞版弹出,超时秒数写 0 表示一直等

真实输出

127.0.0.1:6379>rpush queue:mail m1 m2 m3(integer)3127.0.0.1:6379>lrange queue:mail0-11)"m1"2)"m2"3)"m3"127.0.0.1:6379>lpop queue:mail"m1"127.0.0.1:6379>llen queue:mail(integer)2127.0.0.1:6379>lpush stack:undo u1 u2(integer)2127.0.0.1:6379>lrange stack:undo0-11)"u2"2)"u1"127.0.0.1:6379>lpop stack:undo"u2"127.0.0.1:6379>rpush feed:1 a1 a2 a3 a4(integer)4127.0.0.1:6379>ltrim feed:101OK127.0.0.1:6379>lrange feed:10-11)"a1"2)"a2"

队列和栈的差别就在两端怎么配:rpush进、lpop出,先到先处理,是一个先进先出队列;lpush进、lpop出,后进先出,就是一个栈(上面的stack:undo就是这种用法)。

典型场景:异步任务队列(生产者rpush,消费者用blpop阻塞等待,没任务时不空转)、消息/邮件待发列表、只展示最近 N 条的动态流(lpush+ltrim 0 99)、撤销操作的历史栈。

注意点

  • 当队列用时一定要盯住长度。生产速度长期快过消费速度,这个列表会一直涨,最后把内存吃满。
  • lrange key 0 -1是把整个列表一次性搬回客户端。几十万条元素的列表上执行一次,网络带宽和客户端内存都会瞬间吃紧,列表类数据要分页取。
  • 用blpop做队列,消息弹出即消失,消费者拿到之后崩了这条消息就找不回来。要保证不丢,得看后面的 Stream。
  • 下标访问是 O(N) 的,lindex key 500000这种别写在接口热路径里。

三、Hash:一个 key 装一组字段

是什么:key 下面挂着一组 field-value 对,可以理解成一个很小的 Map,或者数据库里的一行记录。field 和 value 都是字符串,同一个 field 重复写就是覆盖。

常用命令

命令作用
hset key f v [f v ...]设置一个或多个字段
hsetnx key f vfield 不存在时才设置
hget key f/hmget key f1 f2取单个 / 多个字段
hgetall key取出全部 field 和 value
hdel key f删除字段
hexists key f判断字段是否存在
hlen key字段个数
hkeys/hvals key所有 field / 所有 value
hincrby/hincrbyfloat key f n字段按整数 / 浮点步长自增
hscan key cursor游标式遍历字段

真实输出

127.0.0.1:6379>hset user:1001 name"tom"age18city"hz"(integer)3127.0.0.1:6379>hget user:1001 name"tom"127.0.0.1:6379>hincrby user:1001 age1(integer)19127.0.0.1:6379>hkeys user:10011)"name"2)"age"3)"city"127.0.0.1:6379>hgetall user:10011)"name"2)"tom"3)"age"4)"19"5)"city"6)"hz"127.0.0.1:6379>hdel user:1001 city(integer)1127.0.0.1:6379>hlen user:1001(integer)2127.0.0.1:6379>hget user:1001 email(nil)

hset的返回值是"新建了几个字段",key 本来不存在、三个字段全是新建的,所以返回 3;对已存在的 field 再写一次返回 0。

典型场景:缓存用户、商品这类对象,改一个字段只发一条hset,不用把整个对象读出来改完再写回去;购物车用cart:用户id做 key,field 是商品 id、value 是数量,加减数量直接用hincrby;把同一维度的多个计数器收在一个 key 下,避免散落成一堆 key。

注意点

  • hgetall同样是全量返回,字段上千个时就是一次慢查询。只要几个字段就hmget,要遍历全部就用hscan分批。
  • 过期时间只能挂在 key 上,Hash 里的单个 field 没有独立的 TTL。
  • 输出顺序不要依赖。hkeys、hgetall的顺序没有对外保证,需要按顺序展示就自己在客户端排。
  • 字段很少、而且每次都是整体读写的对象,直接存一个 String JSON 反而更省事;Hash 的价值在于"部分更新"。

四、Set:自动去重的无序集合

是什么:一组不重复的字符串,没有顺序,也不能按下标取第几个。底层是哈希表(元素都是整数且数量不多时会用更紧凑的整数集合),单个成员的增删和判断是否存在都是 O(1)。

常用命令

命令作用
sadd key m1 m2添加成员,已存在的会被忽略
srem key m移除成员
scard key成员个数
smembers key返回全部成员
sismember key m是否是成员,是返回 1,否返回 0
srandmember key [count]随机取 count 个,取完仍留在集合里
spop key [count]随机弹出 count 个,取走即移出
sinter/sunion/sdiff交集 / 并集 / 差集
sinterstore/sunionstore/sdiffstore运算结果存进新 key
smove src dst m把成员从一个集合搬到另一个
sscan key cursor游标式遍历成员

真实输出

127.0.0.1:6379>sadd article:1001:like u1 u2 u3 u2(integer)3127.0.0.1:6379>smembers article:1001:like1)"u1"2)"u2"3)"u3"127.0.0.1:6379>sismember article:1001:like u2(integer)1127.0.0.1:6379>sadd article:1002:like u2 u3 u4(integer)3127.0.0.1:6379>sinter article:1001:like article:1002:like1)"u2"2)"u3"127.0.0.1:6379>sunion article:1001:like article:1002:like1)"u1"2)"u2"3)"u3"4)"u4"127.0.0.1:6379>sdiffarticle:1001:like article:1002:like1)"u1"127.0.0.1:6379>scard article:1001:like(integer)3127.0.0.1:6379>srandmember article:1001:like21)"u1"2)"u3"

上面的sadd连传了两次 u2,返回 3 而不是 4,说明重复成员没有被重复计入——这个返回值本身就能当"是否第一次参加"的判断依据。

典型场景:点赞、投票、活动参与名单这类需要天然去重的数据;抽奖池(smembers看池子,srandmember抽奖但保留池子,spop抽出即移出);共同好友、共同关注用sinter;"我关注了但他没关注"用sdiff;标签、兴趣人群匹配。

注意点

  • smembers会返回集合里的每一个成员,百万级的大集合上执行就是事故。线上统计用scard,判断用sismember,遍历用sscan。
  • sinter、sunion、sdiff的耗时跟参与运算的集合大小成正比。多个大集合一起算会明显占用主线程,能定期算好缓存的场景就用带store的命令把结果落到固定 key 上复用。
  • 抽奖要区分语义:spop是抽走就不在池子里了,srandmember是抽完还在,会不会重复中奖完全由这一条命令决定。
  • Set 是无序的,smembers的返回顺序不能当作稳定排序使用。
  • 集合元素多、又需要频繁做交并差时,可以先算一次、把结果存成另一个 key 并设上过期时间,让后面的请求直接读结果,而不是每次都重新算一遍。

五、ZSet:带分数、能排序的集合

是什么:ZSet(有序集合)同样不允许成员重复,区别是每个成员都绑了一个 score。Redis 按 score 从小到大维护顺序,score 相同时按成员的字典序排列;写入、改分、按排名或分数区间查询都是 O(log N) 级别,底层是跳表加哈希表。

常用命令

命令作用
zadd key score member [score member ...]添加成员或修改已有成员的分数
zincrby key n member给成员累加分数,成员不存在按 0 起算
zscore key member查成员的分数
zcard key成员个数
zcount key min max分数区间内的成员数量
zrank/zrevrank key member升序 / 降序排名,从 0 开始
zrange/zrevrange key start stop [WITHSCORES]按排名升序 / 降序取一段
zrangebyscore key min max按分数区间取,支持LIMIT offset count
zrevrangebyscore key max min分数区间倒序取,参数顺序是 max 在前
zrem/zremrangebyrank/zremrangebyscore删除成员 / 按排名删 / 按分数区间删
zrangebylex/zlexcount分数相同时按字典序区间取
zunionstore/zinterstore多集合运算,分数相加后落库

真实输出

127.0.0.1:6379>zadd rank:20240501100user1200user2150user3(integer)3127.0.0.1:6379>zrevrange rank:2024050102WITHSCORES1)"user2"2)"200"3)"user3"4)"150"5)"user1"6)"100"127.0.0.1:6379>zrevrank rank:20240501 user3(integer)1127.0.0.1:6379>zincrby rank:2024050150user1"150"127.0.0.1:6379>zrangebyscore rank:20240501150200WITHSCORES1)"user1"2)"150"3)"user3"4)"150"5)"user2"6)"200"127.0.0.1:6379>zrevrangebyscore rank:20240501 +inf-infWITHSCORES LIMIT0101)"user2"2)"200"3)"user3"4)"150"5)"user1"6)"150"127.0.0.1:6379>zcount rank:20240501150200(integer)3127.0.0.1:6379>zscore rank:20240501 user1"150"

注意zrevrank返回 1,是因为排名从 0 开始算,user2 占的是 0;zrangebyscore里 user1 和 user3 都是 150,按字典序 user1 排在前面。

典型场景:实时排行榜,key 带上时间后缀区分日榜、小时榜,取前 10 名就是zrevrange key 0 9 WITHSCORES;分数段统计和区间名单;延时队列,把任务的到期时间戳当 score,用zrangebyscore key 0 <当前时间戳>捞出所有该执行的任务;带权重的热度排序,比如浏览数加点赞数乘系数。

注意点

  • score 是双精度浮点数,只有 53 位能精确表示整数,超过 2 的 53 次方(约 9×10^15)之后会出现精度丢失。毫秒时间戳当 score 完全没问题,但纳秒时间戳、雪花 ID 这类大整数直接当 score 就会出错;score 也不能写非数字,NaN 会直接报错。
  • 排行榜成员会一直膨胀。只关心前 100 名却把所有参与者都留着,内存和运算都在做无用功,可以定期用zremrangebyrank把尾部名次清掉,并且放到定时任务里做,不要每次写分数都顺带清理。
  • 一个成员只能有一个 score。既要保留"当前分"又要保留"历史最高分",就得开两个 zset,比较之后再更新记录最高分的那一个。
  • zrangebyscore不加LIMIT时会把命中的成员全部返回,区间开大了和lrange 0 -1一样可怕,养成带LIMIT的习惯。
  • zrangebylex只在所有成员 score 相同时才符合预期,score 不一致时结果没有意义。

六、通用命令:与类型无关的那几个

前五节回答的是"数据怎么存",这一节回答的是"key 本身怎么管"。下面这些命令不看 value 是什么类型,任何 key 都能用:判断在不在、看是什么类型、设过期、删除、改名,以及最容易被误用的keys。

顺带说一个排查技巧:拿不准某条命令的参数写法时,不用去翻文档,在客户端里敲help加命令名就行,比如help set、help expire,Redis 会把参数格式、返回值、时间复杂度一并列出来;help @string这种写法还能按类型分组列出整组命令。

keys 与 scan

keys pattern按通配符返回匹配的 key,keys *就是全量。问题在于 Redis 处理命令是单线程的,keys会把整个 key 空间从头扫一遍,扫描期间其他请求只能排队,百万级 key 的实例上执行一次可能卡住好几秒。线上应该禁用keys,改用游标式的scan:

127.0.0.1:6379>scan0MATCH user:* COUNT101)"17"2)1)"user:1:name"2)"user:2:name"127.0.0.1:6379>scan17MATCH user:* COUNT101)"0"2)(empty array)

返回的第一个值是下一轮的游标,拿到 0 就说明遍历结束。COUNT只是给 Redis 的一个提示,每次返回多少条不保证,也可能重复返回同一个元素,客户端要自己去重。

exists、type、del、rename

127.0.0.1:6379>exists user:1001 user:9999(integer)1127.0.0.1:6379>typeuser:1001hash127.0.0.1:6379>typerank:20240501 zset127.0.0.1:6379>renamerank:20240501 rank:top OK127.0.0.1:6379>del queue:mail stack:undo(integer)2127.0.0.1:6379>del not:exist(integer)0

exists支持一次传多个 key,返回的是存在的个数;type返回 string、list、set、zset、hash、stream 或 none,排查问题时很有用;rename在目标 key 已存在时会直接覆盖,不想覆盖就用renamenx;del返回真正删掉的个数。另外del是同步删除,删一个几十万元素的集合会阻塞主线程,Redis 4.0 之后可以用unlink让删除在后台完成。

expire、ttl、persist

127.0.0.1:6379>expire user:100160(integer)1127.0.0.1:6379>ttl user:1001(integer)58127.0.0.1:6379>persist user:1001(integer)1127.0.0.1:6379>ttl user:1001(integer)-1127.0.0.1:6379>ttl not:exist(integer)-2

ttl返回 -1 表示这个 key 没有设置过期时间,返回 -2 表示 key 不存在(已经过期被删掉也算不存在);毫秒版本是pexpire和pttl;persist用来摘掉过期时间。这里有个很容易踩的坑:对一个已经设了过期时间的 key 再执行一次set,过期时间会被清掉,key 变成永不过期,想保留就加上KEEPTTL参数。

小结:该选哪个类型

你的需求选它核心命令
缓存一个值或一段 JSON、做计数器Stringset/get/incr/set NX EX
简单队列、最近 N 条、撤销栈Listrpush+lpop/lpush+ltrim/blpop
一行对象的多个字段、购物车Hashhset/hget/hincrby/hgetall
去重名单、标签、共同好友、抽奖池Setsadd/sismember/sinter/spop
排行榜、延时队列、带权重的排序ZSetzadd/zrevrange/zrangebyscore/zincrby

选型的思路其实就一句话:先想清楚数据要回答什么问题——是"取出来用"(String、Hash),是"先进先出"(List),是"有没有、和谁重合"(Set),还是"谁排在前面"(ZSet)。类型选对了,命令自然简单;类型选错,后面就得靠一堆客户端代码去补。

这张表里的命令,下一篇都能在 Java 侧找到对应写法,比如hset对应opsForHash().put(...),zadd对应opsForZSet().add(...),命令会不会用,直接决定了代码写得顺不顺。

另外提一句,除了这五种,Redis 还有 Pub/Sub 和 Stream:前者是发布订阅,消息发出去就不管了,适合实时通知这类允许丢的场景;后者是带消费者组和 ACK 的消息队列,能补上 List 做队列时消息会丢的短板。这两个属于进阶内容,等把基础类型用熟了再回头看会轻松很多。

到现在为止,我们所有操作都是在redis-cli里手敲的。下一篇进入实战:用 Spring Boot 把 Redis 整合进来,讲清RedisTemplate的常用操作、StringRedisTemplate和它的区别,以及新手一定会遇到的那两个问题——存进去的 key 前面多了一串乱码,取出来的对象报类型转换异常,根因都在序列化配置上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询