Redis不止高并发:低并发场景下的核心价值与工程实践
2026/8/29 4:51:57 网站建设 项目流程

1. 先纠正一个偏见:Redis 不是高并发的“专属玩具”

很多开发者第一次接触 Redis,是在某个高并发项目里。那时候系统流量暴涨,数据库连接被打满,慢查询成片出现,于是团队引入 Redis 做缓存,扛住了一波波读请求。从那以后,Redis 在很多初学者心里就和“高并发”绑定了。

但还有一个更常见的场景被忽略了:很多系统当前并发并不高,日活几千,接口 QPS 不过几十,数据库也远没到瓶颈。这时候如果你提出“我们要不要引入 Redis”,很容易被反问一句:

“系统现在并发又不高,用 Redis 干嘛?不是徒增复杂度吗?”

这句话听起来很有道理,但实际上暴露了一个误解——Redis 的价值从来不只是“抗高并发”,它首先是一个能力极其丰富的基础组件。高并发只是它众多优势中的一种显性收益,而不是唯一适用场景。

本文不打算单纯争论“要不要用 Redis”,而是把它拆开来看:

  • Redis 到底是什么,核心优势有哪些。
  • 除了缓存,Redis 还能解决哪些常见的业务问题。
  • 低并发系统里,Redis 为什么依然值得引入。
  • 完整的代码示例和工程落地建议。

无论你是刚学 Redis 的初学者,还是在业务系统里评估技术选型的后端开发,这篇文章都能给你一个相对完整的参考视角。

2. 先搞懂 Redis 的核心价值,再谈要不要用

2.1 为什么 Redis 这么快

Redis 是一个基于内存的键值存储系统,通常被归类为 NoSQL 数据库,但它和 Memcached 这类纯缓存组件又不一样。它不止能存数据,还能对数据执行运算、设置过期策略、发布订阅消息、执行 Lua 脚本,甚至支持事务和持久化。

它快的原因可以拆成几点:

  • 基于内存:数据主要在内存中读写,内存的访问速度远高于磁盘。
  • 单线程模型(主要指命令执行):避免了多线程上下文切换和锁竞争,也简化了数据一致性问题的处理。
  • 高效的数据结构:跳表、压缩列表、快速链表等底层实现让不同操作都有不错的复杂度表现。
  • IO 多路复用:虽然命令执行是单线程的,但网络 IO 层面可以同时处理大量连接,配合 epoll 等机制实现高吞吐。

所以,Redis 能在单实例上支撑十几万的 QPS,这在高并发场景里非常可观。但你需要注意的是,“快”只是它的基础能力,不是全部。

2.2 初学者对 Redis 的固有印象

很多初学者对 Redis 的认知停留在“缓存数据库”,使用方式就是set一个 key,get一个 key,数据过期了自动淘汰。这种理解没有错,但远远不够。

我可以列几个最常见的使用场景,你看看自己的系统里是不是已经遇到了类似问题:

  • 首页热点数据每次请求都要查数据库,重复查询严重。
  • 用户登录状态存在服务器内存里,多台机器部署后无法共享 Session。
  • 多个服务同时处理订单,需要保证并发扣减库存不超卖。
  • 表单提交按钮用户快速点了两次,重复提交了订单。
  • 热点新闻一发布,阅读数瞬间增长,但数据库更新频繁。
  • 排行榜需要实时排序,每次全量查库统计太慢。
  • 接口需要限制某个用户在 1 分钟内最多调用 10 次。
  • 两个系统之间需要异步传递消息,又不想引入重量级 MQ。

这些问题在低并发系统里都存在。也就是说,即使你的系统没有高并发压力,依然有大量日常业务场景适合用 Redis 来优化

3. 非高并发系统里的 Redis:解决的不只是性能

3.1 缓存:降低数据库压力还是提升用户体验

缓存是 Redis 最常见的用法,但很多低并发系统的开发者容易走入一个误区:数据库现在不慢,为什么还要加缓存?

这里需要区分两个概念:数据库能扛得住用户体验足够好

举个例子。一个知识问答社区,用户浏览问题详情页时,页面上要展示问题内容、作者信息、回答数量、点赞数量、收藏数量。如果所有数据都实时从数据库查询,一次页面请求可能需要关联五六张表。当用户反复刷新页面时,数据库就会反复执行相同逻辑的 SQL。

