Redis装完,redis-cli一敲,能ping通,然后呢?说句实话,很多人对redis基础常用命令的了解,就停在SET一个值、GET一个值。真实业务里一踩坑,连查一个key还剩多久过期都要翻半天文档。这篇文章就想把这层地基夯实。按理说这是最简单的内容,可越简单的东西越容易用糙。我会按实际业务里用到它们的频次来讲,把每个命令为什么这么设计、在什么场景下选它、有哪些坑我踩过或带新人时见过,一起摊开说。不管你是刚上手,还是用了一阵子但没系统整理过命令,都值得花三四十分钟过一遍。
1. 先把Redis命令的共同套路摸清,后面全是顺水推舟
如果你把Redis所有命令摊开看,会发现它们长得特别像:一个动词,后面跟着key,再跟若干参数。这其实是刻意的设计,Redis把命令当成了"一个动作 + 操作对象"来设计,所以记忆成本很低。下面把几个通用的底层规律先讲透,后面敲每类命令时,你就能举一反三。
1.1 命令格式:一个动作、一把钥匙、若干参数
绝大多数Redis命令长这样:命令动词 + key + 可选参数/值。所谓"动词",比如字符串的SET、GET,哈希的HSET、HGET,列表的LPUSH、RPUSH,集合的SADD,有序集合的ZADD。命令名本身就是动作,LPUSH就是List Push,ZADD就是Sorted Set Add,看着英文字面就能猜个大概。真遇到没见过的命令,先猜再查文档,十有八九能猜中,这种设计对记忆特别友好。
有两个细节容易被忽略,但它们影响所有命令。第一,命令关键字不区分大小写,SET和set都行,但key区分大小写,"Name"和"name"是两个完全不同的key。我写脚本时吃过亏:同一个业务里,有人拼key用userId,有人拼成userID,结果缓存命中率肉眼可见地掉了一大截。第二,Redis把key当二进制字节流处理,你塞空格、换行甚至中文进去都不会报错,但排查问题时极其痛苦。所以从一开始就要把key命名规范定下来,别等到线上几百万key才后悔。
1.2 返回值类型决定业务代码怎么写
Redis命令的返回值可以分成几类:状态回复、整数回复、批量字符串、多行字符串、nil,以及错误。SET成功返回OK,INCR返回自增后的整数值,GET返回对应的字符串值,MGET返回一组值,key不存在时GET返回nil,类型不匹配时返回类似WRONGTYPE的错误。
最容易被忽略的是nil和空对象的区别。GET一个不存在的key返回nil,但某个key存在而value是空字符串时,返回的是空字符串,不是nil。哈希的HGET也是,字段不存在和整个key不存在都可能返回nil,但这两种场景的业务语义完全不同——前者是"对象在但属性没设",后者是"对象压根不存在"。写代码判断要不要回源数据库时,最好先用EXISTS或TYPE确认一下,别把两种情况都当成"直接查数据库"。
1.3 key命名规范:冒号分层,一眼看懂
Redis本身不强制key长什么样,但实际项目中我强烈推荐"业务:实体:ID:属性"这种冒号分层风格。比如order:123:status、user:10086:profile。好处有三个:可读性好,出问题一眼看出归属;不同业务之间天然隔离,不容易冲突;方便后续用SCAN按前缀做批量操作。没有了规范,时间一长线上全是没人认识的key,想清理都无从下手。
另外key长度也要控制。极端情况下,一个key是一大串无意义的哈希值,value才几十字节,内存大头全浪费在key上。我以前帮项目清理过历史冗余,光key本身的无谓占用就占了内存的两三成。短一点、有语义一点,准没错。
2. 字符串类型:缓存与计数场景的顶梁柱
字符串是Redis里最基础的结构,也是业务中使用频率最高的类型。它既简单又有一些容易被忽视的参数和场景,我先从最常用的命令说起,再讲那些"知道的人不多但特别好用"的细节。
2.1 SET/GET/MSET/MGET:最基础,但参数经常被忽略
最基本的SET key value和GET key不用多说,但很多人不知道的是,SET后面其实可以跟一堆参数来扩展行为。比如EX seconds表示多少秒后过期、PX milliseconds表示多少毫秒后过期、NX表示只有key不存在时才写入、XX表示只有key已存在时才写入。一个SET,能省掉后续好几条命令。
我日常最喜欢用的是"SET + EX + NX"的组合。举个例子,登录token缓存可以直接写成:
SET login:token:abc123 user_10001 EX 7200一条命令就完成了"写入 + 设置过期时间"。再比如最简单的分布式锁原型:
SET lock:order:123 1 NX EX 30只有key不存在时才能写入成功,配合过期时间防止持锁进程崩溃后锁永远不释放。这条命令不复杂,但它就是很多分布式锁方案的地基。
MSET和MGET是批量版本的SET和GET。一次MGET几十个key,比循环GET几十次快非常多,省的是网络往返时间。要注意,MGET返回的数组顺序和传入key的顺序对应,某个key不存在就返回nil,不会报错。批量虽好,也别一次传几千个key,响应包太大会让网络和内存都很难受。
2.2 INCR/DECR/INCRBY:计数器为什么非它不可
Redis的INCR是对key的值做原子自增,这个特性在并发场景下非常关键。假如你用"GET -> 加1 -> SET"三步去更新计数,并发一高,两个请求同时读到同一个旧值,都会写回同一个新值,计数必然丢。而INCR把"读、加、写"在服务端一步完成,天然没有这种竞态问题。
常见用法包括:
INCR counter:visit:20240101 INCR post:1001:like INCRBY cart:1001:version 5第一个统计某天的访问量,第二个做点赞计数,第三个给某个对象的版本号叠加步长。配套的DECR和DECRBY用于减少,INCRBYFLOAT用于浮点数自增。它们都遵守相同的原子语义。
但这里有一个边界要讲清楚:INCR只保证单条命令内部的原子性。如果你要表达的是"先看当前值是否小于100,小于才自增",这个逻辑靠三条命令拼不起来,并发下会有人超限。要么用Lua脚本把判断和自增写在一个脚本里,要么用Redis的事务WATCH/MULTI,不能指望分段执行还顺便原子。
2.3 字符串里到底该放什么:JSON还是拆字段
字符串是最通用的缓存容器,新手第一反应就是把对象序列化成JSON丢进去,比如SET user:1 '{"name":"xiao","age":18}'。对于字段少、总是整体读写的对象,这是最省事的方案,我推荐这么做。但如果对象字段多、业务频繁只改其中一个字段,整体序列化就有问题:改一个字段也得整个覆盖,并发修改同一个对象容易后写覆盖前写,网络传输还带着一堆冗余字段。
这个时候应该考虑哈希结构,也就是把对象拆成一个个字段单独存。字符串本身还有APPEND、SETRANGE、STRLEN等命令,但实用频率不高,知道有这些命令即可。我更想提醒的是:别让单个value太大。一个5MB的字符串,GET一次就可能阻塞几十毫秒,如果被高频访问,整个Redis实例都会跟着抖动。超过几百KB的内容,先想清楚是不是该拆开、压缩,或者干脆换一种结构。
3. 哈希、列表、集合、有序集合:先把结构选对,再聊命令
这是整篇内容里最值得花时间的一章。很多人把Redis当一个大号Map来用,只会在字符串上打转,结果业务复杂了以后只能硬撑,等改结构时成本巨大。实际上,哈希、列表、集合、有序集合这四种结构,几乎覆盖了常见业务的所有数据组织方式,而且每种结构都有自己的"为什么"。
3.1 哈希:HSET/HGET/HGETALL/HMGET,存对象比JSON更精细
哈希在Redis里是field-value映射,最典型的场景就是存对象属性。比如用户对象,可以这样:
HSET user:100 name "xiaowang" age 28 HGET user:100 name HMGET user:100 name age跟字符串里的JSON方案相比,哈希能局部更新、局部读取。改一个年龄字段,不用把整个对象提出来再序列化回去;读姓名,也不用拉一整个JSON然后解析。字段多、访问频繁的对象,哈希优势非常明显。
有几个细节必须记住:HSET返回1表示新增字段,0表示覆盖旧字段;HDEL删除字段;HEXISTS判断字段是否存在;HLEN看字段个数。HGETALL会一次返回所有field和value,时间复杂度O(N),字段很多时慎用。我见过有人对几千个字段的哈希反复执行HGETALL,把单线程Redis打到阻塞,最后换成HMGET只取需要的字段才缓过来。
3.2 列表:LPUSH/RPUSH/LPOP/RPOP/LRANGE,队列栈都能干
列表按插入顺序存储成员,头尾操作是O(1),中间访问是O(N)。简化理解,它天生适合队列和栈。经典用法有:生产者LPUSH消息,消费者RPOP取出;每次LPUSH新内容,再用LRANGE取最近十条做时间线;记录用户操作日志,LRANGE翻页查看。BRPOP和BLPOP是阻塞版本,队列为空时客户端会挂起等待新数据,而不是空转轮询,这对写一些粘合脚本非常方便。
要注意两个细节。LRANGE的索引是左闭右闭,LRANGE list 0 -1 表示整个列表。列表很大时,LRANGE 0 -1同样危险,只取尾部N条的话用LRANGE list -N -1。另外,频繁访问列表中间元素说明结构选错了:普通列表随机访问是O(N),量大之后会慢得离谱,这种场景应该用ZSET或者哈希结构。
3.3 集合:SADD/SREM/SISMEMBER/SMEMBERS,去重与标签的利器
集合的核心特点是成员唯一、无序。它最常见的两类用途,一是去重,二是标签和权限判断。举个例子:
SADD article:100 tags "redis" "cache" SISMEMBER article:100 tags "redis" SREM article:100 tags "cache"SADD的返回值是1表示新增、0表示已存在,这个返回值经常被用来判断"这个用户是不是第一次来"。SISMEMBER判断成员是否存在是O(1),比把整个集合拉回来在代码里contains强得多。SMEMBERS返回全部成员是O(N),大集合上要非常谨慎。
集合运算也是选它的理由。SINTER取交集可以做共同好友,SDIFF取差集可以做"不在某个白名单里的用户",SUNION取并集可以合并多个标签桶。这些运算在服务端一次完成,不用把数据拉到客户端再手写,网络开销和代码量都能省。
3.4 有序集合:ZADD/ZINCRBY/ZREVRANGE,排行榜就指望它
ZSET是带分数的集合,每个成员对应一个double类型的分数,Redis按分数排序。最经典的应用就是排行榜。基本操作:
ZADD leaderboard 100 "user1" ZINCRBY leaderboard 5 "user1" ZREVRANGE leaderboard 0 9 WITHSCORESZINCRBY给某个成员加分,ZREVRANGE取分数从高到低的前十名,ZRANK/ZREVRANK查排名,ZSCORE查某个成员当前分数。这套组合做实时排行榜几乎零成本,根本不用去数据库排序。
两个容易踩的细节需要单独说一下。同分时ZSET会按成员字典序排列,做在线榜单时如果分数一样,排名顺序可能和你预想的不一样,做前端展示时要有所预期。另一个是分数类型,Redis的ZSET分数是double精度,不要用来存精确金额或商品价格,金额计算还是老老实实用数据库或整数分,Redis的ZSET分数只适合做排序依据和权重,不适合做账单。这两个细节看着不起眼,真到业务上线了都是事故点。
4. 过期时间与内存淘汰:这几个命令是保命用的
如果说前三章是在教你怎么写数据,那这一章就是教你怎么防止数据把自己搞死。Redis是内存型存储,不设置过期时间的key会一直占着内存,很多线上事故回头看,根因都是"缓存忘了设TTL"。这章把过期相关的命令和内存淘汰策略讲透。
4.1 EXPIRE/TTL/PERSIST:缓存不设过期时间等于定时炸弹
日常开发中,写缓存默认都应该带过期时间。哪怕你的数据"几乎永不过期",也最好给一个足够长的TTL,将来代码改版、数据结构变化时,可以靠过期自然淘汰旧数据,而不是手动清理。几个命令要记熟:
EXPIRE key 3600 TTL key PERSIST key SETEX key 3600 valueTTL返回剩余秒数,返回-1表示没设过期时间,返回-2表示key已经不存在。PERSIST可以把key变成永不过期。SETEX等价于SET加EXPIRE,适合一次写入带过期时间的场景。
一个容易忽略的点是,Redis的过期删除不是实时的,而是惰性删除和定期删除结合。被访问时才检查是否过期,后台也会周期抽样清理。所以当一批大key同时过期时,Redis可能会瞬间卡一下或内存抖动明显。大量key需要同一时刻过期时,建议给过期时间加一个随机偏移,把集中过期摊平,防止缓存雪崩。
4.2 SETNX与SET NX EX:一起上的最简分布式锁
SETNX的意思是Set if Not eXists,即只有key不存在时设置成功,返回1成功、0失败。它是做分布式锁最早期的方案之一。但有一个历史教训:很多人只用SETNX加锁,忘了设置过期时间,一旦持锁进程崩溃,锁永远不释放,线上直接死锁。
更稳的写法是用我前面提到的SET命令带NX和EX参数,一条命令完成加锁和过期:
SET lock:pay:order123 1 NX EX 30释放锁也不能简单DEL。原因是锁可能已经因为超时被自动释放了,后续另一个线程拿到了新锁,此时第一个线程执行DEL,会把别人的锁删掉。正确做法是先用GET比对值,确认锁持有者的标识是自己,再DEL。这个比对加删除需要原子执行,最简单的是用Lua脚本。这里不展开Lua,但你要记住:加锁要NX+EX,解锁要校验身份,这两点是基础命令层面必须养成的肌肉记忆。
4.3 CONFIG GET/SET与INFO:查内存、看淘汰,心里有数
内存淘汰策略maxmemory-policy决定Redis内存满了怎么办。默认的noeviction是不淘汰,直接拒绝写操作,开发环境没什么感觉,到了生产就是莫名其妙的写入失败。常用策略有:
- allkeys-lru:所有key按最近最少使用淘汰,适合通用缓存
- volatile-lru:只淘汰设置了过期时间的key,适合混合存储
- volatile-ttl:优先淘汰剩余时间短的key,适合有时效性的数据
对应命令是CONFIG GET maxmemory和CONFIG GET maxmemory-policy,修改用CONFIG SET。但生产环境不建议直接CONFIG SET改完就完事,最好在配置管理里同步成文件,否则重启后配置又丢了。
排查内存问题时,我一般按INFO memory、INFO stats、DBSIZE的顺序先看宏观:used_memory是多少,expired_keys和evicted_keys增长情况,当前key总量。然后才去定位具体key。先看数据,再下结论,而不是一上来就瞎猜。
5. 线上排查:先摸家底,再定位问题,别上来就KEYS *
前面四章已经把常用命令过了一遍。这一章专门讲线上排查时怎么组合这些命令,因为实际生产环境和本地练习完全是两回事。很多人第一次上线上Redis就手抖,一个KEYS * 打下去,整个实例都跪了。我把排查思路和套路整理出来,希望能帮你少踩几个雷。
5.1 摸清现场:DBSIZE、TYPE、EXISTS、TTL的组合拳
收到Redis告警,或业务反馈"缓存不生效",第一件事不是急着找某个key,而是先用低代价命令摸清整体状态。DBSIZE看当前库有多少key;INFO memory看内存占用;INFO stats看命中率、过期数量和淘汰数量。这几个命令本身都很轻,不会打爆Redis。
然后再定位单个key:TYPE看这个key是什么类型,EXISTS看它到底存不存在,TTL看剩余过期时间。举个实际场景:同事说缓存不生效,我先EXISTS,key不存在;再查代码发现写入用的前缀和读取用的前缀差一个字符。这种情况,靠EXISTS和DBSIZE就能慢慢缩小范围。如果业务反馈"类型错误",TYPE命令一眼就能看出是SET成了字符串还是哈希。
5.2 为什么KEYS * 是生产环境的地雷,SCAN才是正确姿势
KEYS pattern会一次性返回所有匹配的key。开发环境几十个key时完全没问题,到了线上几百万个key,KEYS * 会一次性扫描全部key并返回,Redis是单线程模型,这个操作会让所有读写请求排队几秒甚至几十秒,线上直接雪崩。所以任何生产环境都应该把KEYS当作禁用词。
替代方案是SCAN增量遍历。基本用法:
SCAN 0 MATCH user:* COUNT 100返回一批key和下一个游标,客户端拿着游标继续调用,直到游标为0才算遍历完。SCAN不保证一次遍历期间的key变化,也不保证不会重复,但用来"批量删除""批量设TTL""导出key清单"完全够用。如果真要在线上做全量操作,我习惯在低峰期用脚本分批SCAN,每批之间稍做停顿,避免给Redis太大压力。
5.3 定位大Key和慢命令:redis-cli --bigkeys 与 SLOWLOG GET
线上卡顿最常见的原因就两块:大key和慢命令。排查工具一个是redis-cli --bigkeys,它会遍历当前实例,统计每种类型里占用空间最大的key,输出最大字符串、最大哈希、最大列表之类的报告。注意它本身也是遍历,有开销,建议低峰期执行。
另一个是SLOWLOG GET,看慢查询记录。SLOWLOG GET 10能取出最近10条超过阈值的命令,每条包含执行时间、命令参数、发生时间戳。我用这套排查过一次很典型的卡顿:某服务每到午后定期卡住,SLOWLOG GET里全是SMEMBERS,耗时一秒钟以上,操作的集合有上百万成员。定位到具体key以后,把全量拉取的逻辑改成SSCAN分批读取,卡顿就消失了。这类问题的本质不是命令写错,而是没考虑O(N)命令在数据量大时的代价,基础命令真正值钱的地方就在这里。
6. 常用命令速查表与我的运维习惯
最后整理一份速查表,方便你平常翻。同时也把我在运维中用到的习惯列出来,这些习惯看着不起眼,但真到排查故障时能省下很多时间。
6.1 按数据类型整理的一份速查表
| 类型 | 常用命令 | 一句话场景 |
|---|---|---|
| 通用 | PING / SELECT / DBSIZE / EXISTS / TYPE / DEL / TTL / EXPIRE / PERSIST / SCAN | 连接、查看状态、判断存在、查类型、设过期、增量遍历 |
| String | SET / GET / MSET / MGET / INCR / DECR / INCRBY / APPEND / STRLEN | 缓存、计数器、token、分布式锁的NX/EX组合 |
| Hash | HSET / HGET / HMGET / HGETALL / HDEL / HEXISTS / HLEN | 存对象、局部更新属性、配置项 |
| List | LPUSH / RPUSH / LPOP / RPOP / LRANGE / LLEN / BRPOP | 队列、栈、最新动态时间线 |
| Set | SADD / SREM / SISMEMBER / SMEMBERS / SCARD / SINTER / SDIFF | 去重、标签、共同好友、集合运算 |
| ZSet | ZADD / ZINCRBY / ZSCORE / ZRANGE / ZREVRANGE / ZRANK / ZREM | 排行榜、实时热度、带权重的队列 |
这张表不是让你背,而是建议放在手边。你在实际开发里要做的,是看着一条业务需求,先问自己"它该放进哪种结构",再去找对应的命令,千万不要反过来——先想到一个命令,再硬套业务。比如要做排行榜就先想到ZSET,要做去重就先想到SET,要做消息队列就先想到LIST,数据结构选对了,命令基本就是那几条,剩下的只是写熟练度的问题。
6.2 我在实际运维中养成的几个习惯
第一,新写缓存默认带TTL,哪怕是"理论上永久不变"的数据也都给一个很长的过期时间,方便将来改版和清理。第二,生产环境直接封掉KEYS、FLUSHALL这类高危命令,通过Redis的命令重命名或ACL来做,别考验人性。第三,批量操作之前先看DBSIZE和INFO memory,评估一下规模,再决定是一次跑还是SCAN分批。第四,大key一定不能在高峰期处理,拆key、压缩、换结构,都得在低峰期操作,还要留好回滚方案。第五,排查问题的固定顺序是SLOWLOG、bigkeys、DBSIZE、INFO memory,拿日志和数据说话,而不是靠猜。
最后再说一点个人的体会:我把上面这些习惯贴在终端旁边,每次动手写Redis命令前都会过一遍脑子——这条命令会不会阻塞、这个key会不会占太多内存、不设TTL会不会出事。等你想清楚了这几点,redis基础常用命令才算真正掌握,往后遇到更复杂的分布式锁、缓存穿透、集群迁移,心里也有底。