news 2026/8/30 1:50:13

没有高并发就不需要Redis?破除初学者误区,掌握核心应用场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
没有高并发就不需要Redis?破除初学者误区,掌握核心应用场景

刚开始做后端项目那几年,我一直抱着一个想法:Redis 是高并发系统的专属武器,日均请求量只有几万的小项目,直接查数据库就够了,引入 Redis 反而增加维护成本。直到后来在真实业务里被缓存穿透、接口重复提交、会话共享、排行榜实时统计这些问题反复折腾,我才意识到,这个想法错得很离谱。

Redis 真正的价值不是“解决高并发”,而是“让数据访问更合理、让业务逻辑更可靠”。高并发只是它发挥价值最显眼的一个场景,绝不是唯一场景。哪怕你的系统每天只有几百个请求,Redis 也可能因为分布式锁、幂等控制、热点数据缓存、轻量级消息队列等需求而成为必需品。

这篇文章我会从认知误区讲起,把 Redis 的核心数据类型、典型应用场景、实战落地案例、常见踩坑点一次讲清楚。不管你现在做的是企业管理系统、电商后台,还是小型创业项目,都会找到可以直接复用的思路。

1. 为什么“没有高并发就不用 Redis”是初学者的常见误区

1.1 这个误区是怎么来的

网上大量 Redis 教程的开篇都喜欢用“Redis 是高并发架构的核心组件”“秒杀系统、抢红包系统必备”来吸引眼球,看多了之后,很容易形成一种思维定式:高并发项目才需要 Redis,普通项目直接操作数据库就行。

这种说法不能说完全错误,但它把 Redis 的功能窄化了。Redis 确实在缓存、流量削峰、数据热点隔离方面表现突出,可它同时也是一套内存数据结构服务器,提供了丰富的数据类型、原子操作、过期策略、发布订阅、Lua 脚本能力。这些能力与“高并发”并没有必然的绑定关系。

还有一个原因在于,很多人接触 Redis 是从八股文开始的。面试题里总在问 Redis 为什么快、Redis 和 MySQL 怎么保证一致性、Redis 持久化怎么选,这些问题全部默认你已经处在高并发场景中。一旦业务真正落地,面对的是登录会话、数据字典、临时标记、异步任务这些琐碎需求,反而不知道 Redis 该往哪里放。

1.2 Redis 不是缓存中间件,而是数据结构服务器

要真正理解 Redis 的适用范围,需要先纠正一个概念。

我们平时说“Redis 做缓存”,是因为它最常见的用法是充当 key-value 缓存层。但 Redis 官方对自己的定位不是 cache,而是 data structure server,也就是数据结构服务器。

这意味着你面对的不是一个只能存字符串的 Map,而是一整套可以独立完成业务逻辑的数据结构:

  • String 可以完成计数、限流、分布式 ID。
  • Hash 可以完成对象属性的读写。
  • List 可以完成消息队列、时间轴、最新列表。
  • Set 可以完成去重、点赞关系、共同好友。
  • ZSet 可以完成排行榜、延迟队列、优先级任务。
  • Stream 可以完成消费者组模式的消息队列。
  • HyperLogLog 可以在极小内存下完成 UV 统计。
  • Geo 可以完成附近的人、门店距离计算。
  • Bitmap 可以完成用户签到、布隆过滤器的底层实现。

这些能力决定了 Redis 的核心价值在于“以极低延迟完成频繁读写的业务逻辑”,并不仅仅是把数据放内存里。很多没有高并发的系统,同样会遇到频繁读写的状态数据,比如在线用户标记、任务执行进度、验证码存储、接口防重标记。这类数据如果用数据库实现,不仅 SQL 写起来麻烦,而且会给数据库带来大量无价值的读写压力。

1.3 低并发项目的痛点同样需要 Redis 解决

举一个最简单的例子。

一个内部管理系统,每天只有几百个用户访问,量级确实不大。但这个系统大概率会遇到:

  • 多个实例部署时,同一个订单被不同接口重复提交。
  • 用户上传文件后,前端轮询处理状态,后端需要一个临时状态存储。
  • 手机验证码需要 5 分钟有效且只能验证一次。
  • 站点公告需要全量拉取,但每秒被访问几十次。
  • 搜索关键词需要最近 100 条记录。

这些需求如果全部落到 MySQL,要么建一堆临时表,要么用内存 Map 自己管理过期时间。临时表的问题是要处理过期数据的清理,内存 Map 的问题是集群多实例之间无法共享。Redis 的出现恰好用一套标准且简单的 API 解决了这些问题。

所以,判断“要不要用 Redis”,不应该看并发量,而应该看三点:数据是否被高频读取、是否需要跨实例共享、是否需要快速完成逻辑判断。只要命中其中一点,Redis 就有价值。

