:五大数据类型与最常用命令,一次讲清)
个人主页 我不会起名字322 欢迎各位大佬莅临其他栏目 技术栈学习笔记 其他栏目 力扣Hot100题目解析 其他栏目 Go项目学习笔记 文章目录Redis 入门二五大数据类型与最常用命令一次讲清一、String最基础的类型承担了最多的活儿二、List两端进出的有序序列三、Hash一个 key 装一组字段四、Set自动去重的无序集合五、ZSet带分数、能排序的集合六、通用命令与类型无关的那几个keys 与 scanexists、type、del、renameexpire、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失败返回 nilget key取值key 不存在返回 nilmset/mget一次写 / 读多个 key减少网络往返incr/decr值加 1 / 减 1key 不存在时按 0 起算incrby/decrby按指定步长加减只能作用于整数incrbyfloat按浮点步长加减getset key value写入新值同时把旧值返回append/strlen尾部追加内容 / 取长度getrange/setrange按偏移读子串 / 按偏移覆盖真实输出127.0.0.1:6379setarticle:1001:views0OK127.0.0.1:6379incr article:1001:views(integer)1127.0.0.1:6379incrby article:1001:views9(integer)10127.0.0.1:6379incrbyfloat article:1001:score1.51.5127.0.0.1:6379incr article:1001:score(error)ERR value is not an integer or out of range127.0.0.1:6379setlock:order:90011NX EX30OK127.0.0.1:6379setlock:order:90011NX EX30(nil)127.0.0.1:6379ttl lock:order:9001(integer)26127.0.0.1:6379getset article:1001:views010127.0.0.1:6379mset user:1:name tom user:2:name jerry OK127.0.0.1:6379mget user:1:name user:2:name1)tom2)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:6379rpush queue:mail m1 m2 m3(integer)3127.0.0.1:6379lrange queue:mail0-11)m12)m23)m3127.0.0.1:6379lpop queue:mailm1127.0.0.1:6379llen queue:mail(integer)2127.0.0.1:6379lpush stack:undo u1 u2(integer)2127.0.0.1:6379lrange stack:undo0-11)u22)u1127.0.0.1:6379lpop stack:undou2127.0.0.1:6379rpush feed:1 a1 a2 a3 a4(integer)4127.0.0.1:6379ltrim feed:101OK127.0.0.1:6379lrange feed:10-11)a12)a2队列和栈的差别就在两端怎么配rpush进、lpop出先到先处理是一个先进先出队列lpush进、lpop出后进先出就是一个栈上面的stack:undo就是这种用法。典型场景异步任务队列生产者rpush消费者用blpop阻塞等待没任务时不空转、消息/邮件待发列表、只展示最近 N 条的动态流lpushltrim 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 和 valuehdel key f删除字段hexists key f判断字段是否存在hlen key字段个数hkeys/hvals key所有 field / 所有 valuehincrby/hincrbyfloat key f n字段按整数 / 浮点步长自增hscan key cursor游标式遍历字段真实输出127.0.0.1:6379hset user:1001 nametomage18cityhz(integer)3127.0.0.1:6379hget user:1001 nametom127.0.0.1:6379hincrby user:1001 age1(integer)19127.0.0.1:6379hkeys user:10011)name2)age3)city127.0.0.1:6379hgetall user:10011)name2)tom3)age4)195)city6)hz127.0.0.1:6379hdel user:1001 city(integer)1127.0.0.1:6379hlen user:1001(integer)2127.0.0.1:6379hget user:1001 email(nil)hset的返回值是新建了几个字段key 本来不存在、三个字段全是新建的所以返回 3对已存在的 field 再写一次返回 0。典型场景缓存用户、商品这类对象改一个字段只发一条hset不用把整个对象读出来改完再写回去购物车用cart:用户id做 keyfield 是商品 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否返回 0srandmember key [count]随机取 count 个取完仍留在集合里spop key [count]随机弹出 count 个取走即移出sinter/sunion/sdiff交集 / 并集 / 差集sinterstore/sunionstore/sdiffstore运算结果存进新 keysmove src dst m把成员从一个集合搬到另一个sscan key cursor游标式遍历成员真实输出127.0.0.1:6379sadd article:1001:like u1 u2 u3 u2(integer)3127.0.0.1:6379smembers article:1001:like1)u12)u23)u3127.0.0.1:6379sismember article:1001:like u2(integer)1127.0.0.1:6379sadd article:1002:like u2 u3 u4(integer)3127.0.0.1:6379sinter article:1001:like article:1002:like1)u22)u3127.0.0.1:6379sunion article:1001:like article:1002:like1)u12)u23)u34)u4127.0.0.1:6379sdiffarticle:1001:like article:1002:like1)u1127.0.0.1:6379scard article:1001:like(integer)3127.0.0.1:6379srandmember article:1001:like21)u12)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 countzrevrangebyscore key max min分数区间倒序取参数顺序是 max 在前zrem/zremrangebyrank/zremrangebyscore删除成员 / 按排名删 / 按分数区间删zrangebylex/zlexcount分数相同时按字典序区间取zunionstore/zinterstore多集合运算分数相加后落库真实输出127.0.0.1:6379zadd rank:20240501100user1200user2150user3(integer)3127.0.0.1:6379zrevrange rank:2024050102WITHSCORES1)user22)2003)user34)1505)user16)100127.0.0.1:6379zrevrank rank:20240501 user3(integer)1127.0.0.1:6379zincrby rank:2024050150user1150127.0.0.1:6379zrangebyscore rank:20240501150200WITHSCORES1)user12)1503)user34)1505)user26)200127.0.0.1:6379zrevrangebyscore rank:20240501 inf-infWITHSCORES LIMIT0101)user22)2003)user34)1505)user16)150127.0.0.1:6379zcount rank:20240501150200(integer)3127.0.0.1:6379zscore rank:20240501 user1150注意zrevrank返回 1是因为排名从 0 开始算user2 占的是 0zrangebyscore里 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 expireRedis 会把参数格式、返回值、时间复杂度一并列出来help string这种写法还能按类型分组列出整组命令。keys 与 scankeys pattern按通配符返回匹配的 keykeys *就是全量。问题在于 Redis 处理命令是单线程的keys会把整个 key 空间从头扫一遍扫描期间其他请求只能排队百万级 key 的实例上执行一次可能卡住好几秒。线上应该禁用keys改用游标式的scan127.0.0.1:6379scan0MATCH user:* COUNT101)172)1)user:1:name2)user:2:name127.0.0.1:6379scan17MATCH user:* COUNT101)02)(empty array)返回的第一个值是下一轮的游标拿到 0 就说明遍历结束。COUNT只是给 Redis 的一个提示每次返回多少条不保证也可能重复返回同一个元素客户端要自己去重。exists、type、del、rename127.0.0.1:6379exists user:1001 user:9999(integer)1127.0.0.1:6379typeuser:1001hash127.0.0.1:6379typerank:20240501 zset127.0.0.1:6379renamerank:20240501 rank:top OK127.0.0.1:6379del queue:mail stack:undo(integer)2127.0.0.1:6379del not:exist(integer)0exists支持一次传多个 key返回的是存在的个数type返回 string、list、set、zset、hash、stream 或 none排查问题时很有用rename在目标 key 已存在时会直接覆盖不想覆盖就用renamenxdel返回真正删掉的个数。另外del是同步删除删一个几十万元素的集合会阻塞主线程Redis 4.0 之后可以用unlink让删除在后台完成。expire、ttl、persist127.0.0.1:6379expire user:100160(integer)1127.0.0.1:6379ttl user:1001(integer)58127.0.0.1:6379persist user:1001(integer)1127.0.0.1:6379ttl user:1001(integer)-1127.0.0.1:6379ttl not:exist(integer)-2ttl返回 -1 表示这个 key 没有设置过期时间返回 -2 表示 key 不存在已经过期被删掉也算不存在毫秒版本是pexpire和pttlpersist用来摘掉过期时间。这里有个很容易踩的坑对一个已经设了过期时间的 key 再执行一次set过期时间会被清掉key 变成永不过期想保留就加上KEEPTTL参数。小结该选哪个类型你的需求选它核心命令缓存一个值或一段 JSON、做计数器Stringset/get/incr/set NX EX简单队列、最近 N 条、撤销栈Listrpushlpop/lpushltrim/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 前面多了一串乱码取出来的对象报类型转换异常根因都在序列化配置上。