
从面试被问到“Redis 有哪些使用场景”到自己动手在项目里落地Redis这中间其实隔着一大段路。很多人背过“缓存、分布式锁、消息队列”这几个标准答案可真到了线上面对具体业务需求时往往不知道该用哪种数据结构、该注意哪些边界问题。这篇文章我不会去贴一堆抽象理论而是直接梳理我平时最常遇到的16个Redis使用场景每个场景都带数据结构选型、核心命令、实际踩坑记录。无论你是刚学Redis的新手还是准备面试的开发者或者是正在设计方案的技术负责人我都尽量用讲人话的方式把场景背后的设计思路说清楚。1. 高频核心场景缓存、锁与计数Redis最让人依赖的能力就是它的速度。单线程加上基于内存的存储让它能在毫秒级别完成读写。可正因为快很多人都喜欢把它往各个方向硬塞结果反而用错了数据结构。下面这四个场景是我认为Redis最“本分”的用途也是你设计任何高并发系统时最先要考虑的底座。1.1 缓存加速最经典也最容易出事故的场景缓存是Redis的立身之本。它的核心思路很简单把热点数据从关系型数据库搬到Redis里让请求不再每次都去查MySQL或者PostgreSQL。我常用的方式是Cache Aside模式也就是先读缓存读不到再去读数据库然后把结果回填到缓存里同时设置合理的过期时间。为什么大多数团队都选这种模式因为它实现简单、对数据库侵入小。用伪代码描述一下核心逻辑def get_user_profile(user_id): key fuser:profile:{user_id} data redis.get(key) if data is not None: return deserialize(data) # 反序列化 data db.query(select * from user where id%s, user_id) if data: redis.setex(key, 3600, serialize(data)) return data代码看着简单实际落地全是细节。第一个坑是缓存穿透。如果一个用户ID根本不存在那么每次请求都会打到数据库缓存完全失效。我的解决办法是对空结果也做缓存比如缓存一个空对象过期时间设短一点比如60秒同时配合布隆过滤器在请求进入时先挡一波无效key。第二个坑是缓存雪崩也就是大量key在同一时间过期导致请求全部打到数据库。我在实际项目里一般会在设置过期时间时加上一个随机偏移量比如expire_time base_time random.randint(0, 300)避免key集体失效。第三个坑是缓存击穿某个热点key在过期瞬间被大量请求同时打到数据库此时可以限制只有一个线程去重建缓存其他线程短暂等待或者用逻辑过期这种方案。关于序列化这里特别提醒一句。你在热搜词里能看到“redis序列化”这个关键词说明很多人都被坑过。用Redis Desktop Manager看数据时如果看到一堆\xac\xed\x00\x05t...这样的乱码那就是JDK原生序列化的产物。我在Spring Boot项目里会统一配置Jackson序列化让Redis里存的是可读的JSON格式排查问题会轻松很多。1.2 分布式Session共享多实例部署时必用的方案传统单机应用里用户登录后可以用HttpSession保存状态因为所有请求都落在同一台服务器上。只要应用做了负载均衡多个实例之间就不知道彼此的Session用户会被频繁踢出登录状态。Session共享是Redis非常成熟的场景。做法不复杂Session数据统一存到Redis里以Session ID作为key过期时间就是Session的有效期。用户每次请求都带着Session ID服务端从Redis里查出对应数据。这样即使后端扩容到几十个实例也不影响用户状态。这里有两个容易踩的坑。第一Session ID需要在Cookie里设置HttpOnly属性防止前端脚本读取第二要注意Redis里存储对象的过期策略。Session数据不是删除了就没事了如果Redis内存满了触发淘汰策略时会把Session挤掉用户会莫名掉线。所以生产环境千万不要用allkeys-lru要尽量用volatile-lru只淘汰设置了过期时间的key避免误杀长期有效的Session。1.3 分布式锁用SET NX加上Lua脚本保证原子性在单体架构里处理并发可以用Java的synchronized或者ReentrantLock进程内锁就够了。可一旦拆成多个服务实例这种锁就失效了。Redis分布式锁是应对这种场景的常见手段。严格按照正确姿势来实现分布式锁有两个关键点。第一加锁要用SET key value NX PX 30000它在一个命令里同时完成了“不存在才设置”和“设置过期时间”保证原子性。很多人用旧版本命令先SETNX再EXPIRE中间如果服务挂了锁就会永远不释放。第二释放锁要用Lua脚本先比较value再删除避免自己把别人持有的锁误删了。核心逻辑是这样的if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end这个脚本保证只有持有锁的客户端才能释放锁value用UUID生成确保唯一性。不过Redis分布式锁还有个经典问题如果进程在锁过期后还没处理完业务逻辑其他进程就能拿到锁导致并发问题。我在真实项目里的建议是锁的超时时间要足够覆盖业务最慢情况同时业务量大的系统可以考虑看门狗机制延长锁有效期而不是偷懒只设置一个固定过期时间。1.4 点赞、收藏、访问量计数器INCR是最快的方式计数器类是Redis的天然优势场景。点赞数、文章浏览量、在线人数、库存扣减这些高并发写入的场景如果每次都去数据库更新行记录数据库压力会非常大。Redis的原子自增命令INCR和INCRBY就能完美应对。举个例子文章详情页展示阅读数最简单的方式是INCR article:read_count:1001这个操作是原子性的即使多个请求同时到达Redis内部也会一条条执行不会出现并发覆盖的问题。如果是移动端App统计启动次数、每日签到次数也可以用INCRBY一次加多个数值。计数器场景要额外考虑持久化。Redis是内存数据库如果开了AOF持久化Append Only File可以做到每秒刷盘最多丢失1秒数据如果只用RDB快照可能丢失好几分钟的数据。我在线上一般会同时开启AOF和RDBRDB做冷备AOF做增量恢复这样数据安全性高很多。如果你做的计数器数据要求严格不能丢可以在Redis计数后异步把数据同步到MySQL或MongoDBRedis只做高并发读写的屏障。2. 时效性场景排行榜、过期与延迟Redis自带的所有数据结构里ZSET有序集合和EXPIRE机制是我用得最频繁的工具。它们解决的不是存储问题而是“时间维度的业务问题”。这一节我会把排行榜、验证码过期、延迟队列、签到统计这类“和时间打交道”的场景拆开讲每一处都有对应的数据结构。2.1 排行榜TOPNZSET是唯一正解排行榜场景几乎是ZSET的标配。ZSET的每个成员都有一个scoreRedis会根据score自动排序。做积分排行榜时score就是积分值member就是用户ID查询前100名直接ZREVRANGE game_rank 0 99 WITHSCORES如果要做的是“实时积分更新”比如用户完成一局游戏加了20分直接用ZINCRBY game_rank 20 user_10086Redis会重新排序复杂度依然是O(logN)。这个特性让ZSET在电竞排行榜、热销商品榜、创作榜单里成为碾压级方案。很多人问我ZSET底层到底是什么它内部其实是一个跳跃表Skip List加哈希表。跳跃表让有序遍历变得很快哈希表则让直接查找成员的分数也很快。所以你能以O(logN)复杂度完成查找成员、插入、更新分数、范围查询。理解了这个你就能预判它对大流量榜单的支撑能力。榜单场景还要注意一个细节如果排行榜想按时间维度拆成“周榜”“月榜”可以用时间戳作为score的辅助字段或者在key里加日期后缀例如rank:20250616每天新建一个榜单key这样历史榜单保留自然过期不用手动清理。2.2 验证码与限时数据EXPIRE让数据自动失效验证码有效期5分钟、优惠券有效期24小时、临时授权码有效期30秒。这种“过期自动失效”的需求用Redis的过期机制再合适不过。命令很简单SET phone:verify:13812345678 841230 EX 300这里EX 300表示300秒后自动删除。相比存数据库然后定时任务去删Redis的做法既简单又精准。实际开发中有一个我反复踩过的坑验证码短信通道万一发送失败用户点击“重新获取”此时如果简单再SET一次会覆盖旧验证码攻击者可以通过反复点击让旧验证码一直失效。更合理的做法是先判断key是否存在如果存在则返回“请稍后再试”过期后再允许重新发送。还有一个和过期时间相关的经典面试题Redis的过期删除策略到底是什么。Redis默认用惰性删除加定期删除。惰性删除是当key被访问时才发现过期了并删除定期删除是每秒抽样检查一部分key删除其中过期的。理解这一点对设计缓存过期时间有帮助因为你的key如果一直没人访问可能不会立即被物理删除而会占着内存直到定期扫描。2.3 延迟队列用ZSET按时间戳实现延时任务很多业务需要延迟处理下单后30分钟未支付自动关闭、用户注册后24小时未完成资料补齐发提醒、定时任务轮询。传统办法是起一个定时任务每秒钟扫表但数据库压力大、时效性一般。用Redis的ZSET可以做一套轻量级延迟队列。设计思路是把任务执行时间戳作为score任务ID作为member一个后台进程每次ZRANGEBYSCORE queue 0 当前时间戳 LIMIT 0 100把到期任务取出来然后用ZREM删除已处理的任务交给业务线程池执行。伪代码如下while True: now time.time() tasks redis.zrangebyscore(delay_queue, 0, now, start0, num100) for task in tasks: process(task) redis.zrem(delay_queue, task) time.sleep(1)这套方案能扛住千万级任务量缺点是每台消费者机器都在轮询同一批到期任务处理时要做好幂等防止重复消费。我一般会在处理任务前写一个去重标记比如在另一个key里记录已经处理的任务ID设置2小时过期。2.4 签到与活跃用户BITMAP比Set节省大量内存如果产品需要统计用户365天的签到记录、判断用户是否连续签到7天甚至统计日活用户数用Set存日期和用户ID当然可以但内存会非常大。BITMAP是更聪明的做法。BITMAP不是新的数据结构而是字符串类型的位操作。把每一天当作一个位图每个bit表示一个用户是否访问用户ID是偏移量。签到一天的记录可以这样写SETBIT sign:20250615 10086 1统计今天有多少用户活跃可以用BITCOUNT sign:20250615。如果用户量一亿1天只需要1亿bit约12MB内存365天也就是4GB左右可接受。踩坑提醒BITMAP的偏移量最好是数字用户ID或者用户ID做hash后的值不能直接用字符串。如果用户ID是雪花算法生成的18位数字直接做偏移量也是可以的。另外当你要“连续签到”判断时可以循环GETBIT检查近7天的bit这个操作是O(1)的速度极快。3. 队列、社交与位置服务类场景除了核心的缓存和时效性场景Redis在消息队列、实时通知、位置搜索和随机抽奖这几个领域也有大量应用场景。很多时候我们没有必要为轻量级需求引入Kafka或RocketMQRedis足以扛起中等规模系统的消息处理职责。3.1 轻量级消息队列List和Stream各有什么优劣Redis的List支持LPUSH加BRPOP可以实现一个可靠的生产者消费者模型。生产者把任务从左边推入消费者从右边阻塞弹出这样不会空转消耗CPU。要把它当作队列核心命令LPUSH task_queue task_data BRPOP task_queue 0List队列的优点是实现极其简单、性能高、无需额外组件。缺点也很明显不支持多消费者消费同一份消息的广播模式消息没有ack机制消费者拉取后如果处理失败消息就丢了。对于可靠性要求更高的场景Redis 5.0引入的Stream数据结构更合适。Stream类似Kafka每个消息有唯一ID消费者组能独立记录消费位点还能通过XPENDING查看未确认消息并用XACK确认。如果团队不想引入Kafka又需要消息不丢和消费组能力Stream是很好的折中方案。我的建议是如果你的消息队列只是为了削峰填谷任务丢了重新生成也没关系用List完全够如果涉及支付回调、订单状态变更这些不容丢失的消息建议用Stream或者直接上Kafka别硬撑。3.2 发布订阅实时通知与广播Redis的Pub/Sub模式适合做实时消息推送比如用户上线通知、系统广播、WebSocket消息分发。发布者往一个频道发消息所有订阅这个频道的客户端都会实时收到。代码示例SUBSCRIBE news.sports PUBLISH news.sports one score!Pub/Sub的优点是延迟极低缺点是消息不持久化如果订阅者不在线消息就永久丢失了。所以它适合“在线状态通知”“大厅广播”这类即使丢几条也问题不大的场景。如果要求落盘后再推送给用户还是要用Stream或者专业的消息队列。有一个面试题很经典Redis的Pub/Sub和ZSET延迟队列有什么区别答案是Pub/Sub是实时广播没有存储消费完就没了延迟队列是存储型服务重启数据还在。选择之前先想清楚数据丢失的容忍度。3.3 附近的人GEO类型实现位置检索如果App里有“附近的人”“附近的店铺”“打卡地标”这类需求Redis的GEO功能可以派上大用场。GEO本质上是对ZSET的封装把经纬度编码成GeoHash后作为score存储。添加位置GEOADD store_location 116.397128 39.916527 store_1001查询附近5公里内的店铺GEORADIUS store_location 116.397128 39.916527 5 km WITHCOORD WITHDIST ASCGEO的查询性能非常高适合轻量级LBS应用。但如果范围查询规模很大或者有复杂的多边形区域检索建议还是用专门的ES Geo或PostGIS方案。还有一点要注意经纬度坐标的精度直接影响距离计算结果添加数据时尽量用高精度GPS坐标。3.4 抽奖与随机玩法SET和SRANDMEMBER抽奖是运营活动的常见玩法。从需求拆解看有两类抽奖一类是从参与用户中随机抽取奖励另一类是每个用户按权重随机命中不同奖励。Redis的SET可以存参与用户列表抽奖时用SRANDMEMBER lottery:event_1001 3 # 随机抽取3个不重复用户 SPOP lottery:event_1001 # 随机取出并移除一个用户区别在于SRANDMEMBER不会从集合中删除元素适合“抽完还能继续参与下一轮”SPOP会真正移除适合“抽完即退出抽奖池”。如果奖品有不同权重可以使用ZSET把抽奖概率映射到score范围用ZRANDMEMBER或ZRANGEBYSCORE随机落点。抽奖场景要关注防刷。我一般会在Redis里设置每个用户的抽奖次数限制比如INCR user:lottery:1001加EXPIRE 86400配合用户维度锁防止并发下同一用户刷爆奖池。4. 数据保护与复杂业务场景第四个板块是相对进阶的使用场景也是面试里最容易深挖的布隆过滤器、Hash购物车、分布式ID生成和限流。这些场景不仅考验你是否会用Redis还考验你能不能设计一个健壮的系统。4.1 布隆过滤器拦截无效请求保护数据库缓存穿透的一种解决方案就是布隆过滤器。它的原理非常巧妙用多个哈希函数把元素映射到一个bit数组中查询时用相同哈希函数计算位置如果任意一个bit为0说明元素一定不存在如果都是1只能说可能存在。Redis本身没有原生的布隆过滤器类型但Redis 4.0之后提供了Module机制。你可以用BF.ADD、BF.EXISTS指令也可以自己用bitmap实现一个简易版。我在项目里一般直接用Redisson的RBloomFilter它在底层封装了bitmap操作。布隆过滤器适合做“前置拦截”比如注册时判断用户名是否已存在、电商详情页判断商品ID是否有效。它能过滤掉绝大多数的无效请求但不能删除元素误判率会随着数据量上升。使用时要根据预期数据量和误判率计算bit数组大小和哈希函数数量公式是m -n * ln(p) / (ln2)^2。4.2 购物车与复杂对象Hash是结构化的最佳选择购物车数据的特点是一个用户对应多个商品每个商品有数量、选中状态。这种结构天然适合Hash。可以用一个Hash存整个购物车field是商品IDvalue是数量HSET cart:user_1001 goods_1 2 HSET cart:user_1001 goods_2 1 HGETALL cart:user_1001Hash的好处是支持对单个字段进行增删改查内存占用也比较低。如果你的商品信息非常复杂比如还包含价格快照、优惠券信息可以存JSON字符串但要处理好反序列化的兼容性。我也见过有人把购物车直接拼成一个大JSON字符串用String类型存这在小规模应用没问题但用户加购、取消时就需要取全量再覆盖写性能差。改成Hash后单个商品的更新就是一次原子操作体验完全不一样。购物车场景的过期策略要慎重。用户今天加购隔了一个月回来看购物车不应该莫名其妙被清空。我的做法是不设置过期时间推进用户清理或者服务端定期归档但为了不占用Redis过多内存可以对超过30天不活跃的购物车做冷数据迁移。4.3 分布式ID生成INCRBY和雪花算法互补很多场景需要全局唯一ID比如订单号、消息ID、日志ID。Redis可以提供一种简单方案一个全局自增key每次业务方调用INCRBY获取一段ID区间然后在本地内存里生成不重复ID。核心命令INCRBY order_id_generator 1000这个方案的好处是ID趋势递增对数据库B树写入友好性能也很好。它的缺点是依赖Redis的高可用如果Redis挂掉ID生成就断了。所以很多公司会用雪花算法Snowflake做本地生成不依赖外部服务但雪花算法要保证机器ID唯一并且时钟回拨要处理得当。我个人的选型经验是并发量不大走Redis INCRBY省心并发量极高用雪花算法或者号段模式。号段模式其实是Redis INCRBY的变体。服务启动时一次性从数据库或者Redis取一批号段比如从1到10000本地内存用完了再取下一批。这样数据库和Redis的压力都很小。4.4 限流固定窗口、滑动窗口与令牌桶接口限流是保护系统的关键手段。Redis实现限流有好几种方案。最简单的固定窗口用INCR加EXPIRE每秒计数器超过阈值就拒绝。INCR api:limit:/order EXPIRE api:limit:/order 1但固定窗口有临界问题比如第一秒的最后一毫秒和第二秒的最初一毫秒都可能达到阈值导致两秒内实际流量是阈值的两倍。更平滑的做法是滑动窗口用ZSET记录每个请求的时间戳统计时间窗口内请求数ZADD rate:user_1001 1718000000 1718000000 ZREMRANGEBYSCORE rate:user_1001 0 1717999400 ZCARD rate:user_1001如果还想做到更平滑的流量整形可以用令牌桶。Redis结合Lua脚本实现令牌桶算法比如用Hash存令牌数和上一次取令牌的时间以固定速率补充令牌。这样可以把突发流量平滑成一个均匀的下游流量。我线上常用Sentinel或者Guava做单机限流分布式限流用Redis脚本效果已经很稳定。5. 选型、高可用与实操心得场景讲得再多最终还是要落到一个稳定的运行时环境上。这几年很多人会问我Redis到底怎么安装、怎么上生产、怎么配置主从才能高可用。我不打算展开讲很长的部署教程重点分享几个直接影响使用体验的实操要点。5.1 安装与配置Windows、Docker与可视化工具Redis官方推荐在Linux上运行但开发调试时Windows用户也一样能跑。早期Redis在Windows上体验很差靠微软团队维护的移植版才能用现在更推荐在Windows上用WSL2装Linux环境或者直接用Docker跑官方镜像。如果坚持用原生Windows版本下载时注意认准稳定版别随便找一个第三方编译包。我习惯在Docker里做开发测试docker run -d --name redis -p 6379:6379 redis:7.2-alpine要模拟主从架构可以用Docker Compose一次起两个容器主节点设置密码从节点通过REPLICAOF同步。主从的核心意义是读写分离和数据冗余但要注意主从是异步复制主节点故障时从节点可能缺最新数据所以真正要追求高可用必须引入哨兵Sentinel或者Cluster模式。可视化工具方面Redis Desktop Manager依然是很多人首选但新版换了许可证需要关注授权。现在也有不少免费工具比如Another Redis Desktop Manager。重点不在于工具本身而在于连接生产环境时要做访问控制不要裸奔。5.2 序列化、内存与性能排查序列化这个点值得单独拿出来讲。Redisson、Spring Data Redis在用的时候默认序列化策略不同你存进去的是对象拿出来可能就是乱码。我见过最典型的场景在Redis Desktop Manager里看到key全是\xac\xed...然后排查半天发现是JDK序列化。解决方案是统一Key使用String类型Value用Jackson或者Fastjson2序列化成JSON数组和对象结构都清晰可见线上排查问题的速度快很多。内存排查用INFO memory找到内存增长原因用redis-cli --bigkeys扫描大key。大key是Redis性能杀手一个值几MB的String或者一个上千万的集合在遍历和扩容时都会阻塞主线程导致服务卡顿。我的经验是超过10KB的value就值得警惕尽量拆分用SCAN循环查大key时消耗较大不建议在业务高峰期执行。5.3 主从、哨兵与持久化策略生产环境Redis的高可用方案我最推荐的是哨兵模式加一主二从。哨兵负责监控主节点状态主节点挂了就自动从从节点中选举一个新的主节点。整个过程业务无感知比手动切换靠谱得多。持久化策略要根据业务容忍度来选缓存场景可以只开RDB一小时一次快照数据重要场景必须开AOF用appendfsync everysec做每秒刷盘这样最多丢1秒数据。注意AOF文件越来越大后Redis会在后台自动重写不会影响业务。如果不小心把Redis当数据库用对数据零丢失那就必须考虑AOF加RDB双开甚至引入外部备份机制。我个人在实际操作中还有一个很深的体会Redis真的不是一个“放进去就万事大吉”的组件。它很强大但你必须清楚每个场景背后的数据安全边界。缓存丢了可以重建计数器丢了可能还能容忍但延迟队列、购物车、分布式ID这些一旦丢数据业务会直接出问题。所以无论你用哪个场景先想清楚可靠性再来谈性能。最后分享一个小技巧——上线前用redis-benchmark压一下你的核心命令确认QPS和延迟在预期内。再热门的场景也怕一个慢查询或大key突然冒出来。把这些细节都照顾到了Redis才会真的成为你系统里的高速公路而不是定时炸弹。