2. 环境准备与 Redis 安装

在开始写代码之前,先把 Redis 环境准备好。这一部分会根据不同操作系统给出安装方案,建议选择自己最熟悉的一种,不要每个环境都折腾一遍。

2.1 Windows 环境安装 Redis

Redis 官方并不原生支持 Windows,但微软和第三方社区维护了 Windows 移植版本,适合本地开发调试。

比较常见的做法是直接下载 Redis 官方 Windows 版本压缩包(一般是 Redis-x64-xxx.zip),解压后进入目录执行:

redis-server.exe redis.windows.conf

将 Redis 作为服务安装可以执行:

redis-server.exe --service-install redis.windows.conf --loglevel verbose redis-server.exe --service-start

连接测试使用命令行客户端:

redis-cli.exe -h 127.0.0.1 -p 6379 ping

Windows 移植版本一般功能完整,适合学习,但生产环境不建议使用 Windows 部署 Redis。Linux 才是 Redis 官方推荐的生产环境。

2.2 Linux 环境安装 Redis

以 CentOS / Ubuntu 为例,推荐使用源码编译安装或 Docker 安装。

源码安装步骤:

wget https://download.redis.io/releases/redis-7.0.12.tar.gz tar xzf redis-7.0.12.tar.gz cd redis-7.0.12 make make install PREFIX=/usr/local/redis

编译完成后,Redis 服务端程序会被安装到/usr/local/redis/bin目录下,可以通过 redis-server 启动。

启动方式支持前端启动和后台启动两种。开发环境图方便,可以直接用默认配置:

redis-server

生产环境建议使用配置文件后台运行:

redis-server /usr/local/redis/conf/redis.conf --daemonize yes

验证是否启动成功:

redis-cli -h 127.0.0.1 -p 6379 ping

如果返回 PONG,说明服务正常。

这里需要提醒一句,实际安装时版本号请以官网最新稳定版为准,不要照抄文章里的具体版本号。不同版本的配置项会有差异,比如 Redis 6 开始默认启用 ACL 用户体系和 Redis 7 对部分命令权限做了调整。

2.3 Docker 安装 Redis 与主从搭建

如果你本机装了 Docker,安装 Redis 的成本会低很多。

单机启动:

docker run -d \ --name redis \ -p 6379:6379 \ -v /data/redis/data:/data \ -v /data/redis/conf/redis.conf:/etc/redis/redis.conf \ redis:7.0 \ redis-server /etc/redis/redis.conf

本地快速体验也可以不挂载配置:

docker run -d --name redis -p 6379:6379 redis:7.0

如果你想同时体验主从复制,可以用 docker-compose 一次性拉起三个容器,三个容器分别对应一主两从:

version: '3' services: redis-master: image: redis:7.0 container_name: redis-master ports: - "6379:6379" command: redis-server --appendonly yes redis-slave1: image: redis:7.0 container_name: redis-slave1 ports: - "6380:6379" command: redis-server --slaveof redis-master 6379 redis-slave2: image: redis:7.0 container_name: redis-slave2 ports: - "6381:6379" command: redis-server --slaveof redis-master 6379

启动:

docker-compose up -d

这是一个最基础的主从架构,从节点会实时同步主节点的数据。主从架构的价值在于读写分离和数据冗余,但注意它不具备自动故障转移能力,生产环境需要引入哨兵或直接使用 Redis Cluster。

2.4 可视化客户端推荐

命令行操作 Redis 虽然高效,但查看 key、分析大 key、查看内存占用时,使用可视化工具有时会方便很多。

目前比较常用的客户端有:

  • Redis Desktop Manager:老牌客户端,界面直观。
  • Another Redis Desktop Manager:国产开源客户端,跨平台,功能比 RDM 更活跃。
  • Redis Insight:Redis 官方推出的可视化工具,支持数据浏览、命令行、内存分析、慢日志分析,功能比较完整。

工具的选择属于个人习惯,不影响核心逻辑。建议至少熟悉 redis-cli 的命令行操作方式,因为生产环境排查问题时,最容易接触到的还是命令行。

3. Redis 核心数据类型,先建立正确的存储思维

Redis 学习的地基不是缓存,而是数据类型。不同数据类型对应不同的底层实现和适用场景,选对类型,往往比调优更有效。下面按开发中最常遇到的场景来介绍。

3.1 String:计数器、缓存、限流

String 是最基础的类型,本质上是一个二进制安全字符串,最大 512MB。

常用命令:

SET user:1:name "张三" GET user:1:name INCR page:view:20250101 EXPIRE user:1:name 3600 SETNX lock:order:1001 1