即使数据库负载不高,这些重复查询也意味着:

  • 更长的响应时间。
  • 更多无效的数据库连接占用。
  • 无法为未来流量上涨预留空间。

引入 Redis 缓存后,热数据可以直接从内存返回,接口响应时间从几百毫秒降到个位数毫秒。用户感受到的“流畅”是真实存在的,不只是并发压力下的需求。

所以,缓存的本质不是“救火”,而是把高频读操作从慢速存储搬到快速存储。哪怕目前并发不高,只要某个数据存在明显的“读多写少”特征,就值得考虑缓存。

3.2 分布式锁:解决多实例下的并发安全

很多低并发系统早期是单机部署,用 JDK 自带的synchronized或者ReentrantLock就能实现并发控制。但随着业务扩展,系统总有一天会从单机变为多实例部署。一旦存在多个进程,JVM 级别的锁就失效了。

举一个典型的场景:用户领券。假设同一张优惠券只能领取一次,代码里先查是否已领取,再执行领取逻辑。

// 伪代码 if (couponService.hasReceived(userId, couponId)) { return "已领取"; } couponService.receive(userId, couponId);

单机部署时,加一个同步锁就能基本保证安全。但多实例部署后,两个请求可能同时落在两台机器上,它们同时查库发现都没有领取记录,然后同时插入领取记录,最终导致用户重复领取。

这时候 Redis 分布式锁就是一个通用方案。Redis 的SETNX命令天然适合实现锁的获取,配合过期时间可以避免死锁。

// 加锁伪代码 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { try { // 执行领取逻辑 } finally { // 释放锁时校验持有者 String value = redisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { redisTemplate.delete(lockKey); } } }

注意,这个实现里还有一个释放锁的判断问题,完整的 Redisson 实现会更好。但不管用哪种方式,Redis 在这里解决的是“多实例并发一致性”问题,而不是“高并发压力”问题。

3.3 计数器:点赞、阅读量、库存扣减

业务系统里经常需要计数,比如文章的阅读量、视频的点赞量、商品的库存量。

一种常见做法是直接更新数据库字段:

UPDATE article SET read_count = read_count + 1 WHERE id = ?

这种做法在低并发下没问题,但存在几个隐患:

  • 频繁更新数据库行,可能触发行锁竞争。
  • 每次都产生一个 update 语句,数据库写压力持续累积。
  • 如果高峰期过来,数据库可能成为性能瓶颈。

用 Redis 计数,可以先把数据在内存里累加,再定期同步到数据库。示例:

// 每来一次阅读,执行自增 Long count = redisTemplate.opsForValue() .increment("article:read_count:" + articleId); // 达到一定阈值后,批量同步到数据库 if (count % 100 == 0) { articleService.updateReadCount(articleId, count); }

库存扣减也类似。可以用 Redis 预减库存,然后再异步落库,避免超卖。

// 扣减库存 Long stock = redisTemplate.opsForValue() .decrement("product:stock:" + productId, 1); if (stock < 0) { // 恢复并提示库存不足 redisTemplate.opsForValue() .increment("product:stock:" + productId, 1); throw new BusinessException("库存不足"); }

需要注意的是,库存扣减这类场景对数据一致性要求很高,不能只依赖 Redis,还需要配合数据库事务、消息队列等手段做最终一致性设计。

3.4 限流:控制接口的访问频率

很多系统不是没有流量波峰,而是没有提前感知。比如秒杀活动、营销大促、热点事件,这些场景会在短时间涌入大量请求。此时如果没有限流,服务很容易被打挂。

Redis 的INCR命令配合过期时间,可以快速实现一个简单的计数器限流。

// 每秒钟最多允许 10 次请求 String key = "rate:limit:" + userId + ":" + currentSecond; Long current = redisTemplate.opsForValue().increment(key); if (current == 1) { redisTemplate.expire(key, Duration.ofSeconds(1)); } if (current > 10) { throw new BusinessException("请求过于频繁"); }

