1. 引言
在互联网应用高速发展的今天,性能优化始终是开发者绕不开的核心话题。而在众多优化手段中,缓存无疑是最直接、最有效的一环。Redis 作为一款开源的高性能键值对存储系统,凭借其丰富的数据结构、极致的读写速度和灵活的持久化策略,已经成为业界缓存方案的事实标准。
本文将带你系统性地认识 Redis 缓存:从它的核心特性、常见数据结构,到缓存穿透、缓存击穿、缓存雪崩等经典问题的成因与解决方案,再到与 Spring Boot 的整合实战,帮助你建立一套完整的 Redis 缓存知识体系。
2. Redis 是什么
Redis(Remote Dictionary Server,远程字典服务)是一个基于内存的、支持多种数据结构的开源 NoSQL 数据库。它由意大利开发者 Salvatore Sanfilippo(网名 antirez)于 2009 年发布,目前由 Redis 社区和 Redis Ltd. 维护。
Redis 之所以被广泛用于缓存场景,主要得益于以下几个核心特性:
- 基于内存存储:数据读写全部在内存中完成,单线程模型下依然能轻松达到每秒十万级以上的读写吞吐。
- 丰富的数据结构:不仅支持 String,还支持 Hash、List、Set、ZSet、Bitmap、HyperLogLog、Geo 等,能够覆盖绝大多数业务场景。
- 单线程与多路复用:Redis 使用单线程配合 I/O 多路复用机制,避免了多线程上下文切换和锁竞争的开销,保证了操作的原子性和稳定性。
- 持久化能力:虽然主打缓存,但 Redis 也提供 RDB 和 AOF 两种持久化方案,可在重启后恢复数据。
- 高可用与分布式:通过主从复制、哨兵(Sentinel)和集群(Cluster)模式,Redis 可以构建高可用、可水平扩展的缓存架构。
3. Redis 的常见数据结构
Redis 之所以强大,很大程度上归功于它丰富的数据结构。理解每种结构的适用场景,是合理使用 Redis 缓存的前提。
3.1 String(字符串)
String 是 Redis 最基础的数据类型,可以存储字符串、整数或浮点数,最大容量为 512MB。它适用于缓存简单的键值对,如用户会话、配置项、计数器等。
bash
SET user:1001 "{\"name\":\"张三\",\"age\":25}"
GET user:1001
INCR page_view_count
3.2 Hash(哈希)
Hash 适合存储对象类型的数据,它相当于一个 field-value 的映射表。相比把整个对象序列化为 String,Hash 可以单独更新某个字段,节省网络开销。
bash
HSET user:1001 name "张三" age 25 city "北京"
HGET user:1001 name
HGETALL user:1001
3.3 List(列表)
List 是一个双向链表,支持从两端推入和弹出元素。常用于实现消息队列、最新动态列表、时间线等场景。
bash
LPUSH news:latest "新闻1" "新闻2" "新闻3"
LRANGE news:latest 0 9
3.4 Set(集合)
Set 是无序、去重的字符串集合,支持集合间的交、并、差运算。适合做标签系统、好友关系、共同关注等场景。
bash
SADD user:1001:tags "Java" "Redis" "Spring"
SADD user:1002:tags "Java" "MySQL"
SINTER user:1001:tags user:1002:tags
3.5 ZSet(有序集合)
ZSet 在 Set 的基础上为每个元素关联了一个分数(score),可以按分数排序。它非常适合排行榜、延时队列、优先级任务等场景。
bash
ZADD ranking 100 "用户A" 90 "用户B" 80 "用户C"
ZREVRANGE ranking 0 9 WITHSCORES
4. Redis 缓存的核心机制
4.1 缓存读写流程
一个典型的 Redis 缓存读写流程如下:
#mermaid-svg-VJD3WfswbpeIiFeJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VJD3WfswbpeIiFeJ .error-icon{fill:#552222;}#mermaid-svg-VJD3WfswbpeIiFeJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VJD3WfswbpeIiFeJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VJD3WfswbpeIiFeJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VJD3WfswbpeIiFeJ .marker.cross{stroke:#333333;}#mermaid-svg-VJD3WfswbpeIiFeJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VJD3WfswbpeIiFeJ p{margin:0;}#mermaid-svg-VJD3WfswbpeIiFeJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster-label text{fill:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster-label span{color:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster-label span p{background-color:transparent;}#mermaid-svg-VJD3WfswbpeIiFeJ .label text,#mermaid-svg-VJD3WfswbpeIiFeJ span{fill:#333;color:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ .node rect,#mermaid-svg-VJD3WfswbpeIiFeJ .node circle,#mermaid-svg-VJD3WfswbpeIiFeJ .node ellipse,#mermaid-svg-VJD3WfswbpeIiFeJ .node polygon,#mermaid-svg-VJD3WfswbpeIiFeJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VJD3WfswbpeIiFeJ .rough-node .label text,#mermaid-svg-VJD3WfswbpeIiFeJ .node .label text,#mermaid-svg-VJD3WfswbpeIiFeJ .image-shape .label,#mermaid-svg-VJD3WfswbpeIiFeJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-VJD3WfswbpeIiFeJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VJD3WfswbpeIiFeJ .rough-node .label,#mermaid-svg-VJD3WfswbpeIiFeJ .node .label,#mermaid-svg-VJD3WfswbpeIiFeJ .image-shape .label,#mermaid-svg-VJD3WfswbpeIiFeJ .icon-shape .label{text-align:center;}#mermaid-svg-VJD3WfswbpeIiFeJ .node.clickable{cursor:pointer;}#mermaid-svg-VJD3WfswbpeIiFeJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VJD3WfswbpeIiFeJ .arrowheadPath{fill:#333333;}#mermaid-svg-VJD3WfswbpeIiFeJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VJD3WfswbpeIiFeJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VJD3WfswbpeIiFeJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VJD3WfswbpeIiFeJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VJD3WfswbpeIiFeJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VJD3WfswbpeIiFeJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster text{fill:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ .cluster span{color:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VJD3WfswbpeIiFeJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VJD3WfswbpeIiFeJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-VJD3WfswbpeIiFeJ .icon-shape,#mermaid-svg-VJD3WfswbpeIiFeJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VJD3WfswbpeIiFeJ .icon-shape p,#mermaid-svg-VJD3WfswbpeIiFeJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VJD3WfswbpeIiFeJ .icon-shape .label rect,#mermaid-svg-VJD3WfswbpeIiFeJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VJD3WfswbpeIiFeJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VJD3WfswbpeIiFeJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VJD3WfswbpeIiFeJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
客户端发起请求
缓存中是否有数据?
直接返回缓存数据
查询数据库
将结果写入 Redis 缓存
设置过期时间 TTL
4.2 过期策略
Redis 对设置了过期时间的键采用两种删除策略:
- 惰性删除:当某个键被访问时,才检查它是否过期,过期则删除。这种策略节省 CPU,但可能造成过期数据残留。
- 定期删除:Redis 每隔一段时间(默认 100ms)随机抽取一部分设置了过期时间的键进行检查,删除其中已过期的键。这种策略兼顾 CPU 与内存。
两者结合,Redis 在大多数场景下能及时清理过期数据,同时避免频繁扫描带来的性能损耗。
4.3 内存淘汰策略
当 Redis 内存达到上限(maxmemory)时,会根据配置的淘汰策略处理新写入的数据。常见策略包括:
noeviction:不淘汰,直接返回写入错误。allkeys-lru:从所有键中淘汰最近最少使用的键。volatile-lru:从设置了过期时间的键中淘汰最近最少使用的键。allkeys-random:从所有键中随机淘汰。volatile-ttl:从设置了过期时间的键中淘汰剩余存活时间最短的键。
生产环境通常选择 allkeys-lru 或 volatile-lru,以在内存有限的情况下保留最热的数据。
5. 缓存经典问题与解决方案
5.1 缓存穿透
问题描述:查询一个数据库中不存在的数据,缓存中自然也没有,请求直接打到数据库。恶意攻击者可以利用这一点,用大量不存在的 key 打垮数据库。
解决方案:
- 缓存空值:即使数据库查询结果为空,也把空结果缓存起来,并设置较短的过期时间(如 5 分钟)。
- 布隆过滤器:在缓存之前加一层布隆过滤器,快速判断 key 是否可能存在,不存在则直接拦截。
java
// 缓存空值示例
Object value = redis.get(key);
if (value == null) {
value = db.query(key);
if (value == null) {
redis.set(key, "", 300); // 缓存空值 5 分钟
} else {
redis.set(key, value, 3600);
}
}
5.2 缓存击穿
问题描述:某个热点 key 在过期瞬间,大量并发请求同时发现缓存失效,全部打到数据库,导致数据库压力骤增甚至宕机。
解决方案:
- 互斥锁(Mutex):当缓存失效时,只允许一个线程去查询数据库并重建缓存,其他线程等待。
- 逻辑过期:不给 key 设置物理过期时间,而是在 value 中记录逻辑过期时间,后台异步刷新。
java
// 互斥锁示例
String value = redis.get(key);
if (value == null) {
if (redis.setnx(lockKey, "1", 10)) { // 获取锁
try {
value = db.query(key);
redis.set(key, value, 3600);
} finally {
redis.del(lockKey);
}
} else {
Thread.sleep(100);
value = redis.get(key); // 重试获取
}
}
5.3 缓存雪崩
问题描述:大量 key 在同一时间集中过期,或者 Redis 服务宕机,导致大量请求直接打到数据库,引发连锁故障。
解决方案:
- 过期时间随机化:为每个 key 的过期时间加上一个随机值,避免集中失效。
- 多级缓存:在 Redis 之上再加一层本地缓存(如 Caffeine),即使 Redis 不可用,本地缓存仍能扛住一部分流量。
- Redis 高可用:通过哨兵或集群模式保证 Redis 服务不宕机。
- 服务降级与限流:在数据库层做好熔断、限流,保护核心业务。
java
// 过期时间随机化示例
int baseExpire = 3600;
int randomExpire = baseExpire + new Random().nextInt(600); // 加 0~10 分钟随机值
redis.set(key, value, randomExpire);
6. Redis 持久化机制
虽然 Redis 定位是缓存,但在某些场景下我们也希望重启后数据不丢失。Redis 提供了两种持久化方式:
6.1 RDB(快照)
RDB 通过定期生成内存数据的二进制快照文件(dump.rdb)来实现持久化。优点是文件紧凑、恢复速度快;缺点是可能丢失最后一次快照之后的数据。
bash
# 配置示例:60 秒内如果有 1000 次写操作则触发快照
save 60 1000
6.2 AOF(追加文件)
AOF 以日志形式记录每次写操作,重启时通过重放日志恢复数据。优点是数据安全性高(可配置每秒同步);缺点是文件体积较大,恢复速度相对较慢。
bash
# 配置示例:每秒同步一次
appendfsync everysec
生产环境常采用 RDB + AOF 混合持久化,兼顾恢复速度与数据安全。
7. Spring Boot 整合 Redis 实战
7.1 引入依赖
xml
<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>
7.2 配置连接
yaml
spring:
redis:
host: localhost
port: 6379
password:
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
7.3 使用 RedisTemplate 操作缓存
java
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String USER_KEY_PREFIX = "user:";
public User getUserById(Long id) {
String key = USER_KEY_PREFIX + id;
// 1. 先查缓存
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, User.class);
}
// 2. 缓存未命中,查数据库
User user = userMapper.selectById(id);
if (user != null) {
// 3. 写入缓存,设置过期时间
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
}
return user;
}
public void updateUser(User user) {
userMapper.updateById(user);
// 更新数据库后删除缓存,保证一致性
redisTemplate.delete(USER_KEY_PREFIX + user.getId());
}
}
7.4 使用注解式缓存
Spring 提供了 @Cacheable、@CachePut、@CacheEvict 等注解,可以更简洁地实现缓存逻辑。
java
@Service
public class ProductService {
@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
@CacheEvict(value = "product", key = "#product.id")
public void updateProduct(Product product) {
productMapper.updateById(product);
}
}
8. 缓存与数据库一致性
缓存与数据库的数据一致性是分布式系统中的经典难题。常见的策略有:
- Cache Aside(旁路缓存):读时先读缓存,未命中则读库并回填;写时先更新数据库,再删除缓存。这是最常用的模式,适合读多写少的场景。
- Read Through:缓存层负责从数据库加载数据,应用层只与缓存交互。
- Write Through:写数据时同时更新缓存和数据库,保证强一致,但性能开销较大。
- Write Behind(异步写回):先更新缓存,异步批量写回数据库,性能最好但可能丢失数据。
对于大多数业务,Cache Aside + 删除缓存 已经足够。若对一致性要求极高,可引入消息队列或 Canal 监听数据库 binlog 来异步刷新缓存。
9. 总结
Redis 缓存是提升系统性能的利器,但要用好它,需要理解其数据结构、过期与淘汰机制,并针对缓存穿透、击穿、雪崩等经典问题做好防御设计。同时,缓存与数据库的一致性、持久化策略、高可用架构也是生产环境必须考虑的要素。
希望本文能帮助你建立起 Redis 缓存的完整知识框架。在实际项目中,建议从简单的 Cache Aside 模式入手,逐步引入布隆过滤器、多级缓存、集群部署等进阶方案,让缓存真正成为系统性能的加速器,而不是新的瓶颈。