INCR 和 DECR 是原子操作,即使在多线程或分布式环境下,也能保证计数不出现错乱。基于这个特性,String 经常用于实现:

  • 接口访问次数统计。
  • 分布式 ID 的初始值生成。
  • 短信发送频率限制。
  • 秒杀库存的预扣减。

SETNX 是 SET if Not Exists 的缩写,只有在 key 不存在时才能设置成功,这是实现分布式锁和接口幂等的重要基础。

3.2 Hash:对象属性的最佳存储

Hash 更适合存储一个“对象”。

如果使用 String 存储用户对象,通常只能整体序列化成一个 JSON 字符串,修改其中一个字段时,需要先把整个对象取出来,反序列化,修改字段,再整体写回。而 Hash 可以直接针对单个字段进行操作。

常用命令:

HSET user:1001 name "张三" age 25 city "杭州" HGET user:1001 name HGETALL user:1001 HINCRBY user:1001 age 1

这套结构特别适合存储用户资料、商品信息、订单快照等字段相对固定的数据。

3.3 List:消息队列与最新列表

List 是一个双向链表,支持从头部或尾部插入弹出元素。

常用命令:

LPUSH task:queue "task-1" RPUSH task:queue "task-2" LPOP task:queue BRPOP task:queue 0

BRPOP 是阻塞弹出命令,如果列表为空,会一直等待直到有新元素到达。这个特性让小规模项目可以在不引入 Kafka 等重量级消息中间件的情况下,实现一个简单的任务队列。

List 还适合做最新列表,比如用户操作日志,每次产生新日志就往左边插入,再配合 LTRIM 只保留最近 100 条:

LPUSH user:1001:logs "login" LTRIM user:1001:logs 0 99

3.4 Set:去重与关系运算

Set 是字符串的无序集合,自动去重,并且支持集合运算。

常用命令:

SADD article:100:likes "u1" "u2" "u3" SCARD article:100:likes SISMEMBER article:100:likes "u1" SINTER user:1:follows user:2:follows

Set 特别适合处理“关注关系”“好友关系”“标签关系”。例如两个用户共同关注了哪些人,一行 SINTER 就能得到结果,数据库可能要写很长的联表查询。

3.5 ZSet:排行榜与延迟队列

ZSet 是带权重的有序集合,每个元素都有一个 score,Redis 按照 score 对元素排序。

常用命令:

ZADD leaderboard 100 "user:1" ZADD leaderboard 98 "user:2" ZINCRBY leaderboard 1 "user:1" ZREVRANGE leaderboard 0 9 WITHSCORES

排行榜是最典型的场景。游戏积分排行、文章热度排行、商品销量排行,都可以用 ZSet 实现。

延迟队列是基于 score 设计的一种技巧,score 存任务执行时间戳,定时任务通过 ZRANGEBYSCORE 取出到期任务执行。

3.6 进阶类型

除了五种基础类型,Redis 还提供了几个有特定用途的类型:

  • HyperLogLog:固定 12KB 内存完成大量数据的去重统计。例如统计一个页面的 UV,几千万条访问记录占用内存也很小。
  • Bitmap:位操作。适合用户签到、在线状态、布隆过滤器实现,底层基于 String 类型。
  • Geo:地理坐标存储。可以计算两点距离、查找附近的人。
  • Stream:Redis 5.0 引入的消息队列类型,支持消费者组、消息确认、消息追溯,比 List 方案更可靠。

3.7 数据类型选择对照表

业务场景推荐数据类型核心命令
缓存单个对象String / HashSET、GET、HMSET、HGETALL
计数器StringINCR、DECR
接口幂等StringSETNX
消息队列List / StreamLPUSH、BRPOP、XADD、XREADGROUP
去重 / 关系运算SetSADD、SINTER、SUNION
排行榜 / 延迟队列ZSetZADD、ZINCRBY、ZREVRANGE
UV 统计HyperLogLogPFADD、PFCOUNT
用户签到BitmapSETBIT、BITCOUNT
附近的人GeoGEOADD、GEOSEARCH

4. Redis 不止缓存:能落地的业务场景逐个击破

这一节我会结合真实业务中常见的痛点,展示 Redis 在“非高并发”需求下的应用思路。每个场景都有对应的代码示例和思考过程。

4.1 场景一:接口幂等,防止重复提交

业务里经常出现这种情况:用户点击“提交订单”按钮后,因为网络波动、前端异常、用户连点,同一个请求被提交了多次。如果不做幂等处理,数据库里会出现多条重复订单。

传统做法是查数据库判断是否已经存在相同订单。但查数据库本身存在并发窗口,两个请求同时查询,发现都不存在,然后同时插入,重复数据依旧会出现。

Redis 的 SETNX 可以很好地解决这个问题。