更复杂的滑动窗口限流可以用 ZSET 实现,而生产环境更推荐使用 Redisson 的RRateLimiter或者网关层的限流组件。但核心思路是一样的:利用 Redis 的原子操作完成计数与过期控制。

低并发系统用不上吗?不一定。比如用户注册接口、发送短信验证码接口、支付回调接口,这些接口天然需要做频率限制。你不是为了应对高并发,而是为了安全防护和资源控制。

3.5 Session 共享:多实例部署的基础设施

现在的应用部署很少是单机模式了。即使系统并发不高,你可能也会因为容灾、开发测试环境隔离等原因部署多台机器。

如果用户登录状态存在单机 Session 里,那么 Nginx 负载均衡到不同机器时,用户就会出现“登录状态丢失”的问题。

解决方案有两个主流方向:

  • 用 Redis 存储 Session,Spring Session 可以很轻松地集成。
  • 用 JWT 等无状态 Token,把用户信息放在客户端。

第一种方案更传统,但对现有系统改造较小。引入 Spring Session 后,Session 数据自动进入 Redis,多实例共享不再需要额外处理。

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>

所以,即使当前系统部署的机器不多,只要存在多实例的可能,Redis 就是一个值得提前布局的基础设施。

4. 从零开始:Redis 环境准备与基础操作

4.1 操作系统与版本说明

本文的操作示例以常见环境为例,代码思路适用于 Redis 6.x 及以上版本,也基本兼容 5.x。具体版本需要根据你的实际环境调整,这里不强行绑定某个版本号,重点演示安装、配置和基础命令的通用流程。

操作系统可以选择:

  • 开发环境:Windows 10/11 或 macOS。
  • 服务器环境:CentOS 7/8、Ubuntu 20.04/22.04 等 Linux 发行版。
  • Docker 环境:适用于快速启动 Redis 容器,适合本地开发和测试。

4.2 Windows 下安装 Redis

Windows 下官方并不直接提供 Redis 安装包,常见的做法是使用 Memurai 或者微软老版本的 Redis 移植版。但更推荐的方式是使用 Docker Desktop 运行 Redis 容器。

如果你只需要本地快速测试,可以在 Windows 上直接下载 Redis 的 Windows 移植版压缩包。解压后目录里包含redis-server.exeredis-cli.exe

启动服务:

redis-server.exe

启动客户端:

redis-cli.exe

客户端里输入ping,如果返回PONG,说明服务正常运行。

这种方式适合学习,但生产环境务必使用 Linux 或容器化部署。

4.3 Linux 下通过 Docker 安装 Redis

Linux 服务器上最方便的方式是使用 Docker。

拉取镜像:

docker pull redis

运行容器:

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

进入容器并连接客户端:

docker exec -it redis-server redis-cli

4.4 Redis 客户端连接工具

日常开发中,命令行客户端redis-cli适合快速验证,但可视化工具更适合排查线上问题。常见的 Redis 客户端可视化工具包括:

  • Redis Desktop Manager(RDM):老牌工具,但新版本已不再免费。
  • Another Redis Desktop Manager:开源免费,目前社区活跃度较高,功能也比较全面。
  • Redis Insight:Redis 官方出品的可视化工具,界面现代,支持数据浏览、命令行、内存分析等功能。

这些工具本质上都是通过 Redis 的通信协议连接到服务端,和语言无关。只要你配置了正确的 IP、端口、密码,就能用它们查看 key 的分布、过期时间、内存占用等信息。

下面通过一张表格总结不同工具的适用场景:

工具类型适用场景是否免费
redis-cli命令行快速验证、脚本操作
Another Redis Desktop Manager桌面可视化日常开发排错、数据浏览开源免费
Redis Insight桌面可视化官方推荐,内存分析、性能监控免费
Redis Desktop Manager桌面可视化老用户习惯使用部分版本收费

4.5 常用基础命令

连接服务后,先掌握最常用的命令。

# 设置键值 SET user:name "张三" # 获取键值 GET user:name # 设置键值并指定过期时间,单位是秒 SET user:token "abc123" EX 3600 # 查看剩余过期时间 TTL user:token # 判断键是否存在 EXISTS user:name # 删除键 DEL user:name # 查看所有键,生产环境慎用 KEYS *

