ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis常用命令全解析:从数据结构到实战场景

Redis常用命令全解析:从数据结构到实战场景 很多人学Redis的时候第一反应是先找一份“redis基础常用命令”清单把set、get、hset挨个背下来。我当年也是这么开始的但干了几年缓存相关的事之后越发觉得命令本身只是表象真正拉开差距的是理解每条命令背后对应的数据结构特性、执行代价和适用场景。这篇文章就按我实际工作中的使用频率把最常用的一批Redis命令重新捋一遍适合刚接触Redis、想把基础打牢的同学也适合用了一段时间但总觉得哪里差点意思的开发者。看完你至少能明白什么场景该用什么类型的命令哪些命令看起来简单但坑很深以及一个普通缓存读写逻辑在命令层面是怎么串起来的。1. 动手之前先把自己正确接进Redis实例1.1 redis-cli和AUTH第一行命令就是它很多人学Redis命令时默认自己已经在redis-cli里面了但实际排查问题时我见过太多人卡在最开始的连接环节。Redis最常用的客户端工具就是redis-cli启动方式很简单redis-cli -h 127.0.0.1 -p 6379不写-h和-p默认就连本机6379端口本机开发时直接敲redis-cli就能进。如果Redis设置了密码会有两种情况一种是进入后每次执行命令都报NOAUTH Authentication required需要先执行AUTH yourpassword还有一种是用-a参数直接带密码登录redis-cli -a yourpassword这里有个实际建议-a这种方式虽然方便但密码会出现在shell历史记录、进程列表里在共享机器上容易泄密。我平时更习惯用环境变量REDISCLI_AUTH或者干脆进交互模式再敲AUTH。另外老版本Redis对AUTH命令本身也有一层默认限制比如redis-cli进入之后如果你连续输错密码太多次会影响后续命令执行这个在测试环境折腾的时候容易踩到。注意连接问题排不掉的时候先不要怀疑命令不对先确认redis-cli -h和-p到底有没有指向你心里想的那个实例。我见过有人查了半天命令最后发现连的是本地另一个端口的老Redis。1.2 从一个诡异报错说起连接什么库直接决定你看到什么Redis默认有16个逻辑库编号从0到15用SELECT命令切换SELECT 0 SELECT 15听起来很简单对吧但坑就在这里很多新手在测试环境里用SELECT 15写了点数据第二天换了个终端连上来默认在0号库怎么查都查不到key第一反应是“数据丢了”实际上只是库不对。所以在讲任何常用命令之前我强烈建议你先养成三个习惯连上Redis后先看当前是哪个库可以用CLIENT INFO或者直接SELECT 0切回默认库。用DBSIZE查看当前库有多少个key这个命令返回数字能快速判断你有没有连错环境。尽量在业务中固定使用同一个库不要把生产业务数据散落在0到15各个库。库多了以后排查问题非常痛苦数据迁移、清理脚本都要多带一个参数。还有一个命令要提一下FLUSHDB清空当前库FLUSHALL清空所有库。这两个命令在基础教程里很常见我也知道很多人想在测试环境“清一下重来”但真的不在生产环境随便用。尤其是FLUSHALL一条命令下去所有key都没了而且Redis默认是异步删除还是同步删除要看配置恢复成本极高。如果你只是在测试环境折腾最好先确认当前连接的确实是测试实例。1.3 PING、DBSIZE、INFO这几个自检命令排障时最先用新手最爱问为什么我的程序连不上Redis为什么缓存不生效遇到这类问题与其瞎猜不如先把几个自检命令过一遍。首先是PING。这是一个最基础的存活检测命令Redis会返回PONG。如果你能拿到PONG说明网络通、鉴权过了、实例活着。如果连PING都报错那就不是业务代码的问题是连接链路的问题。然后是INFO。这个命令输出一大段Redis的运行状态里面包含很多关键信息。我会重点关注几个段落# Memoryused_memory表示Redis当前占用的内存总量如果这个值接近maxmemory配置就得小心淘汰策略和OOM了。# Statstotal_commands_processed、keyspace_hits和keyspace_misses这两个数字的比值能看出缓存命中率。命中率太低说明缓存设计可能有问题。# Replication主从复制状态connected_slaves、master_link_status都在这里看。INFO的输出非常长你可以在命令后面跟具体的section缩小范围比如INFO memory、INFO stats。我平时定位问题的时候经常是PING先确认存活DBSIZE确认数据量级INFO stats看命中率一套下来问题基本能定位一半。2. String类型日常最高频但很多用法你真的用全了吗2.1 SET/GET基本语法以及那些“差点忘了”的参数SET和GET是Redis里最基础、最常用的命令但如果你以为SET key value就完事了那很多高级用法就浪费了。完整一点的SET命令长这样SET key value [EX seconds] [PX milliseconds] [NX|XX]这里面的参数特别实用EX seconds设置key的过期时间单位秒。比如SET token abc123 EX 3600表示这个token一小时后自动消失。PX milliseconds和EX类似但单位是毫秒适合需要精确过期控制的场景。NX只有key不存在时才设置成功相当于“不存在才写”。XX只有key已经存在时才设置成功相当于“存在才覆盖”。我把它们的区别整理成一张表方便你一眼看懂参数条件常见用途无参数不管key存不存在直接覆盖普通赋值NXkey不存在时成功分布式锁、幂等控制XXkey存在时成功更新但保证key必须存在EX/PX设置过期时间验证码、token、缓存NX EX不存在才写且带过期时间加锁并防止死锁GET看起来更简单但也有一点要注意如果key不存在Redis返回的是nil不同客户端语言解析出来的结果不一样。比如在Java的Jedis里可能是null在Python的redis-py里也是None如果你用命令行看就是(nil)。这本身不是错误只是“没有值”的正常表示。另外批量操作的MSET和MGET也要会用MSET user:1:name tom user:1:age 18 MGET user:1:name user:1:ageMSET和MGET的好处是减少网络往返。假设你原来要执行3次SET就是3次网络请求用MSET一次发过去Redis一次性处理。在高并发场景下这种批量操作对性能的提升非常实在。2.2 INCR/DECR/INCRBY用最简单的方式做计数器如果说SET/GET是String的日常那INCR和DECR就是String里的原子操作利器INCR page:view DECR page:view INCRBY page:view 10 DECRBY page:view 5INCR命令会把key里存储的数字加1DECR减1INCRBY和DECRBY可以指定步长。这个命令最常见的应用就是计数器比如文章浏览量、点赞数、接口调用次数。为什么Redis能保证计数不丢因为INCR是原子性的。所谓原子性简单理解就是Redis单线程执行命令当它处理INCR时同一个时刻不会有另一个命令插进来把这个值改乱。在并发环境下如果你用“先GET再1再SET”这种三步操作两个请求同时读到一个值最后写回去就会少算一次。而INCR把读、改、写合成了一步天然不会出现这个问题。这里有一个新手经常遇到的坑INCR只能对整数类型做自增如果key里存的是abc这种非数字字符串执行会报ERR value is not an integer or out of range。所以用INCR之前要保证这个key从没被写入过非数字内容或者确认它是你专门用来做计数的key。2.3 SETNX和SETEX一把简易分布式锁的前身String类型里还有两个很容易混淆的命令SETNX和SETEX。SETNX是“SET if Not eXists”的缩写只有当key不存在时才设置成功返回1如果key已存在设置失败返回0SETNX lock:order:123 worker-A这个命令天然可以拿来做一个最简单的锁谁成功执行了SETNX谁就拿到了“锁”处理完业务再用DEL lock:order:123释放。不过裸用SETNX有坑如果持有锁的进程崩溃了锁永远不会释放其他请求全部卡死。SETEX是“SET with EXpiration”的缩写等价于同时执行SET key value和EXPIRE key secondsSETEX verify:code:13800138000 300 123456这就是发短信验证码的经典操作5分钟内有效过期自动删除。真正生产级别的分布式锁其实是用SET命令带NX和EX参数一步搞定的SET lock:order:123 worker-A NX EX 10这样既保证了“不存在才设置”又带了过期时间兜底避免死锁。这就是网上很多分布式锁方案里面最核心的一条命令。3. Hash类型存对象比String更聪明尤其是字段级更新3.1 HSET/HGET/HGETALL把用户信息存成一整个对象String类型存一个用户信息通常是把整个对象序列化成JSON再塞进去SET user:1001 {\name\:\tom\,\age\:18,\city\:\shanghai\}这样做的问题很明显如果你只想改用户的年龄也得先把整个JSON拿出来反序列化、改字段、再序列化、再写回去费时费力还容易出错。Hash类型就是专门解决这个问题的。Hash可以理解成一个小的map一个key下面可以挂很多field-valueHSET user:1001 name tom HSET user:1001 age 18 HSET user:1001 city shanghai或者一次设置多个字段HSET user:1001 name tom age 18 city shanghai读取的时候HGET user:1001 name HGETALL user:1001 HMGET user:1001 name ageHGET取单个字段HMGET批量取多个字段HGETALL一次性取出所有字段和值。这里的经验是HGETALL虽然方便但如果一个Hash里字段特别多、值特别大它会把所有内容一次性拉到客户端很容易造成网络阻塞。生产环境里我更倾向于用HMGET只取需要的字段而不是无脑HGETALL。3.2 HINCRBY单独给某个字段做自增点赞数和库存都用得上Hash最大的优势之一是可以在“对象内部”做字段级自增HINCRBY user:1001 score 10 HINCRBY product:888 stock -1比如用户积分、商品库存如果把它们放在Hash里可以用HINCRBY只对某个字段做增减。比如上面的HINCRBY product:888 stock -1就是库存扣减1它和String的INCR一样是原子操作。再举一个具体场景一个文章的点赞功能用HINCRBY article:999 like_count 1每次只操作一个字段完全不碰文章的其他信息。相比之下如果你用String存整个文章对象那每次点赞都要做一次“读全对象、反序列化、改字段、序列化、写回”的操作性能差异非常明显。HINCRBY还有一个变种叫HINCRBYFLOAT可以对浮点字段做自增。比如商品价格、金额场景偶尔会用到。注意如果字段里存的是一个无法转换成数字的字符串HINCRBY也会报错和String的INCR是同一个道理。所以在设计数据结构时要保证被自增的字段从初始化开始就一直是数字类型。3.3 什么时候用Hash什么时候还是老老实实String我知道你会纠结那以后存对象是不是都该用Hash也不一定。我用一张表把两种方式的适用场景说清楚对比维度String存JSONHash存字段读取方式一次取整个对象可只取部分字段修改字段需要序列化反序列化直接HSET/HINCRBY内存占用整体一个key相对简单多字段有额外元数据开销适用场景小对象、一次性读整块数据需要频繁改部分字段的对象实际项目里我是这样划分的用户信息、商品详情这种“需要局部更新、字段带业务含义”的数据优先用Hash简单KV、验证码、token、序列化好的页面片段直接String。另外还要考虑客户端语言的便利性。有些语言的JSON库处理String很方便网上很多教程也默认用String存JSON这也没错关键是你要知道Hash在“字段级更新”这个场景里有多舒服选型的时候心里有底。4. List类型从消息队列到最新列表一把梭4.1 LPUSH/RPUSH/LRANGE先搞懂头和尾List在Redis里是一个双向链表可以从头部插、也可以从尾部插。插入命令有两个方向这块特别容易搞反LPUSH key value # 从左边/头部插入 RPUSH key value # 从右边/尾部插入怎么记忆你站在列表左边往右看LPUSH把新元素塞到最左边RPUSH塞到最右边。比如你依次执行RPUSH queue msg1 RPUSH queue msg2 RPUSH queue msg3那么这个列表从左到右就是msg1、msg2、msg3说白了就是按顺序排队1先进、2其次、3最后。查询列表内容用LRANGELRANGE queue 0 -10 -1表示从第0个元素到最后一个元素也就是全量查看。还有很多时候你只想看最近几条比如取最新的10条LRANGE news:list 0 9这就是“最新列表”最常见的查询方式。很多文章、评论、通知场景里就是用LPUSH把新内容插到头部再用LRANGE 0 9取前10条天然就是最新的。4.2 LPOP/RPOP/BRPOP阻塞读才是消息队列的正确姿势有插就有弹。LPOP从头部弹出一个元素RPOP从尾部弹出一个元素LPOP queue RPOP queue弹出来的元素会从列表中移除所以它天然适合做“消费”操作。一个最简单的消息队列套路是生产者用LPUSH往队列里塞消息消费者用RPOP从尾部取消息先入先出。但裸用RPOP有个问题队列为空时消费者会拿到nil你需要自己写循环不断轮询既浪费CPU又有延迟。Redis专门提供了一个阻塞版命令BRPOPBRPOP queue 0BRPOP和RPOP的区别是如果队列里没有元素它会一直阻塞等待直到有新消息进来或者超过超时时间返回nil。第二个参数是超时秒数0表示永久等待。这个机制比手动轮询优雅得多也是很多人用Redis做轻量级消息队列的原因。当然用List做消息队列也有明显的局限如果消费者处理完消息之后还没来得及确认进程就崩溃了这条消息就丢了。Redis官方后来也推出了Stream类型专门做消息队列支持消费组、ack确认等机制。但是在我们聊“基础常用命令”这个层面先掌握LPUSH BRPOP的组合就足够应付大部分轻量场景了。4.3 LLEN与消息堆积的简单判断List还有个非常实用的命令LLEN返回列表的长度LLEN queue这个命令在排查消息积压时特别有用。比如你的消费者处理速度跟不上生产者队列长度就会不断上涨用LLEN一查就能看到数字。如果平时长度稳定在几千突然涨到几十万那基本可以断定消费端出问题了要么消费者挂了要么处理逻辑变慢。除此之外LINDEX可以按下标取某个位置的元素LSET可以修改指定下标的元素LTRIM可以只保留列表的一部分、把其他元素删掉。比如你想让一个列表始终只保留最近100条记录LTRIM news:list 0 99LTRIM在这里的效果是“剪裁”把0到99之外的元素全部删除。这个操作在控制内存、做滚动日志列表时非常实用。5. Set和ZSet去重、交并集和排行榜一套组合拳5.1 SADD/SMEMBERS/SISMEMBER标签和去重的日常Set是无序、去重的字符串集合。往集合里添加元素用SADD如果元素已经存在重复添加不会报错但也不会重复存储SADD tags:article:1001 redis SADD tags:article:1001 redis database SMEMBERS tags:article:1001SMEMBERS会把集合里的所有元素列出来。这里有个和HGETALL类似的提醒如果集合非常大SMEMBERS会把所有元素一次性拉回来对内存和网络都不友好。数据量大时建议用SSCAN分批遍历或者先评估一下这个集合是不是真的需要全量读取。SISMEMBER判断一个元素是否在集合中时间复杂度是O(1)非常快SISMEMBER tags:article:1001 redis这个命令很适合做“用户是否已参与”的判断。比如一个活动里用SADD记录所有参与用户ID再用SISMEMBER判断某个用户是否已经参与了天然去重而且判断极快。5.2 SINTER/SUNION/SCARD两个集合怎么玩出花Set真正的威力在集合运算。假设你有两个集合一个是“关注了A的用户”一个是“关注了B的用户”SADD follow:A user1 user2 user3 SADD follow:B user2 user3 user4计算同时关注了A和B的用户交集SINTER follow:A follow:B计算关注了A或者关注了B的总用户并集SUNION follow:A follow:B计算只关注A但没关注B的用户差集SDIFF follow:A follow:B这些运算在标签系统里尤其好用。比如给文章打了“Redis”“数据库”“缓存”三个标签要找出同时打了“Redis”和“数据库”标签的文章直接SINTER一次就出来了不用写一堆应用层循环逻辑。SCARD返回集合的基数也就是元素数量SCARD follow:A另外SPOP可以从集合里随机弹出元素SRANDMEMBER可以随机返回若干元素但不删除。这两个命令可以用来做抽奖场景把参与者ID全部SADD进集合然后SPOP抽奖抽中的人自动从集合中移除保证不会被重复抽中。5.3 ZADD/ZRANGE/ZREVRANGE有序集合做排行榜的思路ZSet在Set的基础上多了一个“分数”概念每个元素都关联一个数字分数集合会按照分数从小到大自动排序。最典型的应用就是排行榜ZADD leaderboard 100 user1 ZADD leaderboard 200 user2 ZADD leaderboard 150 user3查看分数从低到高排列的所有元素ZRANGE leaderboard 0 -1 WITHSCORES查看分数从高到低排列的TOP3也就是排行榜前三名ZREVRANGE leaderboard 0 2 WITHSCORESZREVRANGE是倒序取在排行榜场景里用的比ZRANGE多。除了查询更新分数也很方便ZINCRBY leaderboard 50 user1这条命令把user1的分数加50如果它原来在排行榜中间加分后排名会自动调整。这个特性在做“积分变化实时更新排行”时特别强大。ZSet还有一个常见用途延时队列的简易实现。把任务ID作为成员把执行时间戳作为分数然后用ZRANGEBYSCORE取出到当前时间为止需要执行的任务处理完再ZREM掉。这是很多轻量任务调度器在Redis上的经典玩法。6. 关于Key本身过期、删除和扫描别等出事了才补课6.1 EXPIRE/TTL缓存一定要带上过期时间前面讲SETEX的时候提过过期时间但更通用的做法是用EXPIRE对已有key设置过期时间EXPIRE user:1001 600 TTL user:1001TTL查看当前key还剩余多少秒过期。返回结果有三种情况比较关键正数剩余存活秒数。-1key存在但没设置过期时间永久有效。-2key不存在或者已经过期被删除了。Redis的过期删除不是等时间到了立刻把key从内存里抹掉而是采用惰性删除定期删除的策略当key被访问时才检查是否过期同时后台也会定期扫一部分过期key清掉。这就是为什么你有时看到TTL已经过期了但内存没有立刻降下来。这里有一个特别容易踩的坑对同一个key执行SET操作时如果没有带EX、PX参数原来的过期时间会被清除。也就是说你先SET token abc EX 300没过多久又执行SET token def这个token就从5分钟有效期变成了永久有效。生产环境里因为这个原因导致过期失效、缓存永不过期的问题非常多一定要记住。6.2 KEYS为什么不能乱用SCAN怎么用好很多人学Redis的时候第一个学会的超能力就是KEYS *一下把所有key列出来感觉很爽。但这条命令在生产环境是禁忌级的原因在于Redis是单线程模型KEYS需要遍历所有key数据量一大就会阻塞整个Redis实例期间所有读写命令都得排队线上服务直接卡住。如果确实需要按模式扫一批key正确的做法是SCAN命令。它的用法和KEYS完全不一样需要配合游标SCAN 0 MATCH user:* COUNT 100第一次调用游标从0开始命令会返回两个值一个是下一次要用的游标另一个是本次扫描到的key列表。如果游标变成0说明扫描结束。整个过程分多次执行每次只取一部分数据不会长时间阻塞Redis。注意SCAN在扫描过程中如果某些key在扫描期间发生了变化增删改可能会被漏掉或者重复返回所以它不保证一次扫描能拿到绝对一致的快照。但对于大多数批量清理、按前缀统计的场景这个精度完全够用了。6.3 DEL/UNLINK/TYPE/EXISTS日常删键查键的姿势判断一个key是否存在用EXISTSEXISTS user:1001返回1表示存在0表示不存在。它还可以一次传多个key返回的是“有几个key存在”。查看key的类型用TYPETYPE user:1001返回string、hash、list、set、zset等类型。如果发现返回的是none说明key不存在。删除key最常规的是DELDEL user:1001DEL是同步删除如果key是个大key比如一个包含上百万元素的Set删除动作本身也会占用比较长的时间同样会阻塞Redis。Redis 4.0之后提供了UNLINK命令它和DEL的区别是UNLINK先把这个key从键空间里摘掉然后在后台异步释放内存。对于大key清理推荐优先用UNLINK能明显降低阻塞风险。还有两个命令在排查问题时会用到OBJECT ENCODING可以看某个key内部的编码方式RANDOMKEY可以随机返回一个key用来抽查Redis里的数据分布。7. 串一遍真实场景用户详情页的缓存读写7.1 需求与命令流先查缓存不回就有回源逻辑讲了这么多命令可能有人觉得还是散的。我们把它串到一个具体场景里用户详情页。假设用户数据在DB里我们需要在前面加一层Redis缓存来降低DB压力。第一步查询用户信息时先用HGETALL尝试从缓存中取HGETALL user:1001如果返回的字段为空说明缓存里没数据这时候才去查数据库拿到数据后回写缓存HSET user:1001 name tom age 18 city shanghai EXPIRE user:1001 3600这个过程的伪代码如下user HGETALL user:1001 if user 为空: user 从数据库查询 user:1001 if user 存在: HSET user:1001 各字段 EXPIRE user:1001 3600 return user else: return user代码很简单但这里有一个细节为什么用HGETALL判断“缓存是否存在”因为Hash是多个字段如果用户不存在Redis里就没有这个keyHGETALL返回空map。如果你用String存JSON逻辑也是一样GET返回nil就说明没命中。第二步更新用户信息时除了更新数据库也要同步更新缓存HSET user:1001 age 19如果更新后想让缓存立刻失效可以DEL user:1001下次查询自然回源重建。到底是“更新缓存”还是“删缓存”是另一种取舍但从命令层面看无非就是HSET和DEL的选择。7.2 缓存过期、击穿时的命令级应对把上面的基础逻辑跑通之后你很快会遇到一个经典问题缓存击穿。说的是某个热点key刚好过期同一时刻有大量请求发现缓存没命中全部打到数据库数据库瞬间被压垮。常见的命令级应对方案是“互斥锁”。查询流程变成这样user HGETALL user:1001 if user 为空: if SET lock:user:1001 worker NX EX 10 成功: user 从数据库查询 user:1001 HSET user:1001 各字段 EXPIRE user:1001 3600 DEL lock:user:1001 return user else: 说明别的请求正在重建缓存 休眠一小段时间后重试查询这里最核心的命令就是第3章的SET key value NX EX seconds。它保证同一时刻只有一个请求能拿到“重建缓存”的资格其他请求看到锁没抢到不会继续打数据库而是等待后重试。这就是分布式锁在缓存穿透场景里的典型应用。如果你觉得这样等待会稍微增加响应时间还有一种思路是“逻辑过期”缓存不设置物理过期时间而是在value里放一个过期时间戳查询时发现逻辑上过期了立即返回旧数据同时异步去刷新缓存。这个方案实现起来复杂一些但用户体验更好。不管用哪种方案底层靠的还是SET NX EX、EXPIRE、DEL这些基础命令。7.3 用INFO和SLOWLOG给Redis做个体检最后说一个平时排查Redis健康状态特别好用的命令组合INFO和SLOWLOG。SLOWLOG GET可以查看最近执行得很慢的命令SLOWLOG GET 10它会列出慢查询日志包括命令内容、执行耗时、执行时间点。Redis默认慢查询阈值是10毫秒可以通过配置项slowlog-log-slower-than调整。如果SLOWLOG GET里频繁出现KEYS、HGETALL、SMEMBERS这些命令通常是因为它们读取了大量数据需要考虑加索引设计或者改成SCAN分批操作。再配合INFO stats里的keyspace_hits和keyspace_misses可以算出缓存命中率。如果命中率长期很低说明缓存设计有问题要么key的过期时间太短要么热点数据没被正确缓存。这个环节不需要复杂工具一条INFO stats就够用了。我自己的习惯是每次要动Redis之前先INFO memory看一眼内存水位再SLOWLOG GET 20看一眼最近的慢命令心里有个数再动手。这一个步骤真的能避免不少线上事故。最后再分享一个我实际工作里的小习惯给所有缓存key统一一个可辨识的前缀比如user:、product:、lock:然后养成写命令时顺手带上过期时间的习惯。很多初学者调Redis半天不对劲一看全是key没设TTL、过期时间被覆盖、KEYS *扫全库这种基础问题。把这些基础命令和背后的坑弄明白比背再多命令列表都管用。
返回列表