流程如下:

1. 前端提交请求时,带上业务唯一标识 requestId。 2. 后端使用 SETNX 将 requestId 写入 Redis,并设置过期时间。 3. 如果 SETNX 返回成功,说明是第一次处理该请求,继续业务逻辑。 4. 如果 SETNX 返回失败,说明请求已经处理过,直接返回重复提交提示。

核心代码:

// 文件:OrderServiceImpl.java public String createOrder(OrderCreateDTO dto) { // 幂等键:业务维度 + 唯一流水号 String idempotentKey = "idempotent:order:" + dto.getOrderNo(); // setIfAbsent 对应 SETNX,同时设置过期时间 Boolean first = redisTemplate.opsForValue() .setIfAbsent(idempotentKey, "1", Duration.ofMinutes(5)); // 如果 key 已存在,说明请求重复 if (Boolean.FALSE.equals(first)) { throw new BusinessException("订单已提交,请勿重复操作"); } try { // 真正的订单创建逻辑 return doCreateOrder(dto); } catch (Exception e) { // 业务失败时删除幂等标记,允许用户重试 redisTemplate.delete(idempotentKey); throw e; } }

这个方案的巧妙之处在于把并发判断变成了一次 O(1) 的 Redis 操作。过期时间起到了“兜底”作用,即使业务执行失败且没有显式删除标记,key 也会在 5 分钟后自动消失。

4.2 场景二:分布式锁,解决多实例下资源互斥

当系统以多实例部署时,JVM 级别的 synchronized 或 Lock 都失去了作用。因为不同实例是不同的 JVM,锁只能锁住自己进程内的线程,锁不住其他实例。

分布式锁的经典实现方式就是用 Redis 的 SETNX + 过期时间。

// 文件:RedisLockUtil.java public boolean tryLock(String lockKey, String requestId, long expireSeconds) { return redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(expireSeconds)); } public boolean releaseLock(String lockKey, String requestId) { // 释放锁时要校验是否是自己的锁,防止误删他人锁 String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) " + "else return 0 end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), requestId ); return result != null && result > 0; }

使用示例:

