开篇先从疑问切入。很多人第一次接触 Redis,可能都是从“面试题”或“项目缓存优化”开始的。但真正使用一段时间后,你会发现 Redis 能做的事情远不止缓存。它可以是分布式锁的承载体,可以做消息队列,可以扛住海量计数场景,甚至可以作为轻量级数据库使用。一个中间件能火这么多年,而且在面试、实战、系统设计里反复出现,背后一定有值得深挖的设计理念。本文就围绕“Redis 凭什么这么火”这个问题,拆解它的 3 个核心秘密:为什么它快、为什么它并发能力强、为什么它这么好用。同时会给出环境搭建、Java 客户端操作、分布式锁实战、缓存设计与常见坑点,帮助你从会用走向会用。
先说清楚一个概念:Redis 是开源的、基于内存的数据结构存储系统,通常被归类为 NoSQL 数据库,也可以叫内存数据库或缓存中间件。它的官方定义是“Redis is an open source, BSD licensed, advanced key-value store”,但纯 key-value 已经不足以概括它的能力。它支持字符串、哈希、列表、集合、有序集合、位图、HyperLogLog、地理坐标、流(Stream)等多种数据结构,每种结构都有对应的原子操作。正是这些数据结构,让 Redis 从“缓存工具”变成了“业务工具箱”。
很多开发者第一次被 Redis 吸引,就是因为它快。单机 Redis 的读性能可以达到每秒十万次级别,写性能也在万级到十万级之间。这个“快”并不只是因为内存,而是内存、数据结构、网络模型、IO 模型共同作用的结果。也正因为快,Redis 才能承担缓存、计数器、排行榜、分布式锁、信号量等对延迟敏感的场景。如果数据库查询需要几十毫秒,Redis 读操作往往在亚毫秒级别,差距非常明显。
但 Redis 的魅力不只是“快”。它最反直觉的设计是单线程模型。在 Java 并发编程中,我们想尽办法用多线程提升吞吐量,Redis 却反过来用单线程加事件驱动模型,规避了锁竞争、上下文切换、线程安全问题,让性能在并发场景下依然稳定。这个设计思路值得每一个后端开发者认真理解。
本文适合这些读者:刚入门 Redis、被面试题里“Redis 为什么快、为什么单线程还这么快”困扰的初级开发者;想用 Redis 做缓存、分布式锁、消息队列的 Java 后端工程师;以及那些已经在用 Redis,但还想把性能调优、缓存一致性、生产排错做扎实的技术同学。
读完之后,你会获得四样东西:对 Redis 设计理念的系统认知;一套可复制到本机的 Redis 环境搭建流程;一个基于 Jedis 的完整 Java 操作示例;以及一份覆盖缓存穿透、缓存击穿、缓存雪崩、分布式锁误删、连接池配置等高频问题的实战排查手册。
1. 背景与核心概念
1.1 Redis 到底是什么
Redis 的全程是 Remote Dictionary Server,即远程字典服务。字典(Dictionary)这个词很形象,因为 Redis 的数据模型本质上就是一个大的哈希表:通过 key 找到 value,而 value 可以是多种数据结构。
它的核心特性可以归纳为 5 点:
- 基于内存存储,数据读写极快。
- 支持持久化,可以把内存数据保存到磁盘(RDB、AOF)。
- 数据结构丰富,不限于字符串。
- 支持过期策略,适合缓存场景。
- 提供发布订阅、事务、Lua 脚本、管道等进阶能力。
在系统架构中,Redis 最常见的角色是处于应用和关系型数据库之间的缓存层。应用先查 Redis,没有数据再查 MySQL,并把查询结果回填到 Redis。这样做能大幅降低数据库压力,提升接口响应速度。典型场景包括首页热点数据、商品详情、用户会话、验证码、排行榜等。
除了缓存,Redis 还被广泛用于:
- 分布式锁:利用 SETNX + EXPIRE 或 Redisson 实现多实例互斥。
- 排行榜:使用有序集合 ZSet。
- 计数器:使用 INCR、DECR 做 PV、UV、库存扣减。
- 消息队列:使用 List、Stream 实现轻量级消息通信。
- 签到、去重统计:使用 Bitmap、HyperLogLog。
- 分布式 ID 或不重复号段:使用 INCR 或 Lua 脚本。
这也是为什么 Redis 火的原因之一:它不是单一工具的定位,而是可以用一套系统解决多个问题。
1.2 Redis 与普通缓存 / 本地 Map 的区别
很多新手会问:我在 Java 里用 ConcurrentHashMap 做缓存不也行吗,为什么要用 Redis?
区别不在“能不能存”,而在“存给谁用”。本地缓存 Map 是 JVM 内存,数据只在当前进程内可见。如果服务部署了多个实例,用户请求落在不同实例上,就会各自维护一份缓存,互相不可见。而且 JVM 内存有限,服务重启缓存就没了。
Redis 是独立部署的中间件,所有实例共享同一份缓存数据。它提供了网络访问接口、持久化、过期策略、数据淘汰策略、主从复制、集群分片这些能力,是一个“可横向扩展、可持久化、可集中管理”的缓存服务。
简单对比:
| 对比项 | Java 本地 Map | Redis |
|---|---|---|
| 存储位置 | JVM 堆内 | 独立进程内存 |
| 多实例共享 | 不共享 | 共享 |
| 持久化 | 无 | RDB / AOF |
| 过期策略 | 需要自己实现 | 内置 EXPIRE、TTL |
| 数据结构 | 基本类型、对象 | String、Hash、List、Set、ZSet 等 |
| 并发控制 | JVM 锁 | 单线程 + 原子命令 / Lua |
| 客户端访问 | 本地方法调用 | 网络协议,多语言 SDK |
掌握了这个区别,你就知道哪些数据该放本地缓存,哪些该放 Redis。
1.3 “Redis 火”的底层原因
从工程角度看,一个中间件能被大规模使用,通常要满足三个条件:
- 能解决真实痛点。Redis 解决了数据库读压力大的问题。
- 使用门槛低。Redis 命令简单,学习成本比搜索引擎、消息队列低很多。
- 生态成熟。官方支持多种语言客户端,有主从、哨兵、集群方案,能从小项目用到大型系统。
这三点 Redis 全部满足。而接下来要讲的 3 个核心秘密,分别对应它的性能优势、并发优势、功能优势。
2. 环境准备与版本说明
2.1 本文使用的环境
为了保证示例能实际运行,先说明我的演示环境。你可以结合自己的系统调整,区别主要在安装方式上。
- 操作系统:Linux(CentOS 7 以上,或 Ubuntu 18.04 以上)
- Redis 版本:以 6.x 稳定版为例说明,重点演示命令和配置思路
- JDK 版本:Java 8 及以上
- 构建工具:Maven 3.6+
- 客户端:Jedis 4.x
- 可视化工具:Redis Desktop Manager(RDM)或 Another Redis Desktop Manager,可自行选择
如果你的操作系统是 Windows,建议使用 WSL2 安装 Linux 环境,或者使用 Redis 官方提供的 Windows 移植版本。需要注意,Redis 官方并不正式支持 Windows,生产环境绝大多数部署在 Linux 上。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.2 安装 Redis 服务
下面以 Linux 源码编译方式安装 6.2.x 为例。源码编译虽然步骤多一点,但能让你理解 Redis 的安装结构。
# 下载并解压 wget https://download.redis.io/releases/redis-6.2.14.tar.gz tar -xzf redis-6.2.14.tar.gz cd redis-6.2.14 # 编译 make # 如果 make 报错缺少 gcc,先安装 # yum install -y gcc编译完成后,会在 src 目录生成 redis-server、redis-cli、redis-sentinel 等可执行文件。也可以执行 make install 把它们安装到 /usr/local/bin。
启动 Redis 服务:
# 前台启动方式,方便看日志 src/redis-server # 指定配置文件后台启动 src/redis-server /path/to/redis.conf验证是否启动成功:
src/redis-cli ping如果输出 PONG,说明服务正常。
生产环境通常会设置 daemonize yes,让 Redis 后台运行;设置 requirepass 开启访问密码;设置 bind 限制监听地址。下面是一份最小安全配置参考,内容放到 redis.conf:
# 后台运行 daemonize yes # 监听地址,本机访问用 127.0.0.1,远程访问按需修改,不要直接配 0.0.0.0 bind 127.0.0.1 # 认证密码,生产环境必须设置 requirepass your-strong-password # 设置日志文件 logfile "/var/log/redis/redis.log" # 开启持久化 appendonly yes修改配置后重启 Redis:
src/redis-cli shutdown src/redis-server /path/to/redis.conf开启密码后,命令行访问需要带密码:
src/redis-cli -a your-strong-password这里要提醒一句:命令行带 -a 密码会出现在 shell 历史中,演示环境可以,生产环境建议先登录客户端再执行 AUTH 命令,或使用 REDISCLI_AUTH 环境变量。
2.3 可视化客户端选择
如果你习惯用图形界面操作 Redis,可以安装 Redis Desktop Manager。这款工具曾用过另一个名字 Another Redis Desktop Manager,新版官方名称为 Redis Desktop Manager。它可以查看 key 列表、查看数据结构、执行命令,适合日常调试。
也有不少团队使用 Redis Insight,它是 Redis 官方推出的桌面客户端,功能更现代,自带性能监控。选择哪款取决于个人习惯,不影响 Redis 本身的使用。
3. 核心秘密一:纯内存与高效数据结构
3.1 内存让延迟从“量变”到“质变”
Redis 的第一个核心秘密,是它的数据主要放在内存中。
传统的 Web 请求链路通常是:应用查询 MySQL,MySQL 需要做磁盘 IO、解析 SQL、走索引、回表、返回数据。一次普通查询耗时 5ms 到 30ms 很常见。如果并发上来,数据库的连接数、CPU、磁盘 IO 都会成为瓶颈。
Redis 把数据放内存,省去了磁盘寻道和块读取的时间。内存的随机访问延迟是纳秒级,相比机械硬盘的毫秒级延迟,差距在几个数量级。再加上 Redis 的网络协议简洁、命令处理逻辑高效,一次简单读写往往只需不到 1ms。
但要注意,“内存快”只是基础。如果实现得很粗糙,比如为每个 key 都复制一份完整字符串、每操作一次做一次序列化,性能一样上不去。所以 Redis 在数据结构的实现上也下了很大功夫。
3.2 针对数据结构的特殊编码
Redis 的值并不是简单用字符串表示。同样的类型,会根据数据规模使用不同的底层编码,目的是节省内存、提高操作效率。
例如 String 类型底层可以是 int、raw、embstr 三种编码:
- 如果 value 是整数且范围合适,Redis 直接用整数存储,节省空间。
- 如果是短字符串,Redis 使用 embstr 编码,一次分配内存。
- 如果是长字符串,则使用 raw 编码。
Hash 类型在字段少、值小的情况下采用 ziplist 压缩列表,字段多了会转换为 hashtable。ZSet 在数据量小的时候使用 ziplist,数据量大了使用 skiplist 与 dict 的组合。
这种“小数据用紧凑结构,大数据用高效结构”的做法,让 Redis 在小数据量场景下内存占用极低。这也是为什么 Redis 被称为“数据结构服务器”而不是简单的 KV 存储。
下面用命令做一个小实验,先设置一个整数,再设置一个长字符串,观察不同类型:
127.0.0.1:6379> SET counter 100 OK 127.0.0.1:6379> STRLEN counter 3 127.0.0.1:6379> TYPE counter string实际内存占用可以通过 OBJECT ENCODING 查看:
127.0.0.1:6379> OBJECT ENCODING counter "int"这条命令会返回底层编码。你可以在自己环境中多设置几个 key,看看不同类型在不同数据规模下的编码变化。这是理解 Redis 内存优化的一个入口。
3.3 缓存淘汰与过期策略
既然 Redis 基于内存,就不能无限存数据。Redis 提供了多种内存淘汰策略,常见的有:
- noeviction:不淘汰,内存不够时写入返回错误。
- allkeys-lru:对所有 key 使用 LRU(最近最少使用)淘汰。
- volatile-lru:只对设置了过期时间的 key 使用 LRU 淘汰。
- allkeys-lfu / volatile-lfu:使用 LFU(最不经常使用)淘汰。
- random:随机淘汰。
缓存场景通常选择 allkeys-lru 或 volatile-lru。你需要根据业务判断:哪些数据是热点,哪些数据可以丢,哪些数据不能丢。生产环境建议在 redis.conf 中显式配置:
maxmemory 2gb maxmemory-policy allkeys-lru过期策略则依赖 key 的 TTL。TTL 到期后,Redis 并不会立即删除这个 key,而是采用惰性删除加定期删除结合的方式。惰性删除是在每次读取时判断 key 是否过期,定期删除是周期性地抽样检查并删除过期 key。理解这一点,就能解释为什么某些 key 过期后,内存并没有立刻下降。
3.4 常见误区:Redis 快就不需要优化了吗
很多人觉得 Redis 天生快,随手用就行。其实 Redis 的性能高低和你的使用习惯密切相关:
- 使用大量 bigkey(比如存了几 MB 的字符串),会导致单次操作耗时变长。
- 在循环里逐条调用 Redis 命令,网络 RTT 会放大耗时。
- 对 Set、ZSet 做交集、并集、范围操作,数据量大时 CPU 开销不小。
- 慢查询会影响后续命令的执行速度,因为 Redis 是单线程处理,一个慢操作会阻塞其他命令。
所以“Redis 快”是它的能力上限,能不能发挥出来,取决于你是否会用命令、是否会拆 key、是否设置合理的过期时间。这个主题放到后面的章节继续展开。
4. 核心秘密二:单线程模型与 I/O 多路复用
4.1 为什么单线程反而快
Redis 在 6.0 之前,核心命令执行是单线程的。听起来很违反直觉:现在服务器动不动几十核,单线程不是浪费 CPU 吗?
答案在于 Redis 的瓶颈通常不在 CPU,而在网络 IO 和内存大小。单线程带来的好处非常明显:
- 没有锁竞争,不存在并发修改问题。
- 没有线程切换的开销。
- 实现简单,不用考虑死锁、竞态条件。
- 命令的执行顺序是确定的,天然串行化。
对于纯内存操作,单线程执行命令的速度已经非常快。真正耗时的往往是网络读写。因此 Redis 使用了 I/O 多路复用机制,配合事件循环,在同一线程内处理多个客户端连接。
可以用一个比喻理解:传统多线程模型就像开很多窗口,每个窗口一个服务员;Redis 单线程模型就像一个服务员在多个窗口间来回服务,哪个窗口有请求就先处理哪个。如果每个请求处理都很快,这个服务员完全忙得过来,还省去了协调多个服务员的成本。
当然,这个比喻忽略了系统调度的细节,但核心思想是一致的:Redis 选择用单线程避开并发复杂度,用事件驱动提升 IO 吞吐量。
4.2 I/O 多路复用与事件循环
I/O 多路复用是指一个线程通过内核机制同时监控多个文件描述符,当某个描述符可读或可写时,内核通知应用程序去处理。Linux 上常见的机制包括 select、poll、epoll,Redis 会根据系统选择最合适的实现。
事件循环可以简化成下面这个流程:
- 接收客户端连接请求。
- 将连接注册到事件循环中。
- 等待事件发生(有数据可读、有空间可写、新连接到来)。
- 事件触发后,调用对应处理器。
- 处理完继续回到等待步骤。
这个模型保证了单个线程能够高效服务大量连接。Redis 官方数据提到,单实例可以支撑数万乃至十万级连接,实际并发量还会受网络带宽、命令复杂度、内存分配影响。
4.3 命令原子性与无锁设计
单线程执行还有一个额外好处:每个命令天然是原子的。在任意时刻,Redis 只会执行一个命令,所以多个客户端同时执行 INCR 操作时不会出现“读-改-写”的中间态竞争。
我们来做一个小实验。启动多个 redis-cli,同时对一个 key 执行 INCR:
# 终端1 INCR click_count # 终端2 INCR click_count # 终端3 INCR click_count无论发多少次,最终结果都是精确递增,不会因为并发而丢失计数。这正是 Redis 适合做计数器、秒杀库存扣减的原因。
对于复杂的“读-判断-写”操作,Redis 也提供了 Lua 脚本支持,把多条命令打包成一个原子操作。后面分布式锁部分会用到这个能力。
4.4 Redis 6.x 之后的变化
很多文章把“单线程”当作 Redis 的固定标签,但 Redis 6.0 引入了多线程 IO。这里必须说清楚:多线程只用于网络数据的读取、解析和写回,核心命令执行仍然由主线程串行完成。
这样做既保留了命令执行的简单性和原子性,又利用多核心加速了网络 IO,避免大量客户端在高吞吐场景下出现网络处理瓶颈。所以现在的说法更准确的是:Redis 命令执行仍然是单线程,但网络 IO 可以多线程处理。
了解了这一点,你就不会在面试中说错“Redis 完全不使用多线程”,也不会误解“Redis 6.0 变成多线程了”。
4.5 单线程模型对使用者的启示
单线程模型带给我们几个工程启示:
- 避免慢查询。凡是 O(N) 的命令,如 KEYS、SMEMBERS、HGETALL,在大 key 上执行会阻塞主线程,引发全库卡顿。
- 控制单次操作的数据量。一次写入超大 value 会造成内存分配阻塞。
- 不要在大事务中执行大量命令。MULTI/EXEC 中的命令会排队执行,事务期间其他客户端请求会等待。
- 使用连接池。客户端反复创建和销毁连接,不仅浪费网络资源,也会让 Redis 频繁处理建立连接事件。
“快”不是无条件的,Redis 单线程的高性能建立在每个命令都很快的前提上。这是你在使用时必须时刻记住的原则。
5. 核心秘密三:丰富的数据类型与原子操作
5.1 一个 Redis,多个工具箱
第三个核心秘密,是 Redis 不满足于做一个 KV 缓存,而是提供了一整套高性能数据结构。下面把最常用的数据类型梳理一遍。
String(字符串)
String 是 Redis 最基础的类型。它可以存字符串、整数、浮点数、二进制数据。除了 GET、SET,还有 INCR、DECR、SETNX、SETEX 等命令。
常用场景:缓存对象序列化后的 JSON、计数器、验证码、分布式锁 value 标记。
SET user:1001 '{"name":"tom","age":18}' GET user:1001 INCR page_view EXPIRE user:1001 300Hash(哈希)
Hash 适合表示一个对象。它内部是一组 field-value,比如用户信息包含 name、age、email。相比把整个对象序列化成 JSON,Hash 支持只修改某个字段,节约网络流量。
HSET user:1001 name "tom" age 18 email "tom@example.com" HGET user:1001 name HGETALL user:1001List(列表)
List 底层是链表结构,支持从头部或尾部压入弹出元素。适合简单消息队列、最新列表、最近浏览历史等场景。
LPUSH notify:queue "msg1" RPUSH notify:queue "msg2" LPOP notify:queue LRANGE notify:queue 0 -1Set(集合)
Set 保证元素唯一,支持交集、并集、差集运算,适合标签、好友关系、去重、抽奖等场景。
SADD tag:java "spring" "redis" SADD tag:backend "redis" "mysql" SINTER tag:java tag:backendZSet(有序集合)
ZSet 在 Set 基础上给每个元素增加 score,可以按分数排序。适合排行榜、延迟队列、最近热播排序等场景。
ZADD ranking:game 100 "player1" ZADD ranking:game 200 "player2" ZINCRBY ranking:game 5 "player1" ZREVRANGE ranking:game 0 -1 WITHSCORESBitmap、HyperLogLog、Geo、Stream
- Bitmap 适合签到、在线状态、布隆过滤器的位级操作。
- HyperLogLog 用来做海量数据的基数统计,误差可控,内存占用极小。
- Geo 用来存储地理位置并计算距离。
- Stream 是 Redis 5.0 引入的消息队列模型,支持消息持久化和消费者组。
这些类型覆盖了很多业务常见场景,让开发者在大多数情况下不用再引入额外的存储组件。
5.2 原子操作:不只是快,更是安全
Redis 的原子性不仅体现在单个命令,还可以通过 Lua 脚本把多条命令组合成原子操作。
一个典型例子是“扣减库存”。
不安全的写法是:先 GET 库存,判断是否大于 0,再 DECR。这个过程在并发下会出现超卖。如果在多线程代码里,你会想到加锁;但在 Redis 里,直接使用 Lua 脚本,可以一次性完成判断和扣减:
local stock = tonumber(redis.call('GET', KEYS[1])) if stock and stock > 0 then redis.call('DECR', KEYS[1]) return 1 end return 0Java 侧通过 Jedis 执行这段脚本时,整个脚本会被 Redis 原子执行,不会插入其他命令。这比“先查再扣”要安全得多。
5.3 消息队列与 Stream
Redis 用作消息队列时,很多人会问:它和 Kafka、RabbitMQ 有什么区别?
结论是:如果业务量不大、对消息丢失要求不高、不想引入额外中间件,Redis 的 List 或 Stream 可以撑起轻量级队列场景。但如果需要消息回溯、严格的分区顺序、大规模堆积、多种消费模式,则更适合使用专业的消息队列。
使用 List 实现简单队列很直观:
# 生产者 LPUSH task:queue "job-1" # 消费者 BRPOP task:queue 5BRPOP 是阻塞式弹出,没有消息时会阻塞等待,避免轮询空转。
Redis 5.0 的 Stream 提供了 XADD、XREAD、XGROUP、XACK 等命令,支持消费者组。实现消息确认、消费组分配、消息历史读取,是比 List 更完整的队列方案。你可以把 Stream 理解为一个能存消息的内存日志,适合在 Redis 生态内做消息发布订阅。
5.4 分布式锁:Redis 高价值场景
Redis 能火,很大一部分原因是它能实现简单可靠的分布式锁。在分布式系统中,多个服务实例需要互斥地执行某个操作(比如定时任务只允许一个节点执行、防止用户重复下单),这时可以用 Redis 的 SETNX 命令。
核心思想:利用 Redis 单线程执行命令的原子性,通过 SET key value NX EX timeout 实现“只有 key 不存在时才能设置成功”。谁设置成功,谁就获得锁;释放时删除 key。
下面给出一个 Java 版本的示例实现。需要注意锁的 value 要能标识持有者,避免误删他人锁;过期时间必须设置,防止持有者宕机导致死锁。释放锁时使用 Lua 脚本校验 value,保证原子性。
// 文件路径:src/main/java/com/example/redis/RedisLockDemo.java import redis.clients.jedis.Jedis; public class RedisLockDemo { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final String LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) " + "else return 0 end"; private final Jedis jedis; public RedisLockDemo(Jedis jedis) { this.jedis = jedis; } /** * 获取分布式锁 * * @param lockKey 锁的 key * @param requestId 请求标识,用于释放锁时校验 * @param expireMs 过期时间,毫秒 * @return 是否获取成功 */ public boolean lock(String lockKey, String requestId, long expireMs) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMs); return LOCK_SUCCESS.equals(result); } /** * 释放分布式锁 * 只有 value 匹配时才删除,防止误删其他线程持有的锁 */ public boolean unlock(String lockKey, String requestId) { Object result = jedis.eval(LOCK_SCRIPT, java.util.Collections.singletonList(lockKey), java.util.Collections.singletonList(requestId)); return Long.valueOf(1L).equals(result); } }这段代码体现了 Redis 分布式锁的三个基本原则:
- 使用 SET NX EX 保证原子设置锁和过期时间。
- value 使用唯一标识,释放时先校验再删除。
- 删除操作使用 Lua 脚本,保证“判断和删除”的原子性。
当然,生产级分布式锁更推荐使用 Redisson,它封装了锁续期、看门狗、公平锁、读写锁、红锁等能力,避免自己造轮子踩坑。但如果只是理解原理,上面的实现足够帮助你理解 Redis 为何在分布式场景中这么重要。
6. 完整实战案例:从安装到缓存应用
6.1 项目结构
下面用一个最小 Java 项目串联起来,演示 Redis 的常见用法。项目只依赖 Jedis,不需要引入 Spring Boot,便于理解流程。
redis-demo/ ├── pom.xml └── src/main/java/com/example/redis/ ├── RedisConnection.java ├── CacheService.java └── CacheApplication.java6.2 配置 Maven 依赖
在 pom.xml 中引入 Jedis:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>redis-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.6</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.36</version> </dependency> </dependencies> </project>版本号以你本地仓库能拉到的稳定版为准,上面是我测试时的版本。
6.3 编写连接管理类
连接管理类负责创建 Jedis 连接。生产环境要用连接池,这里先演示直连。
// 文件路径:src/main/java/com/example/redis/RedisConnection.java import redis.clients.jedis.Jedis; public class RedisConnection { public static Jedis getJedis() { String host = "127.0.0.1"; int port = 6379; String password = "your-strong-password"; Jedis jedis = new Jedis(host, port); if (password != null && !password.isEmpty()) { jedis.auth(password); } return jedis; } }如果你的 Redis 没有设置密码,可以去掉 auth 调用。这里提醒一点:即使只是本机学习,也建议设置密码并限制 bind 地址,避免 Redis 暴露到公网。
6.4 编写缓存服务类
缓存服务类模拟一个典型场景:查询商品信息时先查 Redis,没有则查数据库,并回填缓存。
// 文件路径:src/main/java/com/example/redis/CacheService.java import redis.clients.jedis.Jedis; public class CacheService { private static final String PRODUCT_CACHE_KEY_PREFIX = "product:info:"; /** * 模拟从数据库查询商品信息 */ private String queryFromDatabase(String productId) { System.out.println("查询数据库,productId = " + productId); return "{\"id\":" + productId + ",\"name\":\"示例商品\",\"price\":99.9}"; } /** * 查询商品信息:先查缓存,再查数据库,最后回填 */ public String getProductInfo(String productId) { try (Jedis jedis = RedisConnection.getJedis()) { String cacheKey = PRODUCT_CACHE_KEY_PREFIX + productId; String cached = jedis.get(cacheKey); if (cached != null) { System.out.println("命中缓存"); return cached; } String dbData = queryFromDatabase(productId); jedis.setex(cacheKey, 300, dbData); System.out.println("未命中缓存,已回填,TTL = 300s"); return dbData; } } }这里使用 setex 在设置 value 的同时指定过期时间,避免先 set 再 expire 两步操作中途失败导致的“永不过期”问题。
6.5 编写启动类并验证
// 文件路径:src/main/java/com/example/redis/CacheApplication.java public class CacheApplication { public static void main(String[] args) { CacheService cacheService = new CacheService(); String productId = "1001"; System.out.println("第一次查询:"); System.out.println(cacheService.getProductInfo(productId)); System.out.println("第二次查询:"); System.out.println(cacheService.getProductInfo(productId)); } }用 Maven 运行:
mvn clean compile exec:java -Dexec.mainClass="com.example.redis.CacheApplication"预期输出(带密码的记得先调整 RedisConnection):
第一次查询: 查询数据库,productId = 1001 未命中缓存,已回填,TTL = 300s {"id":1001,"name":"示例商品","price":99.9} 第二次查询: 命中缓存 {"id":1001,"name":"示例商品","price":99.9}因为这个 demo 的 queryFromDatabase 是本地方法,第二次输出仍然会打印“命中缓存”。如果在真实项目中,第二次查询不会访问数据库,这样可以显著降低数据库压力。
6.6 缓存穿透、击穿、雪崩的简单模拟
这三个概念是所有 Redis 缓存开发者必须掌握的。这里先快速说明,后面常见问题部分会给出完整排查。
- 缓存穿透:查询一个不存在的 key,Redis 没有,数据库也没有,请求每次都打到数据库,形成穿透。
- 缓存击穿:一个热点 key 过期,恰好大量请求同时访问,数据库瞬间被打满。
- 缓存雪崩:大量 key 在同一时刻过期,导致数据库压力骤增。
解决思路分别是:
- 穿透:对空结果也做缓存,但 TTL 设置短一些;使用布隆过滤器过滤不存在的 key。
- 击穿:热点 key 不设置过期时间,或者使用互斥锁保证只有一个请求回填缓存。
- 雪崩:过期时间加随机值,避免同时过期;使用多级缓存;热点数据预加载。
这三个问题也是 Redis 面试题中的高频点,值得单独写代码验证。
6.7 Jedis 连接池改造
直连模式适合学习,真实项目必须使用连接池。Jedis 推荐 JedisPool,可以复用连接,降低创建销毁开销。
// 文件路径:src/main/java/com/example/redis/RedisPool.java import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisPool { private static final JedisPool POOL; static { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(50); config.setMaxIdle(20); config.setMinIdle(5); config.setMaxWaitMillis(3000); config.setTestOnBorrow(true); String host = "127.0.0.1"; int port = 6379; String password = "your-strong-password"; if (password != null && !password.isEmpty()) { POOL = new JedisPool(config, host, port, 2000, password); } else { POOL = new JedisPool(config, host, port, 2000); } } public static Jedis getJedis() { return POOL.getResource(); } }注意连接池参数要结合业务设置,不是越大越好。最大连接数过大会导致 Redis 端维持大量空闲连接,占用资源。
7. 常见问题与排查思路
7.1 常见问题汇总表
下面整理了 Redis 使用中的高频问题,开发者可以对照排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 启动报 “Could not create server TCP listening socket *:6379: bind: Address already in use” | 端口被占用 | 检查进程,kill 或换端口 |
| 启动后外部无法连接 | bind 只配置了 127.0.0.1,或防火墙没放行 | 按需修改 bind、配置安全组/防火墙 |
| AUTH 认证失败 | 客户端密码与服务端 requirepass 不一致 | 检查 redis.conf 和客户端参数 |
| 使用 KEYS 命令导致 Redis 卡顿 | 大量 key 遍历,阻塞主线程 | 使用 SCAN 命令分批遍历 |
| 热点 key 过期瞬间数据库被压垮 | 缓存击穿 | 互斥锁重建缓存;热点 key 不设置过期 |
| 大量 key 同一时间过期 | 缓存雪崩 | 过期时间加随机值 |
| 缓存里查询不到、数据库也没有 | 缓存穿透 | 空值缓存、布隆过滤器 |
| 设置 key 后没有自动过期 | 使用了 SET 而不是 SETEX,或 EXPIRE 失败 | 使用 setex / set + expire,并检查返回值 |
| 分布式锁偶尔失效 | 忘记设置过期时间、释放锁误删 | 使用 SET NX EX,释放时校验 value |
| Redis 内存占用高,但没有多少数据 | 大 key 或过期 key 未清理 | 用 redis-cli --bigkeys 分析,优化 key |
| 使用可视化工具连接超时 | 密码错误、网络不通、bind 限制 | 先用 redis-cli 命令行验证 |
| 客户端大量 TIME_WAIT | 每次请求都新建连接 | 使用连接池 |
| 消息队列重复消费 | 没有做消费确认或代码重复提交 | 使用 Stream + ACK,业务侧做幂等 |
7.2 慢查询排查
Redis 提供了慢查询日志。在 redis-cli 中执行:
# 查看慢查询日志 SLOWLOG GET 10 # 查看慢查询阈值 CONFIG GET slowlog-log-slower-than # 设置阈值,单位微秒 CONFIG SET slowlog-log-slower-than 10000如果发现很多命令超过 10ms,需要进一步分析。常见的慢命令包括 KEYS、HGETALL、SMEMBERS、ZRANGEBYSCORE 等。建议使用 SCAN 代替 KEYS,使用 HSCAN/SSCAN/ZSCAN 代替全量获取。
7.3 大 key 排查
大 key 会造成内存分配、网络传输、持久化阻塞。可以使用官方命令:
redis-cli --bigkeys这个命令会遍历 Redis 并输出各种类型中最大的 key。发现大 key 后,可以考虑拆分 key、压缩 value、使用 List 分段存储等方式优化。
7.4 Redis Desktop Manager 连接不上的排查步骤
如果你使用 Redis Desktop Manager 或 Another Redis Desktop Manager 连接失败,按下面顺序排查:
- 确认 Redis 进程运行中:ps -ef | grep redis。
- 确认端口开放:ss -lntp | grep 6379。
- 确认 bind 配置允许远程访问(测试环境可先设为 0.0.0.0,生产环境要谨慎)。
- 确认密码配置:requirepass 与客户端输入的密码一致。
- 检查防火墙和云安全组是否放行 6379 端口。
- 在 Redis 所在机器上执行 redis-cli ping,确认服务本身正常。
记住一个原则:先命令行验证,再用工具排错,不要一开始就怀疑图形客户端。
8. 最佳实践与工程建议
8.1 key 命名规范
Redis key 建议使用业务前缀加冒号分层,比如:
user:info:1001 order:list:20250101 product:detail:sku12345这样做的好处有三个:可读性好;可以按前缀分组搜索;便于区分不同业务的数据。
但要注意不要滥用冒号结构,Redis 的 key 没有层级概念,冒号只是约定。
8.2 连接池参数设置
JedisPool 参数没有标准答案,需要根据 QPS、Redis 实例资源、业务响应时间调整。下面是一些经验值:
- maxTotal:按业务需要的峰值连接数估算,不是越大越好。
- maxIdle:保留空闲连接数,避免频繁创建销毁。
- minIdle:低峰期保留的最小连接数。
- maxWaitMillis:获取连接的最大等待时间,防止线程无限阻塞。
- testOnBorrow:获取连接时做 ping 检测,牺牲一点点性能换取连接可用性。
8.3 缓存一致性方案
缓存和数据库的一致性是老问题。没有一种方案能完美解决所有场景,需要根据业务容忍度选择。
常见的做法是 Cache Aside Pattern:
- 读操作:先读缓存,不命中则读数据库,回填缓存。
- 写操作:先更新数据库,再删除缓存。
关于“先删缓存再更新数据库”和“先更新数据库再删缓存”哪个更好,业界通常更推荐“先更新数据库,再删除缓存”。原因是:写操作后缓存中的旧值已经失效,删除成本低;而如果先删缓存再更新数据库,在更新数据库期间可能有请求把旧数据回填到缓存,造成脏数据。
删除缓存时如果删除失败,可以引入重试机制,或者使用 Binlog 监听(如 Canal)异步删除。
8.4 安全配置要求
生产环境的 Redis 不允许裸奔。至少要做到以下几点:
- 设置强密码,禁止空密码。
- bind 配置只监听内网或本机 IP。
- 禁用危险命令:CONFIG、FLUSHALL、FLUSHDB、KEYS 等,通过 rename-command 重命名。
- 使用非 root 用户运行 Redis 服务。
- 定期备份 RDB 或 AOF 文件。
- 监控内存、CPU、连接数、慢查询。
下面是一个 rename 危险命令的配置示例:
rename-command CONFIG "" rename-command FLUSHALL "" rename-command FLUSHDB ""要注意:重命名命令后,客户端或可视化工具可能无法直接使用这些命令,需要同步调整。
8.5 持久化选型
Redis 持久化有两种主流方式:
- RDB:定期生成内存快照,适合备份、恢复快,但可能丢失最后一次快照后的数据。
- AOF:记录每次写命令,数据安全性高,但文件大、重放慢。
生产环境通常会同时开启 RDB 和 AOF,或根据业务选择。如果服务波动不大,可以使用 RDB;如果对数据一致性要求高,建议开启 AOF,并设置 appendfsync everysec。
在 redis.conf 中:
save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec需要理解:RDB 和 AOF 写入磁盘的操作会有性能消耗,配置需要结合业务容忍度调整。
8.6 监控与容量规划
Redis 不是无底洞。上线前要做好容量估算,比如单个 key 平均大小、key 数量、过期时间、峰值并发量。运行中要监控:
- redis-cli INFO 里的 used_memory、connected_clients、total_commands_processed。
- 慢查询数量。
- 命中率。
- 每秒操作数。
推荐使用官方工具 redis-cli 或可视化监控面板。生产环境更推荐 Prometheus + Redis Exporter 做指标采集,配合 Grafana 展示,这样能提前发现容量和性能问题。
9. 总结与学习路线
写到这里,我们可以回答标题的问题了:Redis 之所以火,不只是因为它是一个“缓存工具”,而是因为它通过纯内存存储和精心设计的数据结构获得了极致性能;通过单线程事件循环获得了高并发下的稳定性;通过丰富的数据类型和原子操作,把一个中间件变成了能覆盖缓存、锁、队列、计数、排行榜等众多场景的通用组件。
如果要从零开始系统学习 Redis,建议按这个顺序走:
- 掌握常用命令:String、Hash、List、Set、ZSet 的基本操作。
- 理解过期与淘汰机制:EXPIRE、TTL、maxmemory、LRU。
- 理解持久化:RDB 与 AOF 的优劣。
- 学 Java 客户端:Jedis、Lettuce、Spring Data Redis。
- 学习集群方案:主从复制、哨兵、Cluster 分片。
- 研究经典场景:缓存穿透、击穿、雪崩,分布式锁,消息队列,延迟队列。
- 学会排查:慢查询、大 key、热 key、内存分析。
- 阅读源码(可选):事件循环、对象编码、RDB 文件格式。
Redis 是一个“越用越有意思”的组件。刚入门时,它只是一个方便的数据存储工具;当你在分布式场景里遇到并发、一致性、性能问题,再回头看 Redis 的设计,会理解它为什么在竞争激烈的中间件生态中始终占据重要席位。希望这篇教程能帮你打好基础,下一步可以亲手试试搭建 Redis 主从和哨兵集群,或者在本地用 Spring Boot 写一个带缓存和分布式锁的完整项目,真正把这些知识变成自己的实战经验。