这里要特别提醒:KEYS *在生产环境是危险操作。因为 Redis 是单线程执行命令,KEYS *会遍历整个键空间,当 key 数量很多时,会阻塞 Redis 服务,影响所有请求。生产环境推荐使用SCAN命令分批遍历。

SCAN 0 MATCH user:* COUNT 100

5. Redis 核心数据类型与应用场景

Redis 最强大的地方之一,就是它提供了多种数据结构。很多看似复杂的需求,用对数据结构后会变得非常简单。

5.1 String:最基础的缓存方式

String 在 Redis 内部既可以存储字符串,也可以存储整数和浮点数。它几乎可以用于所有简单的缓存场景。

常见用途:

  • 缓存用户信息。
  • 缓存配置项。
  • 实现分布式锁。
  • 实现计数器。

一个简单的 Spring Boot 缓存示例:

@Service public class ArticleService { @Autowired private StringRedisTemplate redisTemplate; public Article getArticleById(String id) { // 1. 先查 Redis String cacheKey = "article:info:" + id; String articleJson = redisTemplate.opsForValue().get(cacheKey); if (articleJson != null) { return JSON.parseObject(articleJson, Article.class); } // 2. Redis 未命中,查数据库 Article article = articleMapper.selectById(id); if (article != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(article), Duration.ofMinutes(30) ); } return article; } }

这里要注意缓存穿透问题。如果查的 id 在数据库中不存在,那么缓存里也不会有数据,每次请求都会打到数据库。一种简单方案是缓存空值并设置较短的过期时间:

if (article == null) { redisTemplate.opsForValue().set(cacheKey, "null", Duration.ofMinutes(5)); return null; }

5.2 Hash:更适合存储对象字段

String 存储对象时,一般用 JSON 字符串整体序列化。这有一个问题:如果只修改对象中的一个字段,也需要先取出整个 JSON 字符串,反序列化后修改,再重新序列化写回。

Hash 结构允许你存储一个对象的多个字段,并且可以单独操作某个字段。

// 写入对象字段 redisTemplate.opsForHash().put("user:info:" + userId, "name", "张三"); redisTemplate.opsForHash().put("user:info:" + userId, "age", "25"); // 读取单个字段 String name = (String) redisTemplate.opsForHash().get("user:info:" + userId, "name"); // 读取所有字段 Map<Object, Object> entries = redisTemplate.opsForHash().entries("user:info:" + userId);

Hash 特别适合存储“半结构化”的数据,比如用户资料、商品信息等,字段可变、更新频繁。

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

List 是一个双向链表,支持从头部或尾部插入、弹出元素。常见用途包括:

  • 实现简单的消息队列。
  • 存储用户操作日志。
  • 存储最新文章列表。

生产者:

LPUSH news:list "article-1" LPUSH news:list "article-2"

消费者:

# 从尾部阻塞式弹出,超时时间 10 秒 BRPOP news:list 10

在 Spring Boot 中,可以用opsForList()操作:

// 生产者 redisTemplate.opsForList().leftPush("news:list", "article-1"); // 消费者 String articleId = redisTemplate.opsForList().rightPop("news:list", 10, TimeUnit.SECONDS);

注意,Redis List 实现消息队列的优点是简单、快速,但不支持消息确认、消息回溯等高级特性。如果消息丢失会造成业务问题,建议使用专业的消息队列组件。

5.4 Set:去重与交集运算

Set 是无序集合,元素不可重复,支持交集、并集、差集运算。

常见用途:

  • 记录文章的点赞用户。
  • 实现标签系统。
  • 统计 UV(独立访客)。
// 用户点赞 redisTemplate.opsForSet().add("article:likes:" + articleId, userId); // 取消点赞 redisTemplate.opsForSet().remove("article:likes:" + articleId, userId); // 统计点赞数 Long likeCount = redisTemplate.opsForSet().size("article:likes:" + articleId); // 判断用户是否点赞 Boolean liked = redisTemplate.opsForSet().isMember("article:likes:" + articleId, userId);

5.5 ZSet:排行榜的利器

ZSet(有序集合)在 Set 的基础上增加了分数(score)属性,集合中的成员会根据分数从小到大排序。

这几乎是排行榜功能的标准答案。比如实时排行榜:

// 分数 +1 redisTemplate.opsForZSet().incrementScore("ranking:hot", articleId, 1); // 获取前 10 名 Set<String> top10 = redisTemplate.opsForZSet() .reverseRange("ranking:hot", 0, 9); // 获取某个成员的排名 Long rank = redisTemplate.opsForZSet() .reverseRank("ranking:hot", articleId);

如果没有 ZSet,你要实现排行榜需要定时扫描全表、按分数排序、再截取前 N 条。有了 ZSet,每次操作都是 O(log N) 级别的,效率和简单性都提升了一个档次。

5.6 其他类型:Bitmap、HyperLogLog、Geo