String lockKey = "lock:stock:1001"; String requestId = UUID.randomUUID().toString(); boolean locked = redisLockUtil.tryLock(lockKey, requestId, 10); if (!locked) { throw new BusinessException("系统繁忙,请稍后重试"); } try { // 扣减库存、发放优惠券等临界区业务 } finally { redisLockUtil.releaseLock(lockKey, requestId); }

这里有几个值得注意的点。requestId 用来标识锁的持有者,释放锁时先判断 value 是否一致,避免因为锁过期导致误删。锁不能设置永久有效,必须给过期时间,防止持有锁的实例宕机后产生死锁。过期时间又不能太短,否则业务还没执行完,锁已经释放,其他线程就能同时进入临界区。生产环境常用 Redisson 封装好的分布式锁,它内置了看门狗机制,可以自动续期。

4.3 场景三:轻量级消息队列,实现任务异步化

一个低并发系统,可能只是希望把耗时操作异步化。比如用户导出 Excel 报表,数据量几十万时,同步执行可能需要几十秒,用户只能一直等待。更合理的方案是先把任务丢进队列,立即返回“导出中”的提示,后台线程再慢慢处理。

Redis 的 List 队列可以实现这个功能。

生产者:

LPUSH export:task "{userId: 1001, type: 'order', date: '2024-01-01'}"

消费者:

BRPOP export:task 0

Java 侧可以使用 RedisTemplate 完成消息的写入和消费。对于小规模项目,这个方案完全够用。如果业务量继续增长,再考虑 RocketMQ、RabbitMQ、Kafka。

不过用 List 做消息队列有天然缺陷:消息没有确认机制,消费者拉取消息后如果处理异常,消息会丢失。Redis Stream 解决了这个问题,提供了消费者组、消息确认、Pending 列表。如果你是在新项目里落地消息队列,建议优先使用 Stream。

XADD order:stream * orderId 1001 status created XREADGROUP GROUP exporter consumer-1 COUNT 10 BLOCK 0 STREAMS order:stream >

4.4 场景四:缓存穿透、击穿、雪崩的处理

不管并发量高不高,只要用 Redis 做缓存,就必须考虑缓存失效带来的三类问题。很多初学者以为只有大促场景才需要担心,实际上小系统也会因为一个热点数据失效而被数据库压垮。

缓存穿透指查询一个一定不存在的数据。请求到达 Redis,发现没有缓存,于是查数据库,数据库也没有,不写回缓存。如果这个 key 被恶意构造,大量请求绕过 Redis,直接打到数据库。

解决思路是空值缓存和布隆过滤器。

public Object getData(String key) { Object value = redisTemplate.opsForValue().get(key); if (value != null) { return value; } // 数据库没有数据时,写一个短时空值 Object dbValue = queryFromDb(key); if (dbValue == null) { redisTemplate.opsForValue().set(key, "", Duration.ofMinutes(3)); return null; } redisTemplate.opsForValue().set(key, dbValue, Duration.ofMinutes(30)); return dbValue; }

缓存击穿指一个热点 key 在失效瞬间,大量请求同时查询数据库。解决思路是使用互斥锁或逻辑过期。

缓存雪崩指大量 key 在同一时间段集中失效,导致数据库在短时间收到大量请求。解决思路是随机化过期时间。

// 随机化过期时间,避免大量 key 同时过期 long timeout = baseTimeout + ThreadLocalRandom.current().nextLong(300); redisTemplate.opsForValue().set(key, dbValue, Duration.ofSeconds(timeout));

这三个问题是 Redis 缓存治理的核心内容,建议每个使用 Redis 的开发者都理解背后的原因和解决思路。

4.5 场景五:分页查询慢怎么用 Redis 优化

分页查询慢是关系型数据库的典型痛点。深分页场景下,LIMIT 100000, 20 需要扫描 100020 行数据再丢弃前 100000 行,性能会随页码增大而快速恶化。

优化手段有很多,比如使用覆盖索引、延迟关联、游标分页。Redis 的 ZSet 也可以用在某些特定场景中。

例如文章列表按发布时间倒序排列,可以维护一个 ZSet,score 存发布时间,value 存文章 ID:

ZADD article:list 170000000001 article:1001 ZADD article:list 170000000002 article:1002

分页查询时按 score 倒序取出 ID 列表:

ZREVRANGE article:list 100 119

然后根据 ID 批量查询文章详情,这样即使翻到很深的页,也只是从 ZSet 中取区间数据,再由数据库回表。需要注意的是,这个方案适合列表顺序与 score 语义一致的场景,全表记录数量极大且频繁插入时,ZSet 的内存占用和排序成本也不容忽视。

5. 完整项目实战:用 Spring Boot 集成 Redis 实现缓存、幂等、排行榜

我们把前面提到的场景整合成一个完整项目。项目目标是实现一个简易文章系统,包含:

  • 根据文章 ID 查询文章详情,使用 Redis 缓存。
  • 发布文章时带幂等控制。
  • 文章被点赞后更新热门文章排行榜。

5.1 项目结构

redis-tutorial/ ├── pom.xml └── src/main/ ├── java/com/example/redistutorial/ │ ├── RedisTutorialApplication.java │ ├── controller/ArticleController.java │ ├── service/ArticleService.java │ ├── vo/ArticleVO.java │ └── config/RedisConfig.java └── resources/ └── application.yml

5.2 添加依赖与配置

pom.xml 核心依赖:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>

Spring Boot 2.x 默认使用的是 Lettuce 客户端连接池,需要引入 commons-pool2 才能配置连接池。版本号建议根据自己项目使用的 Spring Boot 版本调整,不要照搬。

application.yml 配置:

spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0

开发环境 password 留空即可,生产环境必须设置强密码并建议启用 ACL。

5.3 编写核心代码

创建一个缓存配置类,为 RedisTemplate 指定序列化方式。不配置序列化时,默认使用 JdkSerializationRedisSerializer,会带来可读性差、占用空间大、跨语言困难的问题。

// 文件:config/RedisConfig.java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key 使用 String 序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // value 使用 Jackson 序列化,便于阅读 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }

再写 ArticleVO 和 ArticleService。

// 文件:vo/ArticleVO.java public class ArticleVO { private Long id; private String title; private String author; private Integer likeCount; // 省略 getter/setter }
// 文件:service/ArticleService.java @Service public class ArticleService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String ARTICLE_CACHE_KEY = "article:detail:"; private static final String ARTICLE_PUBLISH_IDEMPOTENT_KEY = "idempotent:article:publish:"; private static final String ARTICLE_HOT_RANK_KEY = "rank:article:hot"; /** * 查询文章详情,优先读缓存 */ public ArticleVO getArticleById(Long id) { String key = ARTICLE_CACHE_KEY + id; Object cacheValue = redisTemplate.opsForValue().get(key); if (cacheValue != null) { return (ArticleVO) cacheValue; } ArticleVO dbResult = queryFromDb(id); if (dbResult == null) { return null; } // 设置 30 分钟过期,并添加随机偏移 long expireTime = 30 * 60 + ThreadLocalRandom.current().nextLong(60); redisTemplate.opsForValue().set(key, dbResult, Duration.ofSeconds(expireTime)); return dbResult; } /** * 发布文章,附带幂等控制 */ public Long publishArticle(String requestId, ArticleVO article) { String idempotentKey = ARTICLE_PUBLISH_IDEMPOTENT_KEY + requestId; Boolean first = redisTemplate.opsForValue() .setIfAbsent(idempotentKey, "1", Duration.ofMinutes(5)); if (!Boolean.TRUE.equals(first)) { throw new RuntimeException("重复提交"); } try { Long articleId = insertDb(article); redisTemplate.opsForValue().set(ARTICLE_CACHE_KEY + articleId, article, Duration.ofSeconds(30 * 60)); return articleId; } catch (Exception e) { redisTemplate.delete(idempotentKey); throw e; } } /** * 文章点赞,并更新热门排行榜 */ public void likeArticle(Long articleId) { String cacheKey = ARTICLE_CACHE_KEY + articleId; ArticleVO article = getArticleById(articleId); if (article == null) { throw new RuntimeException("文章不存在"); } article.setLikeCount(article.getLikeCount() + 1); redisTemplate.opsForValue().set(cacheKey, article, Duration.ofSeconds(30 * 60)); // 排行榜 score 表示点赞总数 redisTemplate.opsForZSet().incrementScore(ARTICLE_HOT_RANK_KEY, String.valueOf(articleId), 1); // 异步或同步更新数据库 updateDbLikeCount(articleId); } }

Controller 层:

// 文件:controller/ArticleController.java @RestController @RequestMapping("/article") public class ArticleController { @Autowired private ArticleService articleService; @GetMapping("/{id}") public ArticleVO getById(@PathVariable Long id) { return articleService.getArticleById(id); } @PostMapping("/publish") public Long publish(@RequestHeader("requestId") String requestId, @RequestBody ArticleVO article) { return articleService.publishArticle(requestId, article); } @PostMapping("/{id}/like") public void like(@PathVariable Long id) { articleService.likeArticle(id); } }

5.4 运行与验证

启动 Redis 服务后,启动 Spring Boot 项目,通过 curl 验证接口:

curl http://localhost:8080/article/1001

第一次访问时,缓存未命中,服务会查数据库并回填缓存。第二次访问时,直接返回缓存数据,响应速度会明显提升。

此时在 redis-cli 中执行:

keys article:*

可以看到缓存 key 已经生成。再执行:

ZREVRANGE rank:article:hot 0 9 WITHSCORES

可以看到热门文章排行榜数据。

通过这个项目,你可以完整理解 Redis 在一个普通 Spring Boot 系统中的实际位置。它不需要高并发,但每一步都在优化系统性能和用户体验。

6. 生产环境必踩的坑:Redis 实践避坑指南

6.1 keys 命令导致 Redis 阻塞

Redis 是单线程处理命令的,keys 命令会遍历所有 key,在 key 数量大的情况下会阻塞 Redis 服务,导致所有请求排队等待。这个坑在一些新手项目中很容易踩到。

比如你想删除所有以ekyc_pic_开头的缓存 key,随手执行:

keys ekyc_pic_*

如果匹配到的 key 只有几十个,可能感觉不到问题。一旦 key 数量达到百万级别,这个命令会让 Redis 卡顿数秒甚至更久。

生产环境应该使用 scan 命令分批遍历:

scan 0 MATCH ekyc_pic_* COUNT 100

scan 命令是游标式遍历,不会阻塞整个 Redis。

6.2 持久化策略不能盲目关闭

Redis 默认开启了 RDB 持久化,但很多初学者为了追求性能,直接把持久化关闭。这种做法在缓存场景下勉强可以接受,但如果 Redis 里存了分布式锁、幂等标记、任务队列等有状态数据,关掉持久化意味着 Redis 重启后数据全部丢失,可能引发业务异常。

常见的持久化方案:

  • RDB:快照式持久化,文件紧凑,恢复快,但可能丢失最后一次快照之后的数据。
  • AOF:日志式持久化,数据可靠性高,但文件体积大,恢复速度相对慢。
  • 混合持久化:Redis 4.0 开始支持,兼顾 RDB 和 AOF 的优点。

生产环境建议开启 AOF,并根据业务容忍度选择 everysec 或 always 写入策略。

6.3 Big Key 和 Hot Key

Big Key 指 value 特别大的 key,比如一个 String 存了 10MB 数据,或一个 Set 里有百万个元素。操作这种 key 时,Redis 需要花费较长时间处理,容易阻塞其他命令。

Hot Key 指访问频率特别高的 key,比如某个明星的粉丝量统计 key。瞬间大量请求访问同一个 key,会让单个 Redis 节点成为瓶颈。

应对思路:

  • 大 key 拆分为多个小 key,比如 Hash 按字段拆分,或 value 压缩后存储。
  • 热 key 做本地缓存,将一部分读请求拦截在应用内存中。
  • 热 key 做副本复制,多个从节点分担读压力。

6.4 安全边界:端口暴露和弱密码

Redis 默认端口 6379 如果没有设置密码,并且暴露到了公网,很容易被恶意攻击。攻击者可以通过 Redis 写入定时任务、公钥文件等方式获取服务器权限。这在安全圈已经有很多真实案例,必须重视。

安全基线建议:

  • 设置强密码或使用 Redis ACL 按用户分配权限。
  • 禁止将 Redis 端口直接暴露到公网,使用内网访问或通过安全组限制来源 IP。
  • 不使用默认端口或至少修改绑定地址 127.0.0.1。
  • 生产环境禁用危险命令,如 FLUSHALL、FLUSHDB、KEYS。
  • 部署前先做漏洞扫描,配合防火墙策略。

6.5 集群不是银弹

Redis 主从复制能解决单点故障,但主从切换需要哨兵或手动干预。Redis Cluster 能自动分片,但引入了多 key 操作限制、重定向机制、客户端适配等复杂度。

如果业务量不大,优先考虑单机加持久化,再通过主从复制保证高可用。不要一开始就贪图集群架构,过度设计同样会带来运维负担。

7. 常见问题排查与解决方案

问题现象常见原因解决思路
启动报“Can't open the log file”日志目录不存在或无权限创建日志目录,检查运行用户权限
连接超时防火墙未放行端口、bind 配置限制、连接数耗尽检查安全组、bind 配置、maxclients 设置
Redis 连接数突然飙升客户端连接池配置过大或未释放调整连接池 max-active 参数,排查代码泄漏
缓存中的数据与数据库不一致更新数据库后未删除缓存使用 Cache Aside Pattern 标准策略
缓存雪崩大量 key 设置了相同过期时间过期时间添加随机偏移
缓存穿透查询不存在的数据无缓存兜底空值缓存、布隆过滤器
缓存击穿热点 key 过期瞬间并发请求互斥锁、逻辑过期
数据丢失持久化策略关闭或配置不当开启 AOF,配置合理的刷盘策略
主从数据不一致网络分区、从节点同步延迟监控同步延迟,必要时强制重新同步
内存持续增长设置了过长的过期时间或没有过期时间梳理 key 生命周期,设置合理 TTL,定期扫描清理

排查 Redis 问题时,首先通过 info 查看整体运行状态,再通过 slowlog get 获取慢查询日志,最后用 monitor 命令监控实时命令流(生产环境慎用,会拖慢性能)。

8. 最佳实践与工程建议

8.1 缓存设计规范

推荐使用 Cache Aside Pattern,也就是旁路缓存模式:

  • 读:先读缓存,缓存未命中再读数据库,回填缓存并设置过期时间。
  • 写:先更新数据库,再删除缓存。

为什么不选择“先删缓存,再更新数据库”?因为删缓存和更新数据库之间存在时间窗口,另一个线程可能读取旧数据并回填缓存,导致缓存长期为脏数据。

为什么不选择“先更新缓存,再更新数据库”?因为一旦数据库更新失败,缓存里的新数据和数据库不一致。

Cache Aside 模式是社区验证过的最稳健方案,缺点是存在短暂不一致,但对大多数业务可接受。如果对一致性要求极高,可以引入消息队列或 Canal 监听 binlog 来异步删除缓存。

8.2 Key 设计规范

合理的 key 设计应当遵循三个原则:

  • 可分类:业务模块前缀,如order:status:1001
  • 可辨识:看到 key 能知道存储内容。
  • 有边界:区分不同的业务线、环境、租户。

推荐格式:

业务模块:实体:ID:字段

例如:

user:info:1001 order:detail:20240101001 idempotent:pay:requestId

同时要注意 key 不要太长,避免浪费内存;不要使用中文,避免编码问题;不要在 key 中拼接不确定的输入,防止 key 设计注入风险。

8.3 线上操作流程建议

修改 Redis 配置、主从切换、清理大 key、上线新缓存逻辑,这些操作都有风险,建议遵循以下流程:

  • 先在测试环境验证完整流程。
  • 操作前备份 Redis 数据和配置文件。
  • 涉及删除数据时,先确认 key 的归属业务和数据价值。
  • 变更后观察监控指标,包括命中率、内存、连接数、慢日志。
  • 保留回滚方案,必要时快速回切。

8.4 哪些项目真正不需要 Redis

任何系统都有成本。额外引入 Redis 意味着多一个中间件需要维护,需要处理缓存一致性,需要关注内存和集群状态。

如果项目满足以下条件,确实可以先不引入 Redis:

  • 请求量极低,数据库压力可以忽略。
  • 单机部署,不存在多实例共享状态问题。
  • 没有分布式锁、幂等、消息队列等需求。
  • 团队对 Redis 运维不熟悉,且没有精力承担额外运维成本。

但即便项目当前不引入 Redis,也建议掌握核心数据类型和常见场景的应用方式。因为业务增长是动态的,第一次引入 Redis 的决策,往往就发生在业务出现痛点的转折点。

9. 从“要不要用”到“怎么用好”:把 Redis 的价值发挥出来

回到文章标题:认为系统没有高并发就不用 Redis,为什么说这是初学者思维?因为初学者往往通过“并发量”这个单一指标来判断技术选型,而成熟开发者会从数据访问特征、业务逻辑复杂度、系统扩展性多个维度来评估。

Redis 是高并发场景的利器,但它同时也是普通业务系统里解决状态管理、幂等、去重、统计、队列等问题的通用工具。学习 Redis,不应该只背几条命令,而是要理解在什么场景选择什么数据类型,为什么这样做,以及生产环境会遇到哪些风险。

如果你目前还停留在“会 SET 和 GET”的阶段,建议下一步深入学习这几个方向:

  • Redis 持久化原理与数据恢复演练。
  • 分布式锁的底层实现与 Redisson 源码分析。
  • Redis Cluster 分片原理与集群运维。
  • 缓存一致性问题的系统性治理思路。
  • 通过实战项目把所有知识点串联起来。

学完这些,你不仅能在面试中讲清楚 Redis 高并发场景的答案,更重要的是,你在日常项目里能真正判断出什么时候该用 Redis,以及怎么用得更好。等到入手项目足够多,你会慢慢发现,技术选型不变量化的那一刻,往往是业务真正读懂的那一刻。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 1:49:58

PINN与GNN联合建模:从PDE约束到复杂几何域的科学计算实践

PINN与GNN这两个词&#xff0c;最近在物理计算和科学机器学习方向频繁出现在一起。单独看&#xff0c;PINN&#xff08;物理信息神经网络&#xff09;负责把偏微分方程作为软约束塞进神经网络训练过程&#xff1b;GNN&#xff08;图神经网络&#xff09;则擅长处理非欧几里得结…

作者头像 李华
网站建设 2026/8/30 1:48:12

时间步条件Transformer:全球天气预报的关键设计与实操指南

Timestep-Conditioned Transformers for Global Weather Forecasting 这个方向&#xff0c;核心是把时间步信息作为条件输入到 Transformer 里&#xff0c;用全局注意力做气象场演变预测。它和常见的图像生成、文本翻译类 Transformer 不一样&#xff0c;也不只是把气象图当成普…

作者头像 李华
网站建设 2026/8/30 1:48:09

技术教程写作的明确性缺失:以“狂三”为例的选题分析

当前标题“【狂三】No Idea”暂无法直接生成一篇合规的 CSDN 技术教程。该标题看起来更像动漫角色名或作品名&#xff0c;没有提供技术栈、项目背景、目标场景等有效信息&#xff0c;也没有可参考的关键词、摘要或网络材料。如果一定要写&#xff0c;只能凭空编造项目内容、代码…

作者头像 李华
网站建设 2026/8/30 1:46:23

AI编程产能提升背后:工程治理与代码审查的平衡实践

近期关于 Grindr CEO 讨论 AI 编程产能的报道&#xff0c;把“AI 是否替代程序员”这个话题又推到了台前。报道援引的说法是&#xff0c;AI 工具已经承担了相当于 200 名工程师的工作量。无论这个数字在统计口径上是否站得住脚&#xff0c;它都指向一个工程团队必须面对的真实问…

作者头像 李华
网站建设 2026/8/30 1:42:07

智能体AI认知能力缺口:五类故障定位与工程干预指南

一次印象很深的调试经历。我让一个基于大模型写的智能体去批量整理项目文档&#xff0c;任务拆得不复杂&#xff1a;读取 PDF、提取关键信息、按模板输出。刚开始一切正常&#xff0c;跑到第 23 个文件时&#xff0c;它突然停住&#xff0c;反复调用同一个接口&#xff0c;每次…

作者头像 李华
网站建设 2026/8/30 1:41:11

AI长任务总丢上下文?prime-agent如何用上下文管理稳住关键状态

长任务跑着跑着就丢了上下文&#xff0c;这是 AI 编程 agent、批量自动化工具在实际使用里最常遇到的坑。无论模型推理能力多强&#xff0c;只要一条任务超过十几步&#xff0c;前面几步的关键决策、输入文件路径、已经确认过的约束条件&#xff0c;到了后面就被模型“忘”掉了…

作者头像 李华