关于Redis五种用途,很多刚接触的同学第一反应就是缓存。这个判断没错,缓存确实是Redis最广为人知的用法,但如果只把Redis当缓存用,多少有点委屈它了。我这里的五种用途,并不是官方规定好的五个功能,而是从实际业务中反复出现的场景里提炼出来的五类经典打法:缓存、分布式锁、排行榜与计数器、消息队列、会话管理。这几个场景背后,其实都靠同一套内核支撑——内存存储、单线程执行模型、丰富的数据结构、灵活的过期策略。适合谁看?后端开发、架构设计、运维排障的人都适合,特别是当你发现Redis内存总在涨、CPU偶尔抖动,或者分布式环境下不知道怎么管理登录态的时候,这篇内容应该能给你一个相对完整的抓手。
1. 先理清思路:Redis到底能解决哪些问题
1.1 为什么是这五种场景,而不是五个功能
很多技术文章喜欢按命令讲Redis,比如String能做什么、Hash能做什么、ZSET能做什么。但实际项目里,我们不是按命令去思考,而是按业务问题去思考。Redis五种用途这个说法,本质上是把高频业务问题分成了五类:读写热点需要加速,就用缓存;多节点争抢同一份资源,就用分布式锁;需要给用户排名、计数,就用ZSET和INCR;需要异步解耦、削峰填谷,就用List或Stream做轻量队列;需要跨节点共享登录态或业务上下文,就用Redis存Session。每一类场景背后都有对应的数据结构选型和配置取舍,搞清楚这些,比死记命令要实用得多。
举个例子,一个电商系统的瞬时流量会先打到商品详情、库存扣减、订单创建三个环节。商品详情适合用缓存,库存扣减不适合用缓存而适合用 Lua 脚本配合原子操作,订单创建适合丢到队列里异步处理,用户的登录态则适合放到 Redis 里集中管理。这不就是同一个服务里同时出现五种用途的典型场景吗?所以别把它们割裂看待,它们是同一套基础设施在不同维度的输出。
1.2 共同内核:内存、单线程、数据结构、过期策略
Redis 能承载这五种用途,不是靠花哨的语法,而是靠四个底层特性。第一是内存存储,读写速度能到十万级 QPS,这是缓存和会话管理能撑住高并发的根本原因。第二是单线程执行模型,每条命令在服务端是串行执行的,不需要加锁,所以 INCR、SETNX 这类复合操作天然具备原子性,这就让分布式锁和计数器变得非常可靠。第三是数据结构丰富,String、Hash、List、Set、ZSET、Stream,各自适合一种访问模式,排行榜用 ZSET 是因为它天生按分数排序,消息队列用 List 是因为它有阻塞读的能力。第四是过期策略,Redis 支持精细到秒和毫秒的 TTL,缓存、Session、限流窗口都能借助过期机制自动清理。
理解这四个内核,再看五种用途就不会觉得零散。缓存的 TTL 设计、分布式锁的过期时间、限流的时间窗口、Session 的续期策略,全都是同一个原理在不同场景下的应用。
1.3 什么场景不该用Redis
聊用途之前,先泼一盆冷水。Redis 不是万能的,下面这几种情况最好别硬上。第一,数据量极大且访问频率不高的冷数据,不要塞 Redis,内存成本会很高,把这些数据放在磁盘数据库就好。第二,需要复杂关联查询和事务强一致性的业务数据,Redis 的关系表达能力很弱,事务支持也有限,硬用会让代码变得很难维护。第三,对数据安全要求极高、完全不能接受丢失的核心资产,Redis 虽然支持 AOF 和 RDB 持久化,但默认配置下的丢失窗口比传统数据库大,需要谨慎评估。
我见过有人把所有业务表都缓存一份到 Redis,美其名曰“加速”,结果每次表结构一变,缓存同步代码就要跟着改一遍,最终维护成本远大于收益。选 Redis 的正确姿势是“某个场景确实需要低延迟、高并发、原子操作”,而不是“我有 Redis,所以什么都往里放”。
2. 用途一:缓存——最常用也最容易被用歪
2.1 缓存穿透、击穿、雪崩的差异
缓存用到一定规模,一定会遇到三个经典问题:穿透、击穿、雪崩。很多人容易混,我用自己的话拆开讲。
穿透,是查询一个根本不存在的数据。比如拿一个不存在的用户ID去查详情,Redis里没有,数据库里也没有,每次请求都直接打到数据库。这下缓存就白搭了,恶意攻击最喜欢打这种接口。解决办法很简单:缓存空值,给不存在的key也写一个短TTL的占位值;或者用布隆过滤器,在缓存前面先判断key是否存在。
击穿,是某个热点key在过期瞬间,大量请求同时打到后端。它不是多个key的问题,是“单个key过期那一刻”的问题。解决思路是互斥更新,也就是在回源数据库之前尝试加锁,只有拿到锁的线程去查库写缓存,其他线程短暂等待后直接读缓存,或者用逻辑过期时间主动刷新。
雪崩,是大量key在同一时间过期,或者Redis实例挂了,导致请求集中打到数据库。解决思路有两条:一是过期时间加随机扰动,比如 300 秒基础上加 0 到 60 秒的随机值,避免集体失效;二是做多级缓存或降级策略,Redis挂了还有本地缓存兜底。
我刚开始做缓存的时候,只想着把热点数据塞进去,根本没想过过期时间会引发连锁反应。直到某天整点一批数据集体过期,数据库连接瞬间被打满,才意识到这三个问题必须提前设计。现在我做缓存方案,一定会先问:key被穿透的概率有多大?热点key的过期时间怎么错开?Redis不可用时线上降级怎么做?
2.2 缓存与数据库的一致性,要分场景取舍
缓存和数据库的一致性,是讨论最多也最容易被杠的话题。严格来说,只要用了缓存,就一定存在不一致的窗口,我们只能把窗口变小,不可能完全消除。
主流做法是 Cache Aside 模式:读的时候先读缓存,读不到再读数据库,然后回填缓存;写的时候先更新数据库,再删除缓存。为什么是删除而不是更新缓存?因为更新缓存并发场景下很容易产生旧值覆盖新值的问题,而删除缓存后,下一次读请求会重新从数据库加载,虽然多一次回源,但逻辑简单得多。
数据库更新和缓存删除这两步之间不是原子的,通常用两种补偿方案。一种是先更新数据库,再延迟双删,也就是删除缓存后,隔几百毫秒再删一次,尽量清掉并发读请求写进去的旧值。另一种是消息队列异步删除,把删除操作发到队列里,由消费者重试。本地也可以用简单的重试机制,但务必保证最终能删成功。
我个人的经验是:对一致性要求高的核心链路,比如支付金额、库存扣减,优先考虑能不能去掉缓存,或者只用 Redis 做限流和数据统计,而不要拿它做精确账目;对一致性要求没那么高的场景,比如用户昵称、文章详情,Cache Aside 加延迟双删已经足够。不要为了一个并不存在的强一致需求,设计出一套极其复杂的缓存同步方案,最后反而拖垮了系统。
2.3 实操配置:TTL 与淘汰策略怎么配
缓存里有两个参数必须认真对待:TTL 和内存淘汰策略。
TTL 要结合数据的访问特征来定。一个商品详情的访问热度可能持续十分钟,那 TTL 可以设在 300 秒到 600 秒;一个验证码只需要 5 分钟,TTL 设在 300 秒即可;一个配置项可能一天都不变,TTL 可以放到 3600 秒以上。同时记得加随机扰动,避免大批key同时过期引起雪崩。
内存淘汰策略在 redis.conf 里通过maxmemory-policy配置。我常用的有两种:allkeys-lru,适合大多数缓存场景,当内存写满时,Redis 会淘汰最近最少使用的key;volatile-ttl,适合只对设置了过期时间的key做淘汰,不设置过期时间的业务数据不会被挤掉。对于既做缓存又做 Session 的实例,一般建议分实例部署,避免缓存淘汰把 Session 挤掉。
配置参考如下:
maxmemory 4gb maxmemory-policy allkeys-lru appendonly no save ""如果这个 Redis 实例只做缓存,关闭持久化可以提升性能,因为缓存数据丢了还能从数据库回源。但注意,这只是“只做缓存”场景的建议,如果还承担锁、队列、Session 职责,持久化策略要单独评估。
2.4 避坑经验:不要缓存一切
我把缓存最容易翻车的一点放在最后说:不要缓存一切。判断一个数据适不适合缓存,要看三个指标——读频率高不高、数据量是否可控、一致性容忍度有多大。如果某个数据一天只被读几次,缓存反而增加了内存占用和维护成本;如果数据量几个 GB,缓存占内存也划不来;如果对一致性要求特别高,缓存带来的风险可能比收益更大。
我还踩过一个典型的坑:把数据库里的全量用户表一次性缓存,导致启动时 Redis 内存直接飙升到上限,然后 key 被频繁淘汰,命中率低到感人。后来改成只缓存活跃用户的高频数据,命中率反而上来了。缓存不是越多越好,是越精准越好。
3. 用途二:分布式锁——先说清锁的本质
3.1 SET NX EX 为什么够用,什么情况下不够
分布式锁解决的是多实例同时操作同一份资源时的互斥问题。最简单的实现,就是 Redis 的SET key value NX EX seconds。NX 的意思是“只有key不存在时才设置成功”,EX 表示过期时间。谁能成功设置这个key,谁就获得了锁;其他实例再去设置就会失败。
为什么这个命令能当锁?因为 Redis 单线程执行命令,SET NX具有原子性,不会出现两个实例同时拿到锁的情况。配合过期时间,又能避免持有锁的实例宕机后死锁。
但这里有个关键:value 必须是唯一标识,通常是 UUID 或业务流水号,释放锁时要去比对。不然可能出现这种情况——A 实例拿到锁,执行时间超过了锁的过期时间,锁自动释放;这时 B 实例拿到锁;A 执行完后如果直接用DEL key去释放,就会把 B 的锁给删掉。所以释放锁不能简单 DEL,要先判断 value 是不是自己的,再删除。这个判断和删除要保证原子性,最好用 Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end3.2 锁续期与可重入:加锁失败了怎么办
分布式锁最容易翻车的点,不是加锁,而是“锁过期了任务还没执行完”。如果任务执行时间不稳定,比如一个数据迁移任务可能跑 3 秒,也可能跑 30 秒,固定 10 秒过期就会有问题。解决思路是续期:持有锁的线程每隔一段时间检查任务是否还在执行,如果没执行完就延长锁的过期时间。这个续期动作可以自己做,也可以用现成的客户端库,比如 Redisson 的 watchdog 自动续期。
锁的持有者还需要考虑重入问题。同一个线程在同一把锁上重复加锁,如果不去重,第二次加锁会失败,业务逻辑就可能异常。简单实现里可以给每个锁记录持有者和重入次数,用 Hash 结构存储,释放一次就减一次计数,计数归零才真正删除。不过大多数业务场景不会复杂到需要可重入,能用最简单的方式就别过度设计。
我在实测中遇到过一种情况:多个服务实例共享同一个 Redis,但不同团队对同一把锁使用了不同的 key 前缀,结果就是“各锁各的”,完全没起到互斥作用。分布式锁是跨团队合作,key 命名和过期时间必须提前约定清楚,否则就是纸面锁。
3.3 实操示例:一段能直接用的加锁代码
这里用 Python 的 redis-py 库给出一段简化但可运行的示例。
import redis import uuid r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) def acquire_lock(lock_key, timeout=10, wait_timeout=5): token = str(uuid.uuid4()) end = time.time() + wait_timeout while time.time() < end: if r.set(lock_key, token, nx=True, ex=timeout): return token time.sleep(0.05) return None def release_lock(lock_key, token): # 使用 Lua 保证原子性 lua_script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ r.eval(lua_script, 1, lock_key, token)加锁时循环重试,是因为业务上往往希望“等锁”,而不是“失败即返回”。但重试间隔不能太短,否则会造成大量无意义的 SET 请求,一般建议 50 到 100 毫秒。锁的过期时间要根据业务最坏耗时来定,并配合续期机制。释放锁时必须传入之前生成的 token,不能只传 key。
3.4 常见误区:主从切换丢锁
Redis 主从架构下,存在一个容易被忽略的问题:如果主节点刚把锁写入,还没来得及同步到从节点,主节点就挂了,从节点顶上后没有这条锁记录,另一个实例就能成功加上同一个锁。这就导致两个实例同时进入了临界区。
要彻底解决这个问题,业界有 Redlock 算法,思路是向多个独立的 Redis 节点依次加锁,超过半数成功才算加锁成功。但 Redlock 本身也有争议,它依赖系统时钟和网络条件,并不是绝对安全的银弹。更务实的建议是:如果你的业务对分布式锁的可靠性要求极高,且无法接受任何极端情况下的并发冲突,要么引入专业协调服务,要么在业务层做幂等兜底。很多场景下,后端数据库的唯一约束、状态机校验,往往比 Redis 锁更能挡住并发问题。
4. 用途三:排行榜与计数器——数据结构的高光场景
4.1 ZSET 做排行榜的思路
排行榜是 Redis 的 ZSET 结构最典型的使用场景。ZSET 的每个成员都带一个分数(score),Redis 内部维护着按分数排序的跳表,因此插入、删除、查询排名的复杂度都很理想。想做热榜,核心就两步:给成员加分,取某个分数区间或排名区间。
以文章热榜为例,每次用户点赞或点击,执行一条ZINCRBY命令就能给对应文章的 score 加一。要用时可得分倒序取前 N 名,命令是ZREVRANGE hotRank 0 9 WITHSCORES。如果还要做分页,ZREVRANGE可以传 start 和 stop 区间,天然支持。
ZINCRBY hotRank 1 post_1001 ZREVRANGE hotRank 0 9 WITHSCORES用 ZSET 的好处是排序在 Redis 内存中完成,不需要每次从数据库查询再在应用层排序。当数据量到百万级,内存占用会比 String 结构高,因此还需要考虑只保留前 N 名的冷热策略。比如每天创建一个新的 key,过期时间设为 7 天,这样历史榜单自然淘汰,不会无限增长。
4.2 计数器与限流:INCR 的原子性
Redis 的INCR命令可以对 String 类型的整数值做原子自增。因为单线程模型,高并发下也不会出现“读、改、写”三步之间的竞态。这个特性在计数器场景特别有用:今日访问量、视频播放量、用户积分、库存扣减,都能用 INCR 或DECR实现。
要注意的是,用 INCR 做计数时,key 的初始化时机要处理好。通常可以先SET key 0初始化,也可以利用INCR的 Google Doc 效果,如果 key 不存在,Redis 会自动从 0 开始自增。但如果你想在自增的同时设置过期时间,就要用INCR配合EXPIRE,或者用 Lua 脚本保证原子性。
限流是计数器的高阶用法。最简单的固定窗口限流,就是用过期时间作为窗口。比如限制每个用户每 60 秒最多请求 10 次,可以把 key 设为用户ID加分钟级时间戳,执行 INCR,第一次执行后设置 60 秒过期;如果 INCR 的结果大于 10,就拒绝请求。严格来说这种实现有临界窗口问题,但对于大多数中小流量场景已经够用。
4.3 滑动窗口限流怎么用 Redis 实现
固定窗口的问题是窗口切换瞬间可能产生双倍请求。如果要用更平滑的滑动窗口,可以用 ZSET 记录每个请求的时间戳,通过计算窗口内的请求数量来判断是否限流。
def is_allowed(user_id, limit=10, window=60): key = f"rate:{user_id}" now = time.time() # 移除窗口外的记录 r.zremrangebyscore(key, 0, now - window) count = r.zcard(key) if count < limit: r.zadd(key, {str(uuid.uuid4()): now}) r.expire(key, window) return True return False这段逻辑的好处是,不需要依赖 key 的过期来清理记录,滑动窗口内的请求数量始终是准确的。坏处是每个请求都要写入一条 ZSET 记录,内存占用比固定窗口高。如果流量特别大,可以压缩时间精度,比如按秒做哈希,把同一秒内的多个请求合并成一条记录,空间换时间。
我实际体感是,大多数业务限流用固定窗口就够,只有对突发流量特别敏感的接口才需要滑动窗口。过度限流设计也会把系统搞复杂,到时候限流本身成了性能瓶颈,那就得不偿失。
4.4 实操案例:实时热榜
给一个完整的实时热榜思路。假设一场直播活动,观众可以给主播送礼物,每次送礼都是热度加分项。热度可以拆成两部分:基础热度(初始值)和实时热度(每分钟更新)。我建议用两个 ZSET:一个存总榜(每日一key),一个存小时榜(每小时一key)。送礼时往两个 ZSET 同时加分,榜单到期自动过期。
ZINCRBY dailyRank 5 anchor_1024 ZINCRBY hourlyRank 3 anchor_1024 EXPIRE hourlyRank 3600前端展示时,需要 Top100 直接ZREVRANGE;需要某个主播的排名,用ZREVRANK anchor_1024。这样即使并发送礼量很大,Redis 也能轻松抗下。如果热度计算逻辑比较复杂,可以在应用层算好加分数,再批量用 Pipeline 发送给 Redis,减少网络往返。
5. 用途四:消息队列与发布订阅——别把Redis当Kafka
5.1 LIST 做简单队列:BRPOP 阻塞读
Redis 的 List 结构非常适合做轻量消息队列。生产者LPUSH把消息推到列表头部,消费者BRPOP从列表尾部阻塞读取,没有消息时消费者会一直挂在那里等待,而不是空转轮询。这种模式称为 BRPOP 阻塞队列,实现简单,延迟低。
# 生产者 r.lpush("task:queue", json.dumps(task_data)) # 消费者 while True: _, message = r.brpop("task:queue", timeout=0) handle_message(json.loads(message))这个方案适合任务量可控、对消息可靠性要求不那么极端的场景,比如发送通知、生成缩略图、异步计算排行榜。但有几个问题必须清楚:第一,消息被BRPOP取出后,消费者如果处理失败,消息就丢了,除非先把消息取出再确认处理成功,否则没有重试机制。第二,List 不能按主题消费,多个消费者共享同一个队列时,一条消息只会被一个消费者取走,如果想广播给多个消费者,就得用 Pub/Sub 或 Stream 的消费组。
5.2 Pub/Sub 的缺点:丢消息
Redis 的 Pub/Sub 是发布订阅模式,发布者把消息发到 channel,所有订阅了该 channel 的客户端都能收到。这种模式适合实时通知、在线状态广播,比如系统内推送提醒、配置变更通知。但它的最大短板是“不做持久化”。消息发出去的时候,如果某个消费者还没连上来,它就永远收不到这条消息。Redis 也不会帮你存消息副本,只要订阅者不在线,消息就没了。
另外,Pub/Sub 的消息是即发即弃的,Redis 会尽可能把消息推给所有活跃订阅者,但如果有消费端处理速度跟不上,消息会积压在 Redis 的 socket 缓冲区里,可能触发内存增长,甚至把 Redis 拖垮。
所以我的建议是,Pub/Sub 只适合对消息丢失不敏感的业务,比如“服务在线状态通知”“前端推送刷新信号”。核心业务消息,还是用下面要说的 Stream 或者真正的消息中间件。
5.3 Stream 类型:相对完整但仍有取舍
Redis 5.0 引入的 Stream,是为消息队列专门设计的数据结构,弥补了 List 和 Pub/Sub 的不足。Stream 支持消息持久化,也是 Redis 主从复制的一部分;支持消费组,类似 Kafka 的 consumer group,每个组内多个消费者可以分工消费不同分区;支持消息确认(ACK),消费者处理完消息后发送确认,没确认的消息还能被重新读取,不会轻易丢失。
Stream 的核心命令有XADD添加消息、XREADGROUP消费组读取、XACK确认消息。一个简单的消费组流程如下:
XGROUP CREATE streamKey groupName 0 XREADGROUP GROUP groupName consumerName COUNT 10 STREAMS streamKey > XACK streamKey groupName messageIdStream 让 Redis 在消息队列场景变得正经了很多,但要说它能完全替代专业消息中间件,我还是持保留态度。首先是消息积压时的内存占用问题,Stream 数据始终在内存里,大量消息堆积会撑爆内存;其次是消费组状态管理能力远不如专业中间件完善,比如死信队列、消费者重平衡策略都得自己写。你可以把 Stream 理解成“比 List 更可靠一点的轻量队列”,适合中低吞吐、希望少引入中间件依赖的项目。
5.4 选型建议:用Redis还是用专门的消息队列
关于 Redis 做消息队列,我的判断标准只有一个:消息是否允许丢失或是否需要复杂的消息路由。如果允许少量丢失,且业务体量不大,直接用 List 或者 Stream 能省掉一整套 Kafka 或 RabbitMQ 的运维成本。如果消息涉及订单支付、财务流水、日志审计,或者需要多消费者组、重试、死信、持久化到磁盘,那老老实实上专业消息队列。
我见过一个项目,早期图省事,用 List 做了订单超时取消的延迟队列,结果 Redis 某次重启后有消息没来得及处理,导致一批订单没被取消,用户被多扣了钱。从那之后,凡是涉及资金和状态流转的消息,我都建议至少用带持久化和 ACK 机制的 Stream,再配合手动补偿,别把宝全押在 Redis 的队列上。
6. 用途五:会话管理与状态存储
6.1 登录态与 Session 集中管理
普通单体应用会把 Session 存在应用服务器内存里,但一旦部署多个实例,用户的登录态就分散在不同机器上,负载均衡把请求打到另一台机器时,用户就得重新登录。解决思路是把 Session 从应用内存搬到 Redis,所有实例都从同一个 Redis 读取 Session,登录态自然就统一了。
做法很简单:登录成功后,生成一个唯一的 token(比如 UUID),以 token 作为 key,把用户信息作为 value 写入 Redis,同时设置过期时间。后续每次请求带着 token,应用从 Redis 查用户信息,查询不到就判定未登录或登录过期。
token = uuid.uuid4().hex r.setex(f"session:{token}", 1800, json.dumps({"user_id": 1024, "role": "admin"}))6.2 SETEX 与 Hash 的选择
存储 Session 时,可以用 String 结构序列化整个用户信息,一条SETEX搞定,简单直观。但如果需要修改 Session 里的某一个字段,比如更新角色、增加积分,String 结构就要先取出来反序列化、改完再写回去,不仅麻烦,还有并发覆盖风险。这时候更合适的是 Hash 结构,每个字段对应一个独立的值。
r.hset(f"session:{token}", "user_id", 1024) r.hset(f"session:{token}", "role", "admin") r.expire(f"session:{token}", 1800) r.hincrby(f"session:{token}", "score", 10)Hash 的另一个好处是可以单独修改字段,不用整体覆盖;坏处是每个字段会额外消耗一点内存。如果 Session 数据字段少、读取频次高,String 更省内存;如果字段多且需要频繁局部更新,Hash 更合适。没有绝对标准,按访问模式选就行。
6.3 分布式环境下的 Session 一致性
当多个服务实例共享 Redis 时,Session 本身的一致性问题主要靠 Redis 的过期和更新策略来保证。写 Session 时要注意,不要给每个实例各自维护一份“本地Session”缓存,否则会出现这台实例看到登录态,另一台看不到的情况。
另一个要考虑的是 Session 过期时间的“续期”设计。用户一直在操作,Session 不应该 30 分钟后就失效;用户长时间不操作,才应该自动过期。常见做法是每次请求时滚动更新过期时间,或者抽一个中间件,在请求进来时执行EXPIRE session:token 1800。这个续期操作要注意频率,如果每个请求都执行一次 EXPIRE,虽然 Redis 处理很快,但也会增加不必要的命令量。更稳妥的方式是按阈值续期,比如距上次续期超过 5 分钟才真正执行 EXPIRE。
6.4 Session 与缓存分实例的实践建议
很多公司为省成本,把 Session 和业务缓存放在同一个 Redis 实例。短期看没什么问题,但一旦缓存数据量暴涨触发maxmemory-policy淘汰,Session 也可能会被淘汰,用户集体掉线。所以我强烈建议,至少用不同的逻辑库或不同 key 前缀,更好的方法是直接分实例部署,缓存实例开 LRU 淘汰,Session 实例开noeviction策略,内存不足时拒绝写入而不是静默淘汰。
noeviction在 Redis 内存耗尽时,会返回 OOM 错误而不是自动删 key,这样可以保护 Session 不被莫名挤掉。代价是服务会报错,但报错总比用户静默掉线好排查。线上环境里,我会给 Session 实例单独设置 maxmemory,并且加上监控告警,内存到 80% 就提前预警。
7. 常见问题速查与实操心得
7.1 我遇到的典型问题与排查思路
做 Redis 相关项目,最常遇到的无非下面这几个问题,我整理成一张速查表,方便你排查时直接对照。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| Redis 内存快速增长 | 缓存 key 无过期时间、队列积压、大 value | 用redis-cli --bigkeys找大 key,用SCAN遍历 key 看 TTL,重点排查Stream或List是否有未消费消息 |
| CPU 飙高 | 高复杂度命令、慢查询、大量 key 同时过期 | 用SLOWLOG GET查看慢命令,避免在 Redis 里执行KEYS、SORT、大范围ZRANGE;分散过期时间 |
| 连接数被打满 | 客户端连接泄漏、应用线程池过大 | 用CLIENT LIST查看客户端来源,检查连接池最大连接数和空闲回收配置 |
| Redis 启动后数据丢失 | 没开持久化,或 RDB/AOF 配置不当 | 确认appendonly和 RDB 策略;区分缓存场景可接受丢失,核心数据建议开 AOF |
| 主从切换后锁失效 | 主从复制延迟导致锁记录未同步 | 评估 Redlock,或业务层加幂等控制;更高可靠场景引入专用协调组件 |
| 延迟偶发飙升 | 大 key 删除阻塞、fork 生成 RDB 阻塞 | 避免超大集合,删除用UNLINK代替DEL;持久化时错峰,或放到从节点 |
其中有两点值得单独强调。第一,KEYS命令绝对不要在生产环境用,它会阻塞 Redis 单线程执行,导致所有命令排队。改用SCAN游标遍历,虽然慢一点,但不阻塞。第二,大 key 删除要小心,直接DEL一个几百万成员的 ZSET,会长时间阻塞 Redis 主线程。用UNLINK命令,Redis 会在后台异步回收内存,实测能明显降低阻塞时间。
7.2 我的 Redis 配置清单
我不太喜欢每次开会都从零讨论 Redis 配置,就直接分享一份个人常用的基础配置,按场景微调即可。
# 基础 bind 0.0.0.0 port 6379 daemonize yes protected-mode yes requirepass your_password # 内存 maxmemory 4gb maxmemory-policy allkeys-lru maxmemory-samples 10 # 持久化(根据场景开启) appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 慢查询 slowlog-log-slower-than 10000 slowlog-max-len 128maxmemory-samples控制 LRU 淘汰时的采样数量,调大一点会让淘汰更精确,但会增加 CPU 消耗,10 是主流推荐值。appendfsync everysec是性能与安全之间的平衡,每秒钟刷一次盘,极端情况下最多丢一秒数据。如果对 Redis 要求极高,可以always,但性能下降明显,大部分业务用不到。
7.3 一点个人经验
Redis 五种用途听起来是五个独立的技术点,但真正把它们融会贯通之后,你会发现自己对分布式系统的理解也顺了。缓存让我学会考虑热点和过期;分布式锁让我理解并发控制的边界;排行榜让我珍惜数据结构的威力;消息队列让我明白异步不是万能的;Session 管理让我重视无状态设计和容灾。这个顺序也是我建议新手的进阶路径,先玩熟缓存,再做锁和计数器,然后尝试队列,最后思考会话管理,每一步都在积累下一阶段的直觉。
最后留一个非常实用的小技巧:Debug 时多用MONITOR命令临时看实时命令,但千万别在生产环境长时间开着,它会消耗大量资源。更好的习惯是结合客户端日志和 Redis 的SLOWLOG,基本上能够还原大多数问题的现场。Redis 本身不难,难的是你愿不愿意在各种奇怪的现场里盯住关键指标,把它当成一门长期手艺来打磨。