  • Bitmap:位图结构,适合统计用户签到、在线状态等场景,内存占用极小。
  • HyperLogLog:用于基数统计,适合统计 UV,允许一定误差但内存占用非常低。
  • Geo:存储地理位置,适合实现附近的人、附近的门店等功能。

这些数据结构在实际项目中也会遇到,但它们属于进阶内容,基础文章里先不展开,后面可以单独写一篇详解。

6. 完整实战:用 Redis 实现一个点赞 + 排行榜模块

前面讲了概念,这一步我们做一个相对完整的实战案例。场景是:一个内容社区系统,需要实现文章点赞功能和热门文章排行榜。

需求拆分:

  • 用户可以对文章点赞和取消点赞。
  • 每篇文章展示点赞数量。
  • 用户能查看自己是否已点赞。
  • 系统需要实时展示点赞数最高的前 10 篇文章。

技术选型:

  • Spring Boot 2.7.x 及以上。
  • Spring Data Redis。
  • Jackson 进行 JSON 序列化。

6.1 创建项目结构

src/main/java/com/example/redisdemo ├── RedisDemoApplication.java ├── controller │ └── ArticleController.java ├── service │ └── ArticleService.java └── vo └── ArticleVO.java

6.2 添加依赖

pom.xml中添加 Redis 相关依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>

6.3 编写配置文件

application.yml

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

这里使用的是默认的 Lettuce 客户端。password留空表示无密码连接,生产环境务必设置强密码。

6.4 编写 Service 层

@Service public class ArticleService { @Autowired private StringRedisTemplate redisTemplate; private static final String LIKE_KEY_PREFIX = "article:likes:"; private static final String RANKING_KEY = "article:ranking"; /** * 点赞或取消点赞 */ public Boolean likeArticle(String articleId, String userId) { String likeKey = LIKE_KEY_PREFIX + articleId; Boolean added = redisTemplate.opsForSet().add(likeKey, userId); if (Boolean.TRUE.equals(added)) { // 点赞成功,排行榜分数 +1 redisTemplate.opsForZSet().incrementScore(RANKING_KEY, articleId, 1); return true; } // 已经点过赞,执行取消 redisTemplate.opsForSet().remove(likeKey, userId); redisTemplate.opsForZSet().incrementScore(RANKING_KEY, articleId, -1); return false; } /** * 获取文章点赞数 */ public Long getLikeCount(String articleId) { String likeKey = LIKE_KEY_PREFIX + articleId; return redisTemplate.opsForSet().size(likeKey); } /** * 用户是否已点赞 */ public Boolean hasLiked(String articleId, String userId) { String likeKey = LIKE_KEY_PREFIX + articleId; return redisTemplate.opsForSet().isMember(likeKey, userId); } /** * 获取热门文章 Top N */ public List<String> getHotArticles(int topN) { Set<String> articles = redisTemplate.opsForZSet() .reverseRange(RANKING_KEY, 0, topN - 1); return articles == null ? Collections.emptyList() : new ArrayList<>(articles); } }

这段代码的核心逻辑并不复杂,但有几个点值得注意:

