资讯动态

没有高并发也能用它:Redis 的业务需求判断与场景实践

发布时间:2026/8/30 2:07:04 来源:尧图企业网站定制
Redis 并不只是高并发系统的专属组件。很多人抱着“系统没有高并发就不用 Redis”这样的想法结果在项目里用数据库自增做订单号、用本地内存缓存热点数据、用 synchronized 处理分布式环境下的幂等问题等到系统拆成多个服务、并发量稍微上来一点才发现每一处都要返工。这个观点的误区在于把 Redis 的价值窄化成了“抗压工具”忽略了它其实是通用存储、分布式共享组件和轻量消息系统的集合体。本文想说的核心观点是要不要引入 Redis应该由业务是否有缓存、计数、队列、分布式协作等需求来决定而不是由“并发量高不高”来决定。理解了这一点很多架构选型和面试回答都会顺很多。1. 重新理解“高并发”与 Redis 的真实关系1.1 “没有高并发就不用 Redis” 这句话错在哪里“高并发”在很多人心里就是几万 QPS、千万日活、大促秒杀。Redis 被广泛宣传为抗高并发的缓存利器于是一个反向推论就出现了系统流量不大Redis 没有用。这个推论在逻辑上就不成立因为 Redis 能否带来收益不取决于流量规模而取决于你有没有需求。先看一个最简单的例子一个内部管理后台日访问量只有几百次但某个列表页需要 join 五张表每次查询耗时 3 秒。这个系统肯定不算高并发但它实时命中了一个明确的痛点查询慢。用 Redis 缓存列表聚合结果后单次查询耗时降到 10 毫秒以内。这里 Redis 解决的不是并发压力而是延迟成本和数据库负载。再看订单号生成场景。没有高并发的商城系统同样面临跨天自增、多实例同时生成订单号的问题。数据库自增可以用但每生成一个订单号都要请求一次数据库事务日志和锁都有开销时间戳拼接虽然快又可能重复。Redis 的INCR命令用原子自增解决问题既不依赖并发量也不需要额外表结构。可见Redis 的价值远不止“扛并发”。所以这句话更准确的说法是没有高并发时Redis 的抗压能力可能用不上但它的数据结构、原子操作和分布式共享能力仍然可能被需要。1.2 业务复杂度比并发量更能决定要不要使用 Redis很多系统虽然没有高流量但架构并不简单。我见过不少项目从测试环境开始就是多副本部署前面挂 Nginx 做负载均衡应用节点至少两个。只要出现多实例就立刻会遇到以下问题本地内存缓存的两个节点数据不一致。A 节点更新了配置B 节点还是旧值。synchronized只能锁住当前进程。两个请求同时落到 A、B 两个节点重复提交无法被拦住。生成本地计数器无法全局唯一不同节点可能生成相同编号。应用重启后内存数据丢失无法恢复会话状态或临时标记。这些问题和 QPS 没有关系只和“系统是不是多实例”“状态是否需要跨进程共享”有关系。Redis 恰好是解决这类问题最廉价的方案。很多低流量项目其实早就需要 Redis只是因为“没有高并发”的思维定式迟迟没有引入反而把代码写得很别扭。1.3 从哪些指标判断 Redis 值得引入判断是否引入 Redis可以按下面几个维度逐项检查维度典型问题是否适合 Redis缓存加速热点数据反复查库单次查询慢数据库 CPU 经常波动适合跨进程共享多个应用实例需要读取同一份配置、Token、限流计数适合原子操作需要全局自增、库存扣减、防重复提交适合过期语义临时验证码、会话、活动状态需要自动失效适合消息通知系统内的异步任务、事件通知、延迟处理适合强事务与复杂查询多表关联、事务回滚、复杂聚合报表不适合不应把 Redis 当主存储如果一个项目全部命中“不适合”那一列那确实可以不引入 Redis。但只要命中一到两列就应该认真评估。高并发只是导致缓存、限流、分布式锁这些需求被放大的一种情况并不是需求本身。2. 数据类型和应用场景要先于并发量考虑Redis 常被说成“键值数据库”但它的核心优势是提供丰富的数据结构和对应操作。很多中小项目引入 Redis往往只是用String做缓存这其实只发挥了 Redis 很小一部分能力。理解数据类型才知道低流量项目里 Redis 还能做什么。2.1 String缓存、计数器、限流String是最常用的类型但用途不只是缓存。SET可以带过期时间INCR、DECR能原子自增INCRBY支持按步长增加SETNX可以做分布式锁的原子占位。# 缓存用户信息10 分钟过期 SET user:1001 {name:zhangsan} EX 600 # 生成订单号 INCR order:seq:20250101 # 接口限流每秒最多 10 次窗口按秒 INCR rate:123:20250101120000 EXPIRE rate:123:20250101120000 1INCR的原子性很关键。即使两个实例同时执行结果也不会重复。这是跨进程共享计数的基础也是 Redis 在高并发场景被大规模使用的原因但这些能力在低流量项目中同样成立。2.2 Hash对象字段级读写String存整个 JSON 时如果要修改其中一个字段只能整体读出来再写回。Hash类型允许对对象内部字段单独操作# 写入订单对象 HSET order:202501010001 status CREATED amount 99.50 # 只修改状态字段 HSET order:202501010001 status PAID # 读取单个字段 HGET order:202501010001 status当对象字段频繁更新、又不需要一次读取全部字段时Hash比String更节省内存也减少不必要的数据序列化。比如用户资料、订单状态、设备状态都适合用Hash。2.3 List、Set、ZSet 分别解决什么List适合队列和时间线数据。左侧写入、右侧读取可以实现轻量 FIFO 队列LRANGE可以分页读取消息列表。LPUSH notify:queue order:1001 RPOP notify:queueSet适合去重和集合运算。共同好友、标签集合、用户关注列表都可以用Set表达SADD user:1:follow 100 200 300 SADD user:2:follow 200 300 400 SINTER user:1:follow user:2:follow # 返回 200 300ZSet是在Set基础上带分数可以按分数排序。排行榜、按时间排序的消息列表、延迟任务的时间戳队列都是典型场景ZADD rank:20250101 100 user:1 ZADD rank:20250101 90 user:2 ZREVRANGE rank:20250101 0 9 WITHSCORES这些结构并不需要等到高并发才用。一个几十万用户的社区应用每天讨论量不大但“共同关注”“热门排行”已经是很自然的需求。2.4 Bitmap、Geo、Stream 的使用时机除了五个基础类型Redis 还提供了一些针对特定场景的结构Bitmap用位表示状态适合签到、在线状态、布尔型埋点。Geo存储经纬度支持附近的人、距离计算。HyperLogLog基数统计适合 UV 统计内存占用极小。Stream高可靠消息队列支持消费者组、ACK、消息回溯。下面用表格做一个速查类型常用命令典型场景不适合的场景StringSET GET INCR SETNX缓存、计数、限流、分布式锁复杂对象频繁字段修改HashHSET HGET HGETALL用户资料、订单状态大字段频繁全量读取ListLPUSH RPOP LRANGE消息队列、时间线需要随机访问索引SetSADD SREM SINTER去重、共同关注需要排序的场景ZSetZADD ZRANGE ZREVRANGE排行榜、延迟队列仅需要去重的场景StreamXADD XREADGROUP XACK可靠消息队列极低频且不关心丢失的通知GeoGEOADD GEORADIUS附近的人精确计算驾车路线BitmapSETBIT GETBIT BITCOUNT签到、布尔标记需要存储复杂结构的场景由此可以看出Redis 的“可广泛使用”来自数据结构而不是“并发”。初学者如果只把 Redis 当成缓存工具就会错过这些本来就需要的编程原语。3. 用一个中小项目验证 Redis 的价值为了说明“没有高并发也需要 Redis”下面做一个最小闭环示例一个日单量不高的订单服务包含热点商品缓存、订单号生成、防重复提交、异步通知四个功能。这个项目不需要大促流量也没有秒杀但每个功能都离不开 Redis 的核心能力。3.1 项目背景一个没有高并发的订单模块假设系统是一个 To B 商城一天订单量几千单访问量不大但是按照部门要求部署了两个应用实例前面有负载均衡。业务上有几个痛点商品详情页经常读取同一批商品数据库单表查询虽然不慢但商品介绍里包含优惠策略、图片列表、库存状态组装起来比较重。订单号不能靠数据库自增因为多实例同时插入时自增由数据库保证但要拼接日期、业务线还要避免重复。前端连续点击提交按钮同一个订单号可能重复创建。下单成功需要通知库存服务但库存服务响应慢不能同步等待。这四个问题都不是高并发问题但都可以用 Redis 解决。3.2 环境准备与依赖配置先准备一个 Redis 实例。开发环境用 Docker 最省事docker run -d --name redis-dev \ -p 6379:6379 \ redis:7-alpine启动后验证连接redis-cli -h 127.0.0.1 -p 6379 ping返回PONG表示正常。然后是 Spring Boot 项目依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependencycommons-pool2是 Lettuce 连接池需要的基础依赖。配置文件spring: data: redis: host: 127.0.0.1 port: 6379 password: timeout: 3000ms lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4连接池参数不建议直接用默认值。max-active太小极端情况会出现连接等待太大又可能占满 Redis 连接。开发环境 32 是常见的起始值生产环境要结合接口峰值调整。3.3 缓存热点商品先读缓存再查库商品详情是典型的“读多写少”场景。即便是低并发系统减少组装数据的时间也能明显降低接口耗时。Service public class ProductService { private static final String PRODUCT_CACHE_KEY product:detail:; public Product getDetail(Long productId) { String key PRODUCT_CACHE_KEY productId; String json stringRedisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return JSON.parseObject(json, Product.class); } Product product productMapper.selectById(productId); if (product ! null) { stringRedisTemplate.opsForValue() .set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES); } return product; } }这里有几个细节需要注意。第一缓存 key 要包含业务前缀避免不同业务互相覆盖。第二缓存的 value 是序列化后的 JSON读取时必须反序列化。第三设置了 30 分钟过期时间避免商品信息长期不更新。第四缓存空值时不要设置过长时间否则商品恢复上线后用户仍然看不到。这个流程本身不依赖并发量。即便每天只有 100 次商品详情访问只要商品数据组装成本高、数据库压力敏感缓存就有收益。3.4 订单号生成用 INCR 避免数据库自增锁订单号需要全局唯一、按日期可读。用 Redis 的INCR实现public String generateOrderNo() { String key order:seq: LocalDate.now(); Long seq stringRedisTemplate.opsForValue().increment(key); stringRedisTemplate.expire(key, 48, TimeUnit.HOURS); return ORD DateTimeFormatter.BASIC_ISO_DATE.format(LocalDate.now()) String.format(%06d, seq); }这里每天一个 keyINCR保证原子自增再拼上日期和序号。第二天自动生成新 key。需要说明的是expire要在increment之后设置因为如果先设置过期时间key 可能还没创建而INCR创建 key 后如果没有过期时间会一直增长下去。给当天订单 key 设置 48 小时过期既能覆盖当天到第二天的补单场景也不会让过期 key 长期残留。INCR能保证多实例环境下序号不重复比直接用时间戳加随机数的方案更可控也比数据库自增减少一次数据库往返。3.5 防重复提交的分布式锁低并发下的必要防御两个应用实例同时收到同一个订单号的重复提交请求时synchronized在单进程内有用跨进程就会失效。这时需要分布式锁。public boolean tryLock(String orderNo, String requestId, long expireSeconds) { String lockKey order:submit: orderNo; Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } public void unlock(String orderNo, String requestId) { String lockKey order:submit: orderNo; String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; stringRedisTemplate.execute( new DefaultRedisScript(script, Long.class), List.of(lockKey), requestId); }setIfAbsent是SETNX的能力扩展它在设置 key 的同时可以指定过期时间比单独执行SETNX再EXPIRE更安全。requestId用来标识当前线程删除锁前先做校验避免删掉别人刚获取的锁。删除锁必须用 Lua 脚本因为“比较值”和“删除 key”是两个操作分开执行会有中间间隙。低并发系统同样需要用分布式锁吗只要部署了两个实例就可能出现同一时间两个请求操作同一个业务对象的情况。数据库行锁可以兜底但 Redis 锁能在业务入口尽早拦截减少数据库锁等待。3.6 消息通知用 Stream 做简单的任务通知订单创建后要通知库存服务。库存服务接口可能耗时也可能暂时不可用。用 Redis Stream 把通知写成异步消息public void notifyInventory(String orderNo) { MapString, Object message Map.of( type, order.created, orderNo, orderNo, time, System.currentTimeMillis() ); stringRedisTemplate.opsForStream().add( StreamRecords.objectBacked(message) .withStreamKey(stream:order-notify) .build() ); }消费端用XREADGROUP配合消费者组处理消息处理成功后调用XACK确认。这个方案的优点是Redis 本身已经存在不用再引入额外中间件Stream比List更适合做可靠队列能记录消费者进度也能回溯消息。验证时可以在命令行查看redis-cli XLEN stream:order-notify redis-cli XRANGE stream:order-notify - COUNT 5如果XLEN返回 1再执行XRANGE能看到刚才写入的消息。这证明异步通知已经生效。4. 防止缓存风险三类攻击和四个常见坑引入 Redis 并不是“加个缓存”这么简单。如果只看到收益忽略使用姿势反而会引入新的故障。下面这些坑和并发量没有关系小流量系统同样会遇到。4.1 缓存穿透、击穿、雪崩的原因与应对这三个问题是缓存场景最经典的故障模型。缓存穿透请求查询一个不存在的数据。比如按不存在的商品 ID 查询缓存没有数据库也没有每次请求直接打到数据库。大量恶意请求或参数异常时即使并发量不高数据库也可能被拖垮。应对方式是缓存空值或者使用布隆过滤器拦截不存在 key。缓存空值时要设置较短过期时间否则数据恢复上线后用户看不到最新结果。缓存击穿某个热点 key 在过期瞬间大量请求同时回源数据库。低并发系统里热点程度有限但成本仍然存在。应对方式是热点 key 不过期或者用互斥锁控制回源逻辑。互斥锁可以使用 Redis 分布式锁实现保证同一时间只有一个请求去查数据库。缓存雪崩大量 key 在同一时间过期或者 Redis 实例不可用导致请求全部落到数据库。应对方式一是给过期时间加随机值避免同一批 key 同时过期二是做好 Redis 高可用三是数据库层做限流和降级。不要以为流量小就可以忽略雪崩。批量初始化缓存时如果统一设置 30 分钟过期到时候就是统一失效。问题现象常用应对实现注意点缓存穿透缓存和数据库都查不到数据空值缓存、布隆过滤器空值过期时间要短缓存击穿热点 key 过期瞬间数据库压力上升互斥锁、热点 key 不过期锁要带过期时间防止死锁缓存雪崩大量 key 同时失效或实例不可用过期时间加随机值、高可用批量初始化时打散过期时间4.2 分布式锁的三个典型坑第一个坑是锁没有设置过期时间。如果获取锁的线程突然宕机锁永远不会释放其他线程全部卡住。解决方式是setIfAbsent时必须传过期时间。第二个坑是锁的过期时间不会自动续期。业务执行时间如果超过锁过期时间锁会被自动释放后面的线程拿到锁两个线程同时进入临界区。解决方式是按业务最长时间设置合理的过期时间更严格的做法是通过 Lua 脚本或 Redisson 的看门狗机制自动续期。第三个坑是删除锁时不校验持有者。线程 A 执行完操作直接把锁删除但如果锁已经过期并被线程 B 重新获取A 就会删掉 B 的锁。解决方式是用requestId作为唯一标识删除前先比较再通过 Lua 原子执行。4.3 避免在生产环境滥用 KEYS 命令Redis 是单线程处理命令的。KEYS pattern会遍历整个键空间数据量大时可能阻塞几十毫秒甚至更久。全站缓存和业务 key 混在一起时KEYS *尤其危险。需要找出某些前缀的 key 时使用SCAN命令redis-cli SCAN 0 MATCH product:detail:* COUNT 100SCAN是分批返回不会一次性阻塞服务。开发环境用KEYS排查问题还可以理解生产环境应该作为禁令执行。4.4 RDB 与 AOF 的选择Redis 持久化有两种方式RDB定期生成全量快照恢复速度快文件体积小但两次快照之间数据可能丢失。AOF记录写命令数据安全性更高但文件体积大重启恢复慢。生产环境建议按数据重要性选择。缓存数据可以接受丢失建议只开 RDB 甚至不持久化订单号、锁状态等不能丢的业务数据建议开启AOF。Redis 7 的混合持久化格式减少了 AOF 重写的开销但选择哪套方案仍要基于业务容忍度。# redis.conf appendonly yes appendfsync everysecappendfsync everysec是安全和性能的常见折中。每次写都fsync最安全但性能最差不fsync性能好但丢数据风险大。对多数中小项目everysec是比较合适的起点。4.5 主从、哨兵、集群的边界不同部署形态应对的是不同问题单机适合开发测试没有高可用。主从复制主节点写入从节点读解决读写压力和数据备份但主节点挂了不会自动切换。哨兵模式在主从基础上增加故障检测和自动主从切换适合生产环境中小规模数据。Cluster 集群数据分片存储适合数据量大且需要横向扩展的场景。不是所有生产环境都需要 Cluster。几十 GB 数据、单点就能承载的情况下主从加哨兵通常更简单。数据量超过单机内存或者写入压力明显超出单实例能力再考虑 Cluster。5. Redis 使用中的排查路径Redis 出问题时很多人的第一反应是“重启一下”。但如果不按链路排查重启后问题还会复现。建议按以下顺序排查。5.1 先看内存和键分布现象通常是Redis 内存持续增长或者内存已经接近maxmemory。先看整体状态redis-cli INFO memory redis-cli INFO stats重点关注used_memory、maxmemory、evicted_keys。如果evicted_keys持续增长说明内存达到上限Redis 正在淘汰 key。再看大 keyredis-cli --bigkeys redis-cli MEMORY USAGE product:detail:1001--bigkeys会扫描键空间并输出大 key 分布。定位到具体 key 后需要判断业务上是否可以拆分、缩短字段、减少存储对象大小。5.2 再看慢查询日志现象是Redis 操作偶发超时或者接口耗时偶尔飙升。执行redis-cli SLOWLOG GET 10 redis-cli SLOWLOG LEN重点关注慢查询中的命令类型。常见慢操作包括KEYS、大集合的SMEMBERS、大 Hash 的HGETALL、大 List 的LRANGE 0 -1。这些命令的时间复杂度与元素数量成正比数据量增长后就会出现慢查询。定位到慢命令后调整客户端代码改成SCAN分批读取或者控制单次读取数量。5.3 然后检查连接数现象是应用日志报连接池获取超时或者 Redis 侧连接数超过限制。执行redis-cli INFO clients redis-cli CLIENT LISTconnected_clients过高说明客户端连接没有被正确释放。常见原因是连接池配置过小或者代码里有频繁创建RedisTemplate的坏味道。Spring Boot 中使用 Lettuce 时连接池的max-active要匹配接口并发同时要避免把RedisTemplate放成方法局部变量。5.4 最后结合客户端代码定位如果 Redis 本身没有异常慢查询也没有再看客户端使用方式。常见问题包括缓存 key 没有设置过期时间冷数据堆积。对象序列化方式导致 value 过大。在事务中发送大量 Redis 命令阻塞时间很长。使用SCAN循环时没有正确处理游标导致重复扫描。问题现象检查命令常见原因处理建议内存持续增长INFO memory、--bigkeyskey 未设置过期大 value 堆积设置合理过期时间拆分大 key某命令偶发超时SLOWLOG GET 10大集合操作、KEYS 扫描改用 SCAN控制单次范围连接池满INFO clients、CLIENT LIST连接不释放、池参数过小检查连接池监控调大 max-active缓存不生效检查 key 分布和过期时间key 拼写前后不一致统一 key 前缀和序列化方式6. 不同环境下的 Redis 使用建议与部署清单6.1 学习环境Docker 或本机安装学习阶段的目标是快速跑通命令和代码不要在生产部署上浪费过多时间。如果本机有 Docker直接使用容器最简单docker run -d --name redis-dev -p 6379:6379 redis:7-alpineWindows 下也可以在 WSL2 或 Docker Desktop 中运行。如果需要在 Windows 本机直接安装要特别注意服务注册路径和配置文件路径启动命令常常因为配置文件路径错误而失败。redis-server.exe redis.windows.conf redis-cli.exe -h 127.0.0.1 -p 6379 ping可视化客户端可以用 Redis Insight 或 Another Redis Desktop Manager。它们适合查看 key 结构和执行命令但排查性能问题时命令行工具redis-cli仍然是第一选择。6.2 开发测试环境合理设置过期策略开发测试环境最容易出现两个问题一是所有 key 都不设置过期时间数据越积越多二是统一设置相同过期时间导致测试时缓存集体失效。建议在开发规范里明确每个缓存 key 必须有业务前缀、必须有 TTL、批量初始化时必须给过期时间加随机偏移。测试环境分配独立 Redis 实例或独立逻辑库避免和本地环境互相干扰。6.3 生产环境日志、监控、持久化的最低要求生产环境建议至少做到开启密码访问使用 ACL 按应用隔离权限。不把 Redis 端口暴露到公网只允许内网访问。根据数据重要性配置 RDB 或 AOF。配置maxmemory和淘汰策略防止缓存数据撑爆内存。开启慢日志设置合理阈值比如 50 毫秒。对内存、连接数、慢查询做监控告警。定期验证持久化文件能否正常恢复。有条件的场景配置主从加哨兵避免单点。部署前检查清单Redis 版本是否与客户端驱动兼容。密码和 ACL 权限是否配置。maxmemory是否合理淘汰策略是否符合业务语义。持久化策略是否与业务容忍度匹配。慢日志阈值是否设置。是否已有监控和告警。是否验证过从备份恢复数据的流程。是否在压测中观察到连接池和大 key 问题。6.4 面试时的回答思路与练习建议面试中如果被问到“系统没有高并发还需要 Redis 吗”不能只说“需要”或“不需要”。更合适的回答思路是不能以“是否高并发”作为唯一判断依据。Redis 解决的问题包括缓存加速、跨进程原子操作、分布式共享状态、轻量消息队列等。即使系统流量不高只要有多实例部署、有热点读、有订单号生成或防重复提交等需求引入 Redis 就有实际收益。高并发只是把 Redis 的价值放大到了更明显的程度。回答后如果能举一个自己实践过的例子比如用 Redis 实现过缓存、分布式锁、计数器会更有说服力。练习时可以做一个最小项目用 Redis 实现接口限流、商品缓存、排行榜、订单号生成。这个项目不需要模拟高并发只需要验证功能正确重点理解每个数据类型背后的适用场景。回到标题的问题没有高并发到底要不要用 Redis我的看法是不要用“并发量高不高”这个单一的维度来做决定而要看系统是否存在缓存热点、跨进程共享状态、原子计数、轻量队列这些需求。低并发系统完全可能因为一次慢查询、一个重复提交、一条订单号不唯一的故障意识到 Redis 的价值。真正的初学者不是不懂高并发而是把工具和高并发绑定起来思考。把这个认知改过来比记住多少 Redis 命令都重要。下一步可以往 Redis 的持久化恢复、主从切换、可观测性和 Redis 7 的新特性继续深入这些内容在生产环境会更受用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价