  1. 用 Set 来存储点赞用户,天然去重,点赞状态和点赞数都由它推导而来。
  2. 用 ZSet 来维护排行榜,每次点赞或者取消点赞都实时更新分数。
  3. 所有操作都是原子的,不会有并发写问题。

6.5 编写 Controller 层

@RestController @RequestMapping("/api/article") public class ArticleController { @Autowired private ArticleService articleService; @PostMapping("/like") public Map<String, Object> like(@RequestParam String articleId, @RequestParam String userId) { Boolean liked = articleService.likeArticle(articleId, userId); Map<String, Object> result = new HashMap<>(); result.put("liked", liked); result.put("likeCount", articleService.getLikeCount(articleId)); return result; } @GetMapping("/likes") public Map<String, Object> getLikeCount(@RequestParam String articleId) { Map<String, Object> result = new HashMap<>(); result.put("articleId", articleId); result.put("likeCount", articleService.getLikeCount(articleId)); return result; } @GetMapping("/hot") public Map<String, Object> hot(@RequestParam(defaultValue = "10") int topN) { Map<String, Object> result = new HashMap<>(); result.put("articles", articleService.getHotArticles(topN)); return result; } }

6.6 运行与验证

启动 Spring Boot 应用后,通过curl命令模拟请求:

# 用户 1001 点赞文章 2001 curl -X POST "http://localhost:8080/api/article/like?articleId=2001&userId=1001" # 再点一次,观察返回 liked = false,说明取消失败 curl -X POST "http://localhost:8080/api/article/like?articleId=2001&userId=1001" # 查询点赞数 curl "http://localhost:8080/api/article/likes?articleId=2001" # 查看热门榜 curl "http://localhost:8080/api/article/hot?topN=10"

预期效果:

  • 第一次点赞返回liked: true,点赞数为 1。
  • 第二次点赞返回liked: false,点赞数为 0。
  • 多次对不同文章点赞后,热门榜能实时反映排名。

这个功能如果只用数据库实现,需要每次查询都去统计点赞表,会很冗余。而 Redis 用两个数据结构就搞定,而且查询效率极高。

6.7 数据落库策略

上面的实现把数据都放在 Redis 里,那数据库还要不要存呢?答案是:要存,但可以异步延迟落库。

常见的落库策略包括:

  • 定时任务每隔一段时间,把 Redis 中的点赞数和排行榜同步到数据库。
  • 每次点赞时同时发送一条消息到消息队列,由消费者异步写数据库。
  • 在系统低峰期做全量同步。

这里提醒一句:点赞功能一般允许一定的延迟,但要注意 Redis 数据持久化。如果你开启了 AOF 或 RDB,Redis 重启后数据还能恢复;如果只是纯内存运行,需要考虑数据丢失的代价。

7. 常见问题与排查思路

7.1 Redis 连接失败

问题现象:应用启动时控制台报错Unable to connect to Redis

可能原因

  • Redis 服务没有启动。
  • IP 或端口配置错误。
  • Redis 开启了保护模式,远程连接被拒绝。
  • 防火墙拦截了 6379 端口。

排查步骤

  1. 先在服务器本机执行redis-cli ping,确认服务是否正常。
  2. 从应用所在机器执行telnet <服务器IP> 6379,测试网络连通性。
  3. 检查配置文件bindprotected-mode设置。
  4. 检查云服务器安全组是否放通 6379 端口。

解决方案

  • 生产环境 Redis 不要直接暴露公网。
  • 通过 VPC 内网 IP 访问。
  • 设置强密码并开启防火墙白名单。

7.2 缓存和数据库数据不一致

问题现象:页面展示的缓存数据与数据库中的数据不一致。

可能原因

  • 更新数据库后没有及时删除缓存。
  • 删缓存和更新数据库之间出现并发窗口。
  • 多个服务同时更新同一个 key 时出现竞争。

解决思路

  • 采用“先更新数据库,再删除缓存”的策略。
  • 如果对一致性要求高,可以通过消息队列异步删除缓存。
  • 给缓存设置过期时间作为兜底方案。

这里要额外说明:Redis 缓存与数据库的强一致很难做到,业界一般接受最终一致性。如果业务要求强一致,不建议引入缓存。

7.3 缓存穿透、缓存击穿、缓存雪崩

这是 Redis 面试和项目中最高频的三个概念。

问题现象解决思路
缓存穿透查询一个不存在的 key,每次都打到数据库缓存空值、布隆过滤器
缓存击穿热点 key 过期瞬间,大量请求打到数据库互斥锁重建缓存、逻辑过期
缓存雪崩大量 key 在同一时间过期过期时间加随机值、多级缓存

初学者面试时常被问到这三个问题的区别。简单记忆方式:

  • 穿透是“查了个不存在的东西”。
  • 击穿是“一个热点 key 挂了”。
  • 雪崩是“一堆 key 一起挂了”。

7.4 Redis keys 命令导致阻塞

问题现象:执行KEYS *后,Redis 响应变慢,大量请求超时。

可能原因:Redis 单线程执行命令,全量遍历已经存在很多 key。

解决方案

  • 生产环境严禁使用KEYS *
  • 使用SCAN命令分批遍历。
  • 业务 key 设计时带上明确的业务前缀,按前缀定位。

7.5 Redis 内存持续增长

问题现象:内存使用率持续偏高,甚至触发淘汰策略导致缓存命中率下降。

可能原因

  • 大量 key 没有设置过期时间。
  • 大 value 写入了 Redis。
  • 数据没有及时清理。

解决方案

  • 根据业务场景设置合理的 TTL。
  • 使用MEMORY USAGE key分析单个 key 的内存占用。
  • 通过 Redis Desktop Manager 或 Redis Insight 查看内存分析报告。
  • 对大 key 进行拆分或者压缩,避免阻塞 Redis。

7.6 Linux 下后台启动 Redis

很多初学者在 Linux 上启动 Redis 时,直接使用redis-server前台运行,一旦关闭终端服务就停了。

推荐使用配置文件方式启动:

# 修改 redis.conf 中的 daemonize 为 yes daemonize yes # 启动 redis-server /path/to/redis.conf # 查看进程 ps -ef | grep redis

如果使用 systemd 管理,需要编写 service 文件;使用 Docker 则建议加--restart=always保证自动重启。

8. Redis 工程落地的十条建议

技术选型和代码实现只是第一步,真正考验功底的是工程落地的细节。这里把我认为最重要的十条建议列出来,很多是从线上问题里总结出来的经验。

8.1 key 命名规范必须提前定好

Redis 的 key 不像数据库表字段那样有强约束,所以更需要人为统一规范。

推荐格式:

业务模块:实体:主键:字段

示例:

user:info:1001 article:likes:2001 order:status:3001

好处是:

  • 一眼看出 key 属于哪个模块。
  • 便于排查问题时定位。
  • 便于使用SCAN按前缀筛选。

8.2 设置合理的过期时间

不是所有 key 都需要永久保存。要根据业务容忍度设置 TTL:

  • 热点数据缓存:30 分钟到 1 小时。
  • 验证码:5 分钟。
  • 分布式锁:根据业务执行时间设置,并预留缓冲。
  • 排行榜:可以长期保存,但历史数据需要清理。

为每个 key 设置过期时间,避免 Redis 内存无限增长。

8.3 区分业务数据与缓存数据

Redis 中同一份数据可能既充当缓存,又充当业务数据源。要明确:

  • 缓存数据可以随时丢失,丢失后可以从数据库重建。
  • 业务数据需要持久化保护,不能只依赖 Redis。

在代码中可以通过 key 前缀区分,比如cache:biz:

8.4 使用连接池

生产环境一定要配置连接池,避免每次操作都创建新连接。

Spring Boot 集成 Lettuce 后,默认自带连接池配置。如果你在配置文件中手动调整,注意以下参数:

  • max-active:最大连接数。
  • max-idle:最大空闲连接数。
  • min-idle:最小空闲连接数。
  • timeout:连接超时时间。

连接数不是越大越好,要根据机器的文件句柄数和 Redis 服务端的连接上限来调整。

8.5 序列化方式要统一

Spring Data Redis 提供了多种序列化器:

  • StringRedisSerializer:适合 key 和简单的 String value。
  • Jackson2JsonRedisSerializer:适合存储对象,可读性好。
  • GenericJackson2JsonRedisSerializer:保存对象类型信息,反序列化更方便。
  • JdkSerializationRedisSerializer:默认方式,可读性差,不推荐线上使用。

一个常见坑是:用默认序列化器写入数据后,通过redis-cli查看时是一堆二进制乱码。解决方法是明确配置序列化器。

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }

8.6 监控必须提前接入

Redis 不是丢进去就不用管的组件。线上环境至少需要监控以下指标:

  • 内存使用率。
  • 连接数。
  • 命中率。
  • 阻塞命令耗时。
  • 慢查询日志。
  • 主从复制延迟。

常见的监控方案有:

  • Redis 自带的INFO命令。
  • RedisInsight 的内存分析和性能面板。
  • Prometheus + redis_exporter + Grafana。

8.7 安全配置:改默认端口、设置强密码

Redis 历史上出过不少因为安全配置不当导致被入侵的案例。生产环境务必:

  • 修改默认端口 6379,或者通过防火墙限制访问来源。
  • 设置强密码,并使用requirepass配置。
  • 不要用root用户运行 Redis。
  • 禁用或者限制危险命令,比如FLUSHALLKEYS

如果你对 Redis 安全不太熟悉,优先选择云厂商提供的 Redis 托管服务,安全能力会比自建更强。

8.8 主从复制和持久化,根据业务级别选择

Redis 允许配置主从复制,但初学者经常会把主从复制和集群混为一谈。简单来说:

  • 主从复制:解决单点故障和读写分离,数据在主节点写入,从节点异步同步。
  • 哨兵模式:在主从复制基础上增加了自动故障转移能力。
  • 集群模式:数据分片存储,适合大规模数据和高可用场景。

如果业务对数据安全要求高,必须开启持久化,RDB 和 AOF 可以同时开启。如果只是缓存场景,可以接受丢失部分数据,只开 RDB 即可。

8.9 大 key 和热 key 治理

大 key 是指单个 key 的值特别大,比如一个 List 里有几十万个元素,一个 Hash 里字段特别多。这类 key 会导致 Redis 阻塞、网络传输变慢。

热 key 是指被高频访问的 key,比如某个爆款商品。

常见治理手段包括:

  • 大 key 拆分:按业务维度拆成多个小 key。
  • 热 key 加副本:在多个节点上复制热点 key,分散读压力。
  • 本地缓存兜底:在一级缓存层面加一个本地缓存,减少对 Redis 的访问。

8.10 不要把所有功能都塞进 Redis

有一个很常见的趋势:团队一旦引入 Redis,什么功能都想往里塞。结果 Redis 里塞满了各种模块的缓存、消息、计数器、去重数据,最后维护成本极高。

建议先明确 Redis 在团队内部承担什么角色。它可以是缓存,可以是锁服务,也可以是轻量级消息队列,但不能什么都做。职责不清的系统,最终一定会出问题。

9. 从一个问题出发,深入学习 Redis

回到文章标题的那个问题:系统没有高并发,到底要不要用 Redis?

我的建议是:不要为了“赶时髦”而引入 Redis,但也不要因为“并发不高”而否定 Redis 的价值。真正决定要不要用的标准应该是——当前业务是否有合适 Redis 解决的痛点。

比如一个后台管理系统,每天只有几百个请求,用户量也很小。那我不会建议你引入 Redis,因为数据库加一个索引可能就把问题解决了。但如果这个系统有排行榜、有点赞、需要存储登录 Session、要限制接口频率、要处理库存,那么即使并发不高,Redis 依然是一个合理且高效的组件。

作为初学者,在学习 Redis 时建议按下面的路径深入:

  1. 掌握基础命令和五种核心数据类型的使用。
  2. 学会在 Spring Boot 中集成 Redis。
  3. 理解缓存穿透、击穿、雪崩的概念与解决方案。
  4. 动手做一个基于 Redis 的实战项目,比如排行榜、点赞、购物车。
  5. 学习分布式锁的原理与 Redisson 的实现。
  6. 了解 Redis 持久化、主从复制、哨兵、集群的原理。
  7. 研究 Redis 底层的对象编码、内存模型、阻塞与异步机制。
  8. 尝试用 Redis 解决消息队列、延迟队列、限流等进阶问题。

每一步都是建立在“理解应用场景”的基础上,而不是死记命令。实践是检验理解的最好方式,最好能自己搭一套环境,把文章里的示例跑一遍,再想想你的业务里有哪些地方可以优化。

希望这篇文章能帮你建立一个相对完整的 Redis 认知框架。如果你在实际操作中遇到其他问题,也欢迎在评论区留言,一起交流排查思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询