缓存基础常见知识点总结
Java 缓存基础常见知识点总结
什么是缓存,为什么需要缓存?
1. 什么是缓存?
缓存(Cache)是一种高效的数据存储机制,它用于临时存储高频访问的数据,以减少数据读取的时间,提高系统性能。缓存通常存放在内存中,比从磁盘或数据库获取数据要快得多。
2. 为什么需要缓存?
缓存的主要目的是提高访问速度,减少系统的负载,优化性能,具体优势包括:
- 减少数据库查询压力:避免重复查询数据库,提高数据库吞吐量。
- 提升系统响应速度:数据存储在内存中,访问速度比磁盘或远程数据库快几个数量级。
- 降低服务器资源消耗:减少 CPU、IO 和网络带宽的占用。
- 优化用户体验:减少请求延迟,提高系统的并发能力。
- 支持高并发:减少后端服务器的负担,提升系统可扩展性。
3. 适用场景
- 热点数据访问:如热门商品信息、新闻文章、排行榜等,减少数据库查询。
- 会话存储:存储用户的登录信息,如
Session
或Token
。 - 页面渲染:如前端
HTML
片段缓存,减少动态生成页面的时间。 - 分布式系统中的数据共享:不同服务之间共享数据,减少数据库查询。
缓存是高性能系统架构中不可缺少的重要部分,但同时也需要考虑缓存一致性、数据过期策略、缓存击穿、缓存穿透等问题,以避免带来新的挑战。
缓存有哪些常见的应用场景?
缓存的应用场景非常广泛,主要用于提高数据访问速度、减少数据库压力、提升系统性能。以下是一些常见的应用场景:
1. 数据库查询缓存
- 场景:查询数据库数据的操作通常较慢,可以将热点数据缓存到内存中,减少数据库查询压力。
- 示例:电商平台的商品详情页,商品信息存入 Redis,避免频繁访问数据库。
2. 页面缓存(HTML 缓存)
- 场景:对于访问量较大的静态页面,可以将其缓存,以减少服务器的渲染负担。
- 示例:新闻网站的首页、商品详情页使用 CDN + 缓存技术,提高加载速度。
3. 会话缓存(Session Cache)
- 场景:存储用户会话信息,避免频繁访问数据库。
- 示例:用户登录信息存入 Redis,避免每次都查询数据库进行身份验证。
4. 分布式缓存
- 场景:在微服务架构下,多个服务共享数据时,可使用分布式缓存来存储数据,提高访问速度。
- 示例:电商系统中的购物车信息存入 Redis,让不同服务可以快速获取数据。
5. 缓存热点数据
- 场景:某些数据访问频率极高,可以将其缓存在内存中,减少数据库或磁盘的访问次数。
- 示例:微博、抖音等热点话题榜单存入 Redis,避免实时计算消耗资源。
6. 消息队列缓存
- 场景:消息队列(如 Kafka、RabbitMQ)需要临时存储数据,保证数据的可靠性。
- 示例:秒杀系统中,用户请求先进入消息队列,避免瞬时流量冲击数据库。
7. 访问计数器
- 场景:统计页面浏览量、点赞数等场景,使用缓存避免数据库频繁写入。
- 示例:视频网站的播放次数、文章阅读量存入 Redis,并定期同步到数据库。
8. 限流和分布式锁
- 场景:高并发场景下,缓存可用于实现分布式限流和分布式锁,防止系统崩溃。
- 示例:使用 Redis 实现秒杀业务的限流,防止恶意刷单。
9. 配置和字典数据缓存
- 场景:存储不常变化的全局配置,如应用配置、权限信息等,避免频繁查询数据库。
- 示例:字典表、用户权限信息存入 Redis,避免每次请求时查询数据库。
10. 分布式 Session 共享
- 场景:分布式系统中,每个请求可能会落在不同的服务器上,需要共享 Session 数据。
- 示例:将用户 Session 存储在 Redis,使所有服务器都可以访问用户会话数据。
这些应用场景说明了缓存在高并发、大流量的系统中起到了关键作用,可以大幅提升系统性能、降低数据库负载、优化用户体验。
Java 中有哪些常见的缓存技术和框架?
在 Java 开发中,缓存技术和框架可以分为本地缓存和分布式缓存两大类。
1. 本地缓存(JVM 内存缓存)
本地缓存存储在 Java 应用的内存(Heap)中,适用于单机应用或小规模系统。
① ConcurrentHashMap
- 简介:Java 自带的线程安全
Map
,可以用作简单的本地缓存。 - 特点:
- 适用于小型缓存场景。
- 不支持自动过期、持久化、分布式存储。
- 应用场景:
- 小规模项目的缓存,例如配置数据缓存。
② Caffeine
- 简介:Google 开发的高性能本地缓存库,比 Guava Cache 更高效。
- 特点:
- 支持自动过期、LRU 淘汰策略、异步加载等。
- 读取性能比 Guava Cache 更快。
- 应用场景:
- 本地热点数据缓存,如短时间内频繁访问的数据。
③ Guava Cache
- 简介:Google 提供的缓存库,支持 LRU 淘汰策略。
- 特点:
- 轻量级,支持过期时间和大小限制。
- 应用场景:
- 适用于 Web 应用的小型缓存,如接口请求缓存。
④ Ehcache
- 简介:Java 领域最常用的缓存框架之一,支持持久化、本地和分布式缓存。
- 特点:
- 支持 内存 + 磁盘 存储,持久化数据。
- 兼容 Spring Cache,支持事务管理。
- 应用场景:
- 适用于 Spring Boot 项目,存储页面缓存、查询结果等。
2. 分布式缓存
分布式缓存可以跨多个服务器共享数据,适用于高并发、大流量系统。
⑤ Redis
- 简介:最流行的分布式缓存,支持多种数据结构(String、List、Set、Hash、ZSet)。
- 特点:
- 单线程模型,高性能。
- 支持持久化(RDB、AOF)。
- 支持分布式、集群部署。
- 应用场景:
- 数据库查询缓存、热点数据存储、分布式锁、排行榜、Session 共享等。
⑥ Memcached
- 简介:早期的高性能分布式缓存,主要存储 Key-Value 数据。
- 特点:
- 仅支持
String
类型,不支持持久化。 - 适用于缓存小数据,读取性能高于 Redis。
- 仅支持
- 应用场景:
- 短时缓存,如临时存储热点数据、页面缓存。
⑦ Tair
- 简介:阿里巴巴开发的分布式缓存系统,兼容 Redis,提供更强的高可用性。
- 特点:
- 高可用,支持 冷热数据分离,比 Redis 更适合大规模数据存储。
- 应用场景:
- 适用于电商、高并发场景(如淘宝、天猫)。
3. 组合方案
可以结合本地缓存和分布式缓存,实现多级缓存架构:
- Caffeine + Redis:本地缓存 + 分布式缓存,提高性能,减少 Redis 访问压力。
- Ehcache + Redis:支持本地缓存持久化,同时保证分布式缓存能力。
4. Spring 生态中的缓存技术
Spring 提供了 Spring Cache 作为缓存抽象层,支持多个缓存实现:
@Cacheable
:方法级缓存,减少数据库访问。- 兼容的缓存框架:
Ehcache
Redis
Caffeine
Guava Cache
总结
缓存技术 | 类型 | 适用场景 | 是否支持持久化 | 是否分布式 |
---|---|---|---|---|
ConcurrentHashMap | 本地 | 小型缓存 | ❌ | ❌ |
Caffeine | 本地 | 高频访问缓存 | ❌ | ❌ |
Guava Cache | 本地 | 轻量级缓存 | ❌ | ❌ |
Ehcache | 本地 | 本地持久化缓存 | ✅ | ❌ |
Redis | 分布式 | 高并发缓存、热点数据 | ✅ | ✅ |
Memcached | 分布式 | 轻量级缓存 | ❌ | ✅ |
Tair | 分布式 | 阿里云高可用缓存 | ✅ | ✅ |
- 小型应用:使用
Guava Cache
、Caffeine
。 - 分布式缓存:首选
Redis
,其次Memcached
。 - 持久化需求:
Redis
、Ehcache
、Tair
。 - Spring 项目:
Spring Cache + Redis/Ehcache
。
选择合适的缓存框架,可以大幅提升 Java 应用的性能。
缓存的常见存储方式有哪些?
缓存存储方式可以根据存储介质和数据分布模式进行分类,常见的存储方式如下:
1. 按存储介质分类
① 内存缓存(Memory Cache)
- 存储位置:数据存储在 RAM(随机存取存储器)中。
- 优点:
- 访问速度极快,读取/写入延迟通常在纳秒级或微秒级。
- 适用于高并发、高吞吐量场景。
- 缺点:
- 受限于内存容量,缓存数据量较小。
- 断电或系统重启后数据丢失(除非支持持久化)。
- 代表技术:
- 本地缓存:
ConcurrentHashMap
、Caffeine
、Guava Cache
- 分布式缓存:
Redis
、Memcached
- 本地缓存:
② 磁盘缓存(Disk Cache)
- 存储位置:数据存储在磁盘(HDD/SSD)中。
- 优点:
- 数据持久化,不会因服务器重启而丢失。
- 存储容量较大,适用于大规模数据缓存。
- 缺点:
- 访问速度比内存缓存慢(SSD 较快,HDD 较慢)。
- 代表技术:
Ehcache
(支持磁盘缓存)RocksDB
(高性能嵌入式存储,Redis 可选支持 RocksDB)
③ 组合缓存(Memory + Disk Cache)
- 存储方式:数据先存入内存,超出一定阈值后写入磁盘。
- 优点:
- 结合内存和磁盘缓存的优势,既能提供高性能,又能持久化存储。
- 避免缓存超出内存限制导致的 OOM(Out of Memory)。
- 代表技术:
Ehcache
(支持内存 + 磁盘存储)Redis + RDB/AOF
(支持持久化的内存数据库)
2. 按数据分布方式分类
④ 本地缓存(Local Cache)
- 存储位置:数据存储在单台服务器的 JVM 内存中。
- 优点:
- 读取速度快,无需网络访问。
- 适用于小型应用或单机模式。
- 缺点:
- 无法共享数据,适用于单机应用,分布式环境中不适用。
- 服务器重启后数据丢失。
- 代表技术:
ConcurrentHashMap
Guava Cache
Caffeine
Ehcache
(单机模式)
⑤ 分布式缓存(Distributed Cache)
- 存储位置:数据存储在多个节点的内存中,可横向扩展。
- 优点:
- 支持多节点共享缓存,适用于分布式系统。
- 具备高可用能力,可避免单点故障。
- 适用于高并发和大数据量的场景。
- 缺点:
- 需要网络通信,访问延迟比本地缓存略高。
- 可能涉及数据一致性问题。
- 代表技术:
Redis
(集群模式)Memcached
Tair
(阿里巴巴分布式缓存)Hazelcast
(分布式 JVM 缓存)
3. 按缓存更新策略分类
⑥ 直接缓存(Write-Through)
- 数据存储方式:写入缓存的同时同步写入数据库。
- 优点:
- 数据一致性高,不容易产生脏数据。
- 缺点:
- 每次写入都涉及数据库操作,影响写入性能。
- 应用场景:
- 金融、电商系统,对数据一致性要求高的场景。
⑦ 延迟写缓存(Write-Behind)
- 数据存储方式:写入缓存后,异步写入数据库。
- 优点:
- 适合高并发写操作,减少数据库压力。
- 缺点:
- 可能会出现数据丢失风险(如果缓存未及时同步到数据库)。
- 应用场景:
- 订单系统、日志存储 等对数据一致性要求不高的场景。
⑧ 旁路缓存(Cache-Aside)
- 数据存储方式:
- 读取数据时,先查缓存,如果缓存未命中,则查询数据库,并将数据写入缓存。
- 更新数据时,先更新数据库,然后清理缓存(或设置短时间失效)。
- 优点:
- 适用于大多数场景,兼顾性能和一致性。
- 缺点:
- 需要额外处理缓存过期和数据同步问题。
- 应用场景:
- 微服务架构下的常见缓存方案,如
Redis + MySQL
。
- 微服务架构下的常见缓存方案,如
4. 按应用场景分类
⑨ CDN 缓存(Content Delivery Network)
- 存储位置:数据存储在 CDN 节点,减少服务器压力。
- 应用场景:
- 静态资源(CSS、JS、图片等)缓存,提高网站访问速度。
- 代表技术:Cloudflare、阿里云 CDN、AWS CloudFront
⑩ 浏览器缓存
- 存储位置:数据存储在浏览器端,如
LocalStorage
、SessionStorage
、Cookie
。 - 应用场景:
- 减少重复请求,优化用户体验。
- 适用于前端网页优化,如
HTML5
的localStorage
。
总结
缓存方式 | 存储位置 | 优点 | 缺点 | 代表技术 |
---|---|---|---|---|
内存缓存 | RAM | 访问速度快 | 容量受限,易丢失 | Redis、Memcached、Caffeine |
磁盘缓存 | 磁盘 | 持久化存储 | 访问速度较慢 | Ehcache、RocksDB |
本地缓存 | 单机 JVM | 读取速度快 | 不能跨服务共享 | ConcurrentHashMap、Guava Cache |
分布式缓存 | 多节点 | 共享缓存,支持高并发 | 需网络通信,数据一致性管理复杂 | Redis(集群)、Memcached |
CDN 缓存 | CDN 服务器 | 降低带宽占用 | 适用范围有限 | Cloudflare、阿里云 CDN |
浏览器缓存 | 用户浏览器 | 提高网页加载速度 | 安全性较低 | LocalStorage、SessionStorage |
最佳实践
- 小型项目:
Caffeine
或Guava Cache
(本地缓存)。 - 高并发、分布式系统:
Redis
(分布式缓存)。 - 大规模数据存储:
Redis + 持久化(AOF/RDB)
或Ehcache(磁盘 + 内存)
。 - 数据库前置缓存:采用Cache-Aside 模式(Redis + MySQL)。
- 静态资源优化:
CDN 缓存
(如 Cloudflare、阿里云 CDN)。
不同的缓存存储方式适用于不同场景,选择合适的方案可以提升系统性能并降低数据库压力。
什么是本地缓存和分布式缓存?它们的区别是什么?
1. 什么是本地缓存(Local Cache)?
本地缓存是存储在单个应用服务器 JVM 内存中的缓存,通常用于单机环境或低并发场景,可以减少数据库查询,提高系统响应速度。
本地缓存的特点
- 存储位置:存储在应用进程的 JVM 内存中。
- 访问速度:本地访问,无需网络通信,读取速度极快(纳秒级)。
- 适用场景:
- 适用于单机应用,如小型 Web 应用、配置缓存等。
- 适用于短期热点数据,如数据查询缓存。
- 缺点:
- 数据不共享:多台服务器之间无法共享缓存,每个服务器都有自己的缓存副本。
- 数据一致性问题:不同服务器可能缓存不同的数据版本,更新时可能出现不一致情况。
- 容量受限:受限于 JVM 内存大小,无法存储大量数据,可能导致 OOM(Out Of Memory)。
常见的本地缓存技术
缓存技术 | 说明 |
---|---|
ConcurrentHashMap | Java 内置线程安全 Map,可作为简单的缓存存储 |
Guava Cache | Google 提供的本地缓存框架,支持 LRU 淘汰策略 |
Caffeine | 性能优于 Guava Cache,支持自动过期、异步加载 |
Ehcache | 既可用于本地缓存,也支持磁盘存储和分布式缓存 |
2. 什么是分布式缓存(Distributed Cache)?
分布式缓存是存储在多个服务器节点上的缓存,可以多个应用实例共享数据,适用于高并发、大流量的分布式系统。
分布式缓存的特点
- 存储位置:数据存储在独立的缓存服务器集群中,不依赖应用服务器的内存。
- 访问速度:比本地缓存慢(因为需要网络通信),但仍然远快于数据库访问(微秒级)。
- 适用场景:
- 高并发系统(电商、社交、金融等)。
- 跨多个服务器共享缓存(如 Session 共享)。
- 缓存大规模数据(如排行榜、热点新闻)。
- 优点:
- 数据可共享:多个服务器都可以访问相同的缓存数据。
- 支持高可用:通常采用主从复制、分片、集群等机制,保证缓存服务的稳定性。
- 存储容量大:可以分布式存储海量数据,不受单机内存限制。
- 缺点:
- 网络开销:访问缓存时需要网络通信,延迟比本地缓存高。
- 数据一致性管理复杂:多个节点共享数据,更新数据时需要同步,可能存在缓存不一致问题。
常见的分布式缓存技术
缓存技术 | 说明 |
---|---|
Redis | 最流行的分布式缓存,支持持久化、集群、主从复制 |
Memcached | 轻量级缓存,支持分布式,但不支持持久化 |
Tair | 阿里巴巴自研的高性能分布式缓存 |
Hazelcast | 分布式内存缓存,支持集群 |
3. 本地缓存 vs. 分布式缓存
对比项 | 本地缓存(Local Cache) | 分布式缓存(Distributed Cache) |
---|---|---|
存储位置 | 存在应用服务器的 JVM 内存中 | 独立的缓存服务器或缓存集群 |
访问速度 | 极快(纳秒级) | 需要网络通信,较快(微秒级) |
数据共享 | 不能共享,每个服务器各有缓存副本 | 多个服务器可以共享数据 |
适用场景 | 单机应用、短期热点数据 | 高并发、分布式系统、大数据量存储 |
数据一致性 | 仅在单机中保证一致 | 可能存在缓存一致性问题,需要额外处理 |
扩展性 | 扩展能力有限,受 JVM 内存限制 | 可扩展,支持分布式集群 |
存储容量 | 受单机内存限制 | 可扩展到多个节点,支持 TB 级数据 |
可靠性 | 服务器重启缓存丢失 | 可通过持久化(Redis AOF、RDB)保证数据可靠性 |
示例技术 | ConcurrentHashMap、Guava Cache、Caffeine、Ehcache | Redis、Memcached、Tair、Hazelcast |
4. 何时使用本地缓存 vs. 分布式缓存?
场景 | 适合的缓存方案 |
---|---|
单机应用、小数据量、高速读取 | 本地缓存(Guava Cache、Caffeine) |
高并发、大数据量、分布式系统 | 分布式缓存(Redis、Memcached) |
热点数据+数据库缓存 | 组合方案(Caffeine + Redis) |
静态资源缓存(CSS、JS、图片) | CDN 缓存 |
短期查询缓存 | 本地缓存(Guava Cache、Caffeine) |
持久化缓存、排行榜、会话共享 | 分布式缓存(Redis) |
5. 组合使用本地缓存和分布式缓存
在实际项目中,可以结合本地缓存和分布式缓存,提高系统性能:
常见方案
二级缓存(L1 + L2)
- L1(本地缓存):Caffeine/Guava Cache
- L2(分布式缓存):Redis
- 示例:
- 先查询本地缓存,未命中时再查询 Redis。
- 通过
Cache-Aside
模式同步 Redis 数据。
Redis + MySQL
- 读操作:先查 Redis,缓存未命中则查询数据库。
- 写操作:先更新数据库,再删除缓存,避免缓存数据不一致。
总结
- 本地缓存(Local Cache):适用于小型应用、短期热点数据,速度极快,但数据不共享,容量受限。
- 分布式缓存(Distributed Cache):适用于高并发、分布式系统、大数据存储,可共享数据,但访问速度稍慢。
- 混合缓存架构:通常使用 本地缓存 + 分布式缓存,减少 Redis 访问压力,提高性能。
在实际开发中,应根据业务需求选择合适的缓存方案,本地缓存适用于低延迟访问,分布式缓存适用于高并发和数据共享场景。
本地缓存和数据库的同步更新常见策略
在分布式系统或高并发场景下,为了提高查询性能,通常会在应用层引入 本地缓存(如 Caffeine、Ehcache)来减少对数据库的直接访问。但是,缓存和数据库的同步更新是一个难题,常见的策略如下:
1. Cache-Aside(旁路缓存)
策略描述
应用程序先查询缓存,如果缓存未命中,则从数据库查询,并将数据写入缓存,供下次查询使用。
更新流程
读操作:
- 先查询缓存
- 如果命中,直接返回
- 如果未命中,从数据库查询,并写入缓存
写操作:
- 先更新数据库
- 然后删除缓存(而不是更新缓存)
优点
- 只在需要时才加载缓存,减少不必要的缓存占用
- 易于实现,适用于读多写少的场景
缺点
- 可能会出现短暂的缓存不一致(数据库更新后,缓存数据仍是旧的,直到被删除)
- 缓存击穿问题(热点数据刚被删除时,短时间内大量请求打到数据库)
2. Read-Through(读穿缓存)
策略描述
与 Cache-Aside 类似,但缓存管理器自动从数据库加载数据。
更新流程
读操作:
- 先查询缓存
- 如果未命中,缓存层从数据库加载数据,并返回给应用
写操作:
- 先更新数据库
- 再更新缓存
优点
- 业务代码无需操心缓存的存取逻辑
- 适用于读多写少的场景
缺点
- 额外的缓存组件管理逻辑,增加复杂性
- 写操作需要同时更新缓存,可能影响性能
3. Write-Through(写穿缓存)
策略描述
应用程序直接更新缓存,然后缓存管理器同步更新数据库。
更新流程
读操作:
- 直接从缓存读取
写操作:
- 先写入缓存
- 由缓存管理器负责同步到数据库
优点
- 保证缓存与数据库的一致性
- 适用于写频繁的场景
缺点
- 写操作速度受缓存影响,可能降低数据库写入吞吐量
- 依赖缓存管理器的可靠性
4. Write-Behind(异步写入)
策略描述
应用程序只更新缓存,由缓存组件异步批量写入数据库。
更新流程
读操作:
- 直接从缓存读取
写操作:
- 写入缓存
- 缓存异步将变更同步到数据库(批量执行)
优点
- 写操作性能高,适用于写入密集型场景
- 减少数据库的写入压力
缺点
- 可能出现数据丢失(缓存未成功写入数据库时发生崩溃)
- 一致性较差(数据库的更新滞后)
5. Cache Consistency(缓存一致性方案)
为了减少缓存与数据库的不一致性,常见的策略有:
- 延时双删策略:
- 第一步:更新数据库后立即删除缓存
- 第二步:短暂延时后(如 500ms)再次删除缓存,防止并发问题
- MQ 消息同步:
- 通过消息队列(Kafka、RabbitMQ)同步缓存更新
- 适用于分布式场景
- 基于 Binlog 监听:
- MySQL 提供 Binlog,可以监听数据库变更并同步更新缓存
- 使用 Canal、Debezium 解析 Binlog,推送到 Redis 进行更新
总结
策略 | 读性能 | 写性能 | 数据一致性 | 适用场景 |
---|---|---|---|---|
Cache-Aside | 高 | 高 | 弱 | 读多写少,热点数据 |
Read-Through | 高 | 低 | 中 | 读多写少 |
Write-Through | 低 | 低 | 强 | 低延迟写 |
Write-Behind | 高 | 高 | 弱 | 写多场景 |
Binlog 同步 | 中 | 低 | 强 | 分布式数据库同步 |
如果你的系统读多写少,可以使用 Cache-Aside 或 Read-Through,如果对一致性要求高,可以用 Binlog+MQ 同步方案。
常见的缓存淘汰策略及适用场景
在高并发系统中,缓存通常无法无限制扩容,因此需要设置缓存淘汰策略,以确保高效利用缓存空间。常见的缓存淘汰策略如下:
1. FIFO(First In, First Out) - 先进先出
策略描述
- 先存入缓存的数据,先被淘汰。
- 类似队列,按插入时间顺序淘汰。
适用场景
- 适用于数据访问模式较稳定,并且最新数据往往最重要的场景。
- 例如 消息队列缓存,老数据优先淘汰。
缺点
- 可能淘汰仍然高频访问的旧数据,导致缓存命中率下降。
2. LRU(Least Recently Used) - 最近最少使用
策略描述
- 淘汰最近最少访问的数据(时间维度)。
- 维护一个访问时间排序的链表,每次访问某个缓存时,该缓存被移动到队尾,淘汰时删除队首的数据。
适用场景
- 适用于访问模式有明显热点,需要淘汰长期未访问数据的场景。
- 例如 网页缓存(最近访问的网页优先保留)。
缺点
- 如果有周期性访问的数据,可能会被不断淘汰后重新加载,增加缓存压力。
- 缓存污染问题:如果短时间内访问了大量一次性数据(如爬虫访问),LRU 可能会将真正的热点数据淘汰。
3. LFU(Least Frequently Used) - 最少使用
策略描述
- 淘汰访问频率最低的数据(次数维度)。
- 维护一个访问计数器,每次访问某个数据时,计数器+1,淘汰访问最少的数据。
适用场景
- 适用于数据访问模式稳定,访问频率高的数据应长期保留的场景。
- 例如 热点数据缓存(如热销商品、热门文章)。
缺点
- 新数据冷启动问题:新数据访问次数初始为 0,可能会被立即淘汰。
- 历史访问依赖问题:长期未访问但曾经访问过很多的数据,可能会一直占据缓存。
4. Random(随机淘汰)
策略描述
- 随机选择缓存中的一部分数据进行淘汰。
适用场景
- 适用于缓存命中率无特殊要求,但缓存更新较频繁的场景。
- 例如 临时数据缓存(如验证码、短期会话数据)。
缺点
- 命中率较低,不适用于访问模式稳定的场景。
5. TLFU(Tiny LFU,增强版 LFU)
策略描述
- 结合 LFU + Bloom Filter,解决 LFU 冷启动问题,确保短时间内的热点数据可以被快速纳入缓存。
适用场景
- 适用于 高吞吐、高并发场景,如 推荐系统、搜索引擎缓存。
缺点
- 需要额外的存储空间维护访问频次。
6. 先进先出 + 时间淘汰(TTL 过期策略)
策略描述
- 设置固定存活时间(TTL),数据存活超过 TTL 后自动淘汰。
适用场景
- 适用于数据时效性强的场景,例如:
- 验证码缓存(如短信验证码 5 分钟内有效)
- 会话缓存(用户登录态 30 分钟失效)
缺点
- 数据可能在仍然活跃时被删除,影响缓存命中率。
7. Redis 内置淘汰策略
Redis 提供了几种内置策略:
淘汰策略 | 说明 | 适用场景 |
---|---|---|
noeviction | 不淘汰数据,写满后返回错误 | 适用于数据必须持久存储 |
volatile-lru | 仅淘汰设置了 TTL的 key,使用 LRU | 适用于短期缓存(如 session) |
allkeys-lru | 在所有 key 中使用 LRU 淘汰 | 适用于大规模缓存 |
volatile-random | 随机淘汰设置了 TTL的 key | 适用于临时数据存储 |
allkeys-random | 在所有 key 中随机淘汰 | 适用于缓存访问模式不确定的情况 |
volatile-ttl | 只淘汰即将过期的 key | 适用于基于 TTL 过期的缓存 |
总结
策略 | 适用场景 | 适合的缓存数据类型 |
---|---|---|
FIFO | 数据有固定生命周期,如消息队列缓存 | 队列型数据 |
LRU | 热点数据访问明显,如热点网页缓存 | 页面缓存、数据库查询缓存 |
LFU | 热点数据长期稳定,如热门商品 | 商品、文章 |
Random | 低命中率数据,如短期缓存 | 验证码、临时数据 |
TTL | 时效性数据,如验证码、Token | 短期有效数据 |
如果你的系统主要是热点数据访问,可以选择 LRU 或 LFU;如果数据有时效性,可以使用 TTL 过期策略。
缓存雪崩及其解决方案
什么是缓存雪崩?
缓存雪崩是指大量缓存数据同时失效,导致所有请求直接打到数据库,造成数据库压力骤增,甚至导致数据库崩溃,系统不可用的情况。
缓存雪崩的常见原因
- 缓存服务器宕机(如 Redis 故障或网络异常)
- 大量数据同时过期(如设置了相同的 TTL 过期时间)
- 短时间内有大量请求(如秒杀、抢购活动)
如何避免缓存雪崩?
1. 数据过期时间分散(TTL 过期时间随机化)
- 问题:如果所有数据的 TTL 设为同一时间,例如
60s
,当数据同时过期,数据库压力会瞬间增大。 - 解决方案:
- 采用随机 TTL 机制,如:
int ttl = 60 + new Random().nextInt(30); // 60~90秒之间随机过期 redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
- 这样可以确保缓存数据不会集中失效,避免数据库瞬时压力过大。
- 采用随机 TTL 机制,如:
2. 设置热点数据永不过期(手动刷新)
- 问题:某些热点数据一旦过期,瞬间大量请求会直接打到数据库,可能引发雪崩。
- 解决方案:
- 设置热点数据永不过期,采用定期后台刷新的方式更新缓存:
redisTemplate.opsForValue().set("hot_data", data); // 不设置 TTL
- 由定时任务(Scheduled Task) 或 后台异步更新来手动刷新缓存。
- 设置热点数据永不过期,采用定期后台刷新的方式更新缓存:
3. 采用多级缓存架构
- 问题:Redis 宕机或重启,所有请求直达数据库,造成数据库压力过大。
- 解决方案:
- 采用 本地缓存 + 分布式缓存 的 多级缓存架构:
- 一级缓存(本地缓存):Ehcache、Caffeine 等
- 二级缓存(分布式缓存):Redis
- 先查询本地缓存,未命中再查询 Redis,Redis 失效时,数据库压力不会瞬间增加:
String value = caffeineCache.getIfPresent(key); if (value == null) { value = redisTemplate.opsForValue().get(key); if (value != null) { caffeineCache.put(key, value); } }
- 采用 本地缓存 + 分布式缓存 的 多级缓存架构:
4. 限流 + 降级
- 问题:高并发请求直接打数据库,造成数据库压力骤增。
- 解决方案:
- 限流:使用 令牌桶 或 漏桶算法 限制请求量:
if (!RateLimiter.tryAcquire()) { return "请求过多,请稍后重试"; }
- 降级:
- 访问量过大时,返回默认数据(如热点新闻返回昨日数据)
- 或者直接返回系统繁忙提示
- 限流:使用 令牌桶 或 漏桶算法 限制请求量:
5. Redis 持久化(RDB/AOF)+ 哨兵/集群
- 问题:Redis 宕机后,所有缓存数据丢失,导致大量请求直达数据库。
- 解决方案:
- 持久化:开启 Redis AOF/RDB 持久化,确保 Redis 重启后仍能恢复数据。
- 高可用方案:
- Redis 主从+哨兵(Sentinel),保证 Redis 宕机时可自动切换主节点
- Redis 集群模式(Cluster),分片存储数据,防止单点故障
总结
方法 | 解决问题 | 适用场景 |
---|---|---|
TTL 过期时间随机化 | 避免大量数据同时过期 | 适用于所有缓存场景 |
热点数据永不过期 | 避免热点数据过期引发雪崩 | 适用于高频访问数据 |
多级缓存(本地缓存 + Redis) | 缓解 Redis 故障影响 | 适用于分布式系统 |
限流 + 降级 | 保护数据库,防止高并发打垮 | 高并发应用 |
Redis 高可用(持久化 + 集群) | 防止 Redis 宕机导致数据丢失 | 关键业务场景 |
通过合理设置 TTL、使用多级缓存、限流降级和高可用 Redis,可以有效避免缓存雪崩,保障系统的稳定性。
缓存穿透及其解决方案
1. 什么是缓存穿透?
缓存穿透是指请求查询的数据在缓存和数据库中都不存在,导致所有请求直接打到数据库,造成数据库压力过大,甚至导致系统崩溃。
2. 发生原因
- 用户恶意攻击:大量请求数据库不存在的数据,导致缓存失效,每次请求都直接查询数据库。
- 缓存未存储空值:如果数据库查无此数据,而缓存未存储该查询结果,则下次相同请求仍然会直接访问数据库。
3. 解决方案
方案 1:缓存空值(推荐)
- 原理:当数据库查询返回
null
时,将null
值存入缓存,并设置短 TTL(如 5 分钟),防止频繁查询数据库。 - 示例代码:
String key = "user:10086"; String value = redisTemplate.opsForValue().get(key); if (value == null) { value = queryFromDatabase(10086); if (value == null) { redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES); // 缓存空值 } else { redisTemplate.opsForValue().set(key, value, 60, TimeUnit.MINUTES); // 正常缓存 } }
- 优点:
- 解决数据库查询不存在数据的高并发请求问题
- 降低数据库压力
方案 2:布隆过滤器(推荐)
原理:使用 Bloom Filter 作为黑名单,拦截不存在的数据请求,避免查询数据库。
示例代码(Guava 实现):
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000000); bloomFilter.put(10086); // 预存存在的数据ID if (!bloomFilter.mightContain(10086)) { return null; // 直接拦截 } String value = redisTemplate.opsForValue().get("user:10086");
优点:
- 内存占用低,可快速拦截大量不存在的数据
- 适用于ID 查询(如用户、商品)
方案 3:查询前增加权限校验
- 原理:对用户的查询行为进行身份校验或参数校验,限制非法请求访问数据库。
- 适用场景:
- 需要登录才能查询的数据(如用户订单信息)
- 需要特定权限才能访问的 API
- 示例代码:
if (!userHasPermission(userId)) { return "无权限访问"; }
方案 4:使用限流策略
- 原理:对相同的请求进行限流,例如:
- 短时间内同一个 IP 访问过多,直接拦截
- 对相同请求进行合并,减少数据库压力
- 示例代码(Guava 限流器):
RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒允许 10 个请求 if (!rateLimiter.tryAcquire()) { return "请求过多,请稍后再试"; }
4. 解决方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
缓存空值 | 适用于普通 KV 查询 | 简单有效,减少 DB 压力 | 可能存入大量空数据,占用缓存 |
布隆过滤器 | 适用于 ID 查询(如用户、商品) | 占用内存低,高效拦截 | 误判率存在,无法删除元素 |
身份/参数校验 | 需要权限的数据 | 防止恶意访问 | 适用范围有限 |
限流 | 高并发攻击 | 保护数据库,限制请求频率 | 可能影响正常用户 |
如果你的系统主要是查询不存在的 ID,建议使用 布隆过滤器;如果查询普通 KV 数据,可以使用 缓存空值策略。
缓存击穿及其解决方案
1. 什么是缓存击穿?
缓存击穿是指某个热点缓存数据过期后,大量并发请求同时查询该数据,导致所有请求直接打到数据库,造成数据库压力骤增,甚至可能引发宕机。
2. 发生原因
- 高并发情况下,缓存数据刚好过期:如秒杀、热点新闻等场景,缓存的数据 TTL 到期后,大量用户同时访问数据库。
- 缓存数据未提前预热:大量访问的热点数据没有提前加载到缓存,导致每个请求都查询数据库。
3. 解决方案
方案 1:设置热点数据永不过期(手动更新)
- 原理:热点数据不设置 TTL 过期时间,而是由后台任务或定时任务主动更新缓存,防止缓存突然失效导致数据库压力骤增。
- 示例代码:
redisTemplate.opsForValue().set("hot_key", data); // 不设置 TTL
- 优点:
- 适用于热点数据固定的场景(如热门商品、排行榜)
- 数据不会突然过期,避免缓存击穿
- 缺点:
- 需要手动更新缓存,可能导致数据更新延迟
方案 2:互斥锁(分布式锁,防止并发击穿)
原理:当缓存过期时,只有一个线程能查询数据库,其余线程等待或返回默认值,防止大量并发请求同时查询数据库。
示例代码:
String key = "hot_key"; String value = redisTemplate.opsForValue().get(key); if (value == null) { // 缓存过期 boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:hot_key", "1", 10, TimeUnit.SECONDS); if (lock) { // 获取到锁的线程执行数据库查询 try { value = queryFromDatabase(); redisTemplate.opsForValue().set(key, value, 60, TimeUnit.SECONDS); } finally { redisTemplate.delete("lock:hot_key"); // 释放锁 } } else { // 其他线程等待一段时间后重试 Thread.sleep(100); value = redisTemplate.opsForValue().get(key); } } return value;
优点:
- 适用于高并发热点数据的查询
- 确保数据库不会被并发访问
缺点:
- 额外的锁机制会增加复杂度
- 可能会降低系统吞吐量
方案 3:异步预加载(缓存预热)
- 原理:提前将热点数据定时加载到缓存,防止缓存突然失效。
- 示例代码:
@Scheduled(fixedRate = 60000) // 每分钟更新一次缓存 public void refreshCache() { String hotData = queryFromDatabase(); redisTemplate.opsForValue().set("hot_key", hotData, 60, TimeUnit.SECONDS); }
- 优点:
- 适用于热点数据稳定的场景(如排行榜、商品详情页)
- 无额外锁机制,性能高
- 缺点:
- 如果数据不稳定,可能导致缓存数据不准确
方案 4:双重缓存(Double-Check 缓存)
原理:当缓存失效时,线程先查询缓存两次,防止多个线程同时访问数据库。
示例代码:
String key = "hot_key"; String value = redisTemplate.opsForValue().get(key); if (value == null) { // 缓存过期 synchronized (this) { // 加锁,保证只有一个线程访问数据库 value = redisTemplate.opsForValue().get(key); if (value == null) { value = queryFromDatabase(); redisTemplate.opsForValue().set(key, value, 60, TimeUnit.SECONDS); } } } return value;
优点:
- 适用于并发量中等的场景
- 逻辑简单,不需要额外的分布式锁
缺点:
- 低效(相比于分布式锁)
4. 解决方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
热点数据永不过期 | 适用于热点固定数据(如排行榜) | 无需额外操作,缓存命中率高 | 需要手动更新缓存 |
分布式锁 | 适用于高并发热点数据 | 保障数据库安全,防止同时查询 | 可能影响系统吞吐量 |
异步预加载 | 适用于数据更新周期固定 | 高效无锁,避免击穿 | 适用于稳定热点数据 |
双重缓存检查 | 适用于中等并发查询 | 逻辑简单,防止并发 | 低效,不适用于超高并发 |
如果你的系统是热点数据查询,推荐使用 异步预加载;如果是高并发查询,可以使用 分布式锁 解决缓存击穿问题。
Redis 与 Memcached 的区别
对比项 | Redis | Memcached |
---|---|---|
数据结构 | 支持多种数据结构(String、List、Set、ZSet、Hash、Bitmap、HyperLogLog) | 仅支持 Key-Value 存储(String) |
持久化 | 支持 RDB(快照)和 AOF(日志),可恢复数据 | 不支持持久化,断电数据丢失 |
分布式 | Redis Cluster 支持分布式存储,可自动分片 | 仅支持 客户端分片,不支持原生分布式 |
存储方式 | 内存 + 磁盘(持久化可选) | 纯内存 |
内存管理 | LRU 淘汰机制 + 定期回收机制 | LRU 淘汰机制 |
并发性能 | 单线程(但高效,支持 多路复用) | 多线程(适合高并发) |
数据一致性 | 支持 事务(MULTI/EXEC),但不支持强一致性 | 不支持事务 |
使用场景 | 适用于 缓存、队列、排行榜、计数器、分布式锁 | 适用于 高性能 KV 缓存 |
Redis 适用场景
- 分布式锁:使用
SETNX
+ 过期时间实现 互斥锁。 - 消息队列:利用 List、Pub/Sub 实现生产者-消费者模型。
- 排行榜:使用 SortedSet(ZSet) 存储排名数据。
- 计数器:使用 INCR、DECR 进行高效计数。
- 分布式 Session:存储 用户 Session 信息,支持持久化。
Memcached 适用场景
- 纯 Key-Value 缓存:适用于 网页缓存、数据库查询缓存。
- 临时数据存储:无需持久化的 短期缓存数据。
总结
适用场景 | 选择 Redis | 选择 Memcached |
---|---|---|
需要持久化数据 | ✅ | ❌ |
需要复杂数据结构(List、Set、ZSet) | ✅ | ❌ |
需要分布式存储 | ✅ | ❌ |
追求高并发性能(多线程) | ❌ | ✅ |
仅作为简单 Key-Value 缓存 | ✅ | ✅ |
如果你需要高性能缓存且数据结构简单,可以选择 Memcached。但如果你需要更多功能(持久化、分布式、复杂数据结构),推荐使用 Redis。
Redis 主要数据结构及适用场景
数据结构 | 说明 | 适用场景 |
---|---|---|
String | 最基本的数据结构,存储字符串、整数、浮点数 | 缓存、计数器、分布式锁 |
List | 有序列表,基于双向链表,可从两端操作 | 消息队列、任务队列、文章列表 |
Hash | 键值对集合,类似于 Map(哈希表) | 用户信息、配置存储 |
Set | 无序集合,去重功能 | 标签、去重、随机抽奖 |
Sorted Set (ZSet) | 有序集合,基于分数排序 | 排行榜、权重排序、排名系统 |
Bitmap | 位存储结构,操作二进制位 | 签到统计、在线用户状态 |
HyperLogLog | 近似去重计数 | UV 统计、独立用户计数 |
Geo | 地理位置存储 | LBS(定位服务)、附近用户搜索 |
Stream | 消息队列(支持消费分组) | 日志存储、分布式消息队列 |
1. String(字符串)
- 特点:可以存储 普通字符串、整数、浮点数,最大支持 512MB。
- 常见操作:
redisTemplate.opsForValue().set("name", "Alice"); // 存储字符串 redisTemplate.opsForValue().get("name"); // 获取值 redisTemplate.opsForValue().increment("counter"); // 计数器 redisTemplate.opsForValue().set("key", "value", 10, TimeUnit.SECONDS); // 设置过期时间
- 适用场景:
- 缓存(如用户 Session、Token)
- 计数器(如网站访问量)
- 分布式锁(
SETNX
实现互斥锁)
2. List(列表)
- 特点:双向链表,支持 从头/尾部插入、删除。
- 常见操作:
redisTemplate.opsForList().leftPush("list", "A"); // 左侧插入 redisTemplate.opsForList().rightPush("list", "B"); // 右侧插入 redisTemplate.opsForList().leftPop("list"); // 弹出元素
- 适用场景:
- 消息队列(
BLPOP
/BRPOP
进行阻塞消费) - 文章评论列表
- 任务队列
- 消息队列(
3. Hash(哈希表)
- 特点:类似 Map,存储
field-value
结构,适用于存储对象。 - 常见操作:
redisTemplate.opsForHash().put("user:1001", "name", "Alice"); redisTemplate.opsForHash().get("user:1001", "name");
- 适用场景:
- 存储对象(如用户信息
user:{id}
) - 存储配置信息
- 存储对象(如用户信息
4. Set(集合)
- 特点:无序、去重,支持交集、并集、差集操作。
- 常见操作:
redisTemplate.opsForSet().add("tags", "Java", "Redis", "Spring"); redisTemplate.opsForSet().isMember("tags", "Java"); // 是否存在
- 适用场景:
- 用户标签系统
- 去重操作
- 随机抽奖(
SRANDMEMBER
)
5. Sorted Set(有序集合)
- 特点:每个元素都有一个分数,按照分数排序。
- 常见操作:
redisTemplate.opsForZSet().add("ranking", "Alice", 100); // 添加数据 redisTemplate.opsForZSet().add("ranking", "Bob", 200); redisTemplate.opsForZSet().reverseRangeWithScores("ranking", 0, 10); // 获取前 10 名
- 适用场景:
- 排行榜(游戏积分榜、热搜榜)
- 延迟队列(分数表示时间戳)
6. Bitmap(位存储)
- 特点:位存储,适用于海量数据存储 0/1 状态。
- 常见操作:
redisTemplate.opsForValue().setBit("user:sign", 1, true); // 标记用户签到 redisTemplate.opsForValue().getBit("user:sign", 1); // 查询用户是否签到
- 适用场景:
- 用户签到(1 表示签到,0 表示未签到)
- 在线状态(用户是否在线)
7. HyperLogLog(去重计数)
- 特点:统计大规模数据的基数,误差约 0.81%。
- 常见操作:
redisTemplate.opsForHyperLogLog().add("uv", "user1", "user2", "user3"); redisTemplate.opsForHyperLogLog().size("uv"); // 统计 UV 数量
- 适用场景:
- UV 统计(网站独立访客数)
- 去重计数(大数据场景)
8. Geo(地理位置)
- 特点:存储 经纬度 信息,可进行距离计算。
- 常见操作:
redisTemplate.opsForGeo().add("places", new Point(116.4, 39.9), "Beijing"); redisTemplate.opsForGeo().distance("places", "Beijing", "Shanghai"); // 计算两点距离
- 适用场景:
- LBS(定位服务)
- 查找附近的人
9. Stream(消息队列)
- 特点:可靠的消息队列,支持消费分组。
- 常见操作:
redisTemplate.opsForStream().add("orders", Map.of("id", "1001", "status", "paid"));
- 适用场景:
- 日志存储
- 分布式消息队列
总结
数据结构 | 适用场景 |
---|---|
String | 计数器、分布式锁、缓存 |
List | 消息队列、文章列表 |
Hash | 用户信息、配置信息 |
Set | 标签、去重、随机抽奖 |
Sorted Set (ZSet) | 排行榜、任务调度 |
Bitmap | 用户签到、在线状态 |
HyperLogLog | UV 统计、去重计数 |
Geo | 定位服务、附近搜索 |
Stream | 日志存储、消息队列 |
如果你的系统需要排行榜,推荐使用 ZSet;如果是海量数据去重统计,推荐 HyperLogLog;如果需要分布式消息队列,可以用 Stream。
Redis 过期策略
Redis 允许给 key
设置过期时间(TTL,Time To Live),当 key
到达过期时间后,Redis 会自动删除它。但 Redis 不会立即清除所有过期数据,而是采用以下三种策略来管理过期 key
。
1. 过期键删除策略
Redis 采用以下三种方式管理过期键:
(1)惰性删除(Lazy Deletion)
原理:仅在客户端访问
key
时,Redis 发现它已经过期,才会删除该key
。优点:
- 对 CPU 友好,不会浪费资源检查无用的
key
。
- 对 CPU 友好,不会浪费资源检查无用的
缺点:
- 可能导致大量过期数据未被及时清理,占用内存。
- 如果有大量过期数据,但很少访问,Redis 可能会积累大量无用
key
,影响内存占用。
示例:
redisTemplate.opsForValue().set("temp_key", "value", 10, TimeUnit.SECONDS); // 设置 10 秒过期 Thread.sleep(11000); // 11 秒后访问 String value = redisTemplate.opsForValue().get("temp_key"); // 发现 key 已过期,删除
(2)定期删除(Active Expiration)
原理:
- Redis 每秒执行 10 次(默认),随机抽取部分带有过期时间的
key
,如果已过期,则删除。 - 通过限制删除的
key
数量,避免影响 Redis 性能。
- Redis 每秒执行 10 次(默认),随机抽取部分带有过期时间的
优点:
- 避免了大量过期
key
长时间存在的问题。
- 避免了大量过期
缺点:
- 可能有部分
key
没有被及时删除,仍然占用内存。 - Redis 需要额外的 CPU 资源进行定期扫描。
- 可能有部分
示例:
- Redis 后台线程 运行时,每 100ms 进行一次批量删除(默认配置)。
- 可以通过
config set hz 10
调整定期任务的频率。
(3)内存淘汰(当内存不足时触发)
原理:当 Redis 内存达到上限,Redis 会根据配置的淘汰策略删除某些
key
以释放空间。策略(
maxmemory-policy
配置项):- noeviction(默认)🚫:不删除数据,返回错误(适用于持久化场景)。
- allkeys-lru 🏆:删除**最近最少使用(LRU)**的
key
,适用于通用缓存。 - volatile-lru 🕐:仅删除带 TTL 过期时间的
key
,按 LRU 规则淘汰。 - allkeys-random 🎲:在所有
key
中随机删除。 - volatile-random 🎲:在带 TTL 的
key
中随机删除。 - volatile-ttl ⏳:删除TTL 最短的
key
。
示例:
CONFIG SET maxmemory 100mb CONFIG SET maxmemory-policy allkeys-lru
2. 过期策略的对比
策略 | 触发时机 | 优点 | 缺点 |
---|---|---|---|
惰性删除 | 访问 key 时 | CPU 友好 | 可能造成大量过期数据占用内存 |
定期删除 | Redis 定期触发 | 及时清理部分过期 key | 可能仍有部分 key 没删除 |
内存淘汰 | Redis 内存不足时 | 确保 Redis 不会 OOM | 可能删除仍在使用的数据 |
3. 过期策略的最佳实践
大部分业务场景(常规缓存):
- 采用 定期删除 + 惰性删除,确保大部分
key
及时清理。 - 设置合理的
maxmemory-policy
,如allkeys-lru
避免 OOM。
- 采用 定期删除 + 惰性删除,确保大部分
热点数据(不希望被删除的
key
):- 不设置 TTL,手动维护更新。
- 使用 LRU 机制,如
allkeys-lru
保留最常访问的数据。
低频访问数据(可自动淘汰):
- 设置 TTL +
volatile-lru
,避免占用大量 Redis 内存。
- 设置 TTL +
4. 代码示例
设置过期时间
redisTemplate.opsForValue().set("key", "value", 10, TimeUnit.SECONDS);
查询剩余 TTL
Long ttl = redisTemplate.getExpire("key", TimeUnit.SECONDS);
批量删除过期 Key
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
if (redisTemplate.getExpire(key) <= 0) {
redisTemplate.delete(key);
}
}
总结
方案 | 适用场景 | 适合的策略 |
---|---|---|
普通缓存数据 | 常规数据存储 | 定期删除 + 惰性删除 |
高频访问热点数据 | 重要数据 | 不设置 TTL,手动更新 |
低频访问数据 | 可自动清理数据 | TTL 过期策略 |
内存受限时 | 内存优化 | allkeys-lru / volatile-lru |
建议:
- 结合 定期删除 + 惰性删除 确保数据及时清理。
- 热点数据 不设置 TTL,避免缓存击穿。
- 合理选择
maxmemory-policy
,避免 OOM。
Redis 为什么是单线程的,性能为何仍然很高?
1. Redis 为什么是单线程的?
Redis 从 2.6 开始采用了 单线程 处理命令请求(I/O 线程除外),主要基于以下考虑:
避免线程上下文切换开销
- 多线程会涉及 CPU 上下文切换,增加系统开销。
- Redis 采用 单线程 + 非阻塞 I/O,避免了多线程锁竞争问题,简化了代码逻辑,提高了性能。
避免加锁,提高执行效率
- 多线程需要使用 锁(mutex、读写锁) 保护共享数据,而单线程不需要加锁,大大提升了执行效率。
大部分操作是 CPU 不是瓶颈
- Redis 主要是 基于内存 进行操作,CPU 计算时间极短,核心性能瓶颈在 网络 I/O 和内存访问,而不是 CPU 计算。
- 单线程足以应对大部分场景,减少了复杂度。
2. 为什么 Redis 仍然很快?
虽然 Redis 是 单线程 处理命令请求,但仍然可以达到 每秒几十万 QPS,主要原因如下:
(1)基于内存操作
- Redis 所有数据都在内存中,查询操作是 O(1) 或 O(logN),速度远快于传统的磁盘数据库(如 MySQL)。
- 例如:
redisTemplate.opsForValue().get("key"); // O(1) 读取数据
(2)采用非阻塞 I/O(多路复用)
- Redis 采用 epoll/kqueue 等多路复用技术,一个线程可管理多个网络连接,避免了阻塞 I/O 。
- 示例:
- 传统
select/poll
:轮询检查所有连接,效率低 epoll
:事件驱动机制,仅处理活跃连接,效率高
- 传统
- 结果:即使是单线程,Redis 仍能高效处理并发请求。
(3)高效的数据结构
- Redis 采用 优化过的数据结构,如:
- String(O(1)):简单 key-value 存取
- Hash(O(1)~O(logN)):基于哈希表,查询快
- ZSet(O(logN)):跳表(skip list)优化排序
- 这些高效数据结构,使 Redis 查询、插入等操作速度极快。
(4)避免了 CPU 竞争
- 单线程避免了 加锁 和 线程调度,减少了 CPU 竞争,提高执行效率。
(5)流水线(Pipeline)技术
- Redis 支持批量命令执行,减少了多次请求的网络开销,大幅提升性能。
- 示例:
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (int i = 0; i < 1000; i++) { connection.set(("key" + i).getBytes(), ("value" + i).getBytes()); } return null; });
- 结果:
- 1000 次 set 命令合并成 1 次网络请求,减少了 RTT(网络往返延迟)。
3. Redis 什么时候使用多线程?
Redis 6.0 开始引入了多线程,但主要用于网络 I/O 处理,核心数据操作仍然是单线程。
Redis 6.0 多线程优化:
- 读取请求(I/O 线程并行处理)
- 解析命令
- 返回结果
数据操作仍然是单线程执行,避免加锁带来的性能损失。
示例(Redis 6.0 开启多线程):
redis.conf: io-threads 4
适用场景:
- 大量连接(如 10W+ 连接)
- 高吞吐场景(如日志收集、消息队列)
总结
特点 | Redis(单线程) |
---|---|
CPU 负担 | 主要是 I/O 等待,单线程足够 |
I/O 处理 | 多路复用(epoll),非阻塞 |
数据访问 | O(1) 或 O(logN),极快 |
线程安全 | 无需加锁,避免竞争 |
高并发优化 | 流水线 + 多路复用 + 内存优化 |
如果你的系统主要是 缓存查询,Redis 单线程足以满足高并发需求。在 Redis 6.0 以后,可以利用 多线程 I/O 进一步提高吞吐量。
如何保证 Redis 的高可用性?
为了确保 Redis 在服务器故障、网络异常等情况下仍能正常工作,需要采用 高可用架构,常见方案如下:
1. 主从复制(Replication)
原理
- Redis 允许一个 主节点(Master) 复制到多个 从节点(Slave)。
- 所有写操作在 Master 执行,从节点仅用于读操作,实现 读写分离。
- 主节点故障时,手动切换从节点为主节点(无自动故障转移)。
配置
- 启动主从模式
redis-server --port 6379 # 主节点 redis-server --port 6380 # 从节点
- 在从节点上执行
redis-cli -p 6380 replicaof 127.0.0.1 6379
- 查看主从状态
redis-cli info replication
优点
✅ 读写分离,提升 Redis 性能
✅ 从节点可分担读压力
✅ 数据备份,防止数据丢失
缺点
❌ 主节点故障时,需要手动切换从节点
❌ 从节点只读,不能自动提升为主节点
✅ 适用场景:适用于 高并发读 场景,如 排行榜、社交应用。
2. Redis 哨兵(Sentinel)
原理
- 在 主从复制的基础上,Redis Sentinel(哨兵) 负责:
- 监控 Master 和 Slave 的健康状态
- 自动故障转移(主节点挂掉时,自动提升 Slave 为 Master)
- 通知客户端新主节点信息
配置
配置
sentinel.conf
port 26379 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000
启动 Sentinel
redis-sentinel sentinel.conf
查询主节点
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
优点
✅ 自动故障转移,保证高可用
✅ 主节点挂掉时,Sentinel 自动切换 Slave 为 Master
✅ 适用于生产环境
缺点
❌ 客户端需要支持 Sentinel 模式
❌ Sentinel 本身也可能成为单点故障(需部署多个)
✅ 适用场景:适用于 高可用要求较高 的业务,如 支付系统、秒杀系统。
3. Redis Cluster(分片集群)
原理
- Redis Cluster 是 Redis 的官方分布式集群方案。
- 采用 分片(Sharding) 方式存储数据,不同节点存储不同 Key,避免单机内存瓶颈。
- 数据自动分配到多个主节点,每个 Master 节点可以有多个 Slave 备份。
- 自动故障转移:某个 Master 挂掉后,集群会自动选举 Slave 作为新 Master。
配置
创建 6 个 Redis 节点
redis-server --port 7000 --cluster-enabled yes redis-server --port 7001 --cluster-enabled yes redis-server --port 7002 --cluster-enabled yes redis-server --port 7003 --cluster-enabled yes redis-server --port 7004 --cluster-enabled yes redis-server --port 7005 --cluster-enabled yes
创建集群
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
查看集群状态
redis-cli -p 7000 cluster info
优点
✅ 数据自动分片,解决单机内存上限问题
✅ 主从复制 + 自动故障转移,保证高可用
✅ 适用于大规模分布式系统
缺点
❌ 不支持多 Key 事务
❌ 客户端需支持 Redis Cluster 模式
❌ 集群管理较复杂
✅ 适用场景:适用于 高可用 + 高性能大规模缓存 场景,如 大规模社交应用、电商平台。
4. 主从 + Sentinel + Cluster(混合架构)
- 结合主从复制、哨兵、Cluster,提供最强高可用方案:
- Redis Cluster 负责分片
- 每个分片使用主从复制
- Sentinel 负责故障转移
✅ 适用场景:超大规模分布式系统(如 腾讯、阿里、抖音 等高并发场景)。
5. Redis 高可用方案对比
方案 | 适用场景 | 读写分离 | 自动故障转移 | 分布式存储 |
---|---|---|---|---|
主从复制 | 读多写少 | ✅ 支持 | ❌ 需手动切换 | ❌ |
Sentinel | 中等规模高可用 | ✅ 支持 | ✅ 自动切换 | ❌ |
Cluster | 大规模数据存储 | ❌(但可通过 proxy 实现) | ✅ 自动切换 | ✅ |
混合架构 | 超大规模高可用 | ✅ | ✅ | ✅ |
总结
需求 | 推荐方案 |
---|---|
简单读写分离,防止单点故障 | 主从复制 |
高可用(自动故障转移) | Redis Sentinel |
海量数据分片(高并发大数据存储) | Redis Cluster |
企业级高可用架构(综合解决方案) | 混合架构(Cluster + Sentinel + 主从) |
如果你的系统是 普通的读多写少场景,可以选择 主从复制;如果需要 自动故障转移,可以使用 Redis Sentinel;如果是 高并发大规模数据存储,建议使用 Redis Cluster。
Redis 持久化方式及优缺点
Redis 提供两种持久化方案:
- RDB(Redis Database)快照持久化
- AOF(Append Only File)日志持久化
- 混合模式(RDB + AOF)
1. RDB(快照持久化)
原理
- Redis 定期 以 二进制快照 方式保存整个数据集 到磁盘 (
dump.rdb
)。 - 触发方式:
- 定时触发(
save 900 1
,表示900 秒内至少 1 次修改触发保存) - 手动触发(执行
SAVE
或BGSAVE
命令)
- 定时触发(
优点
✅ 数据恢复速度快(直接加载 .rdb
)
✅ 对 Redis 运行影响较小(子进程完成持久化,不影响主线程)
✅ 适合大规模数据存储(适用于冷备份、灾难恢复)
缺点
❌ 可能丢失数据(定时快照,Redis 崩溃可能丢失最近修改的数据)
❌ BGSAVE 需 fork 子进程,占用额外 CPU、内存
适用场景
- 冷备份(定期持久化)
- 数据对丢失不敏感的场景(如 排行榜、日志分析)
配置
# 配置 RDB 持久化,满足 3 个条件之一就触发快照:
save 900 1 # 900 秒内至少有 1 次修改
save 300 10 # 300 秒内至少有 10 次修改
save 60 1000 # 60 秒内至少有 1000 次修改
2. AOF(Append Only File,日志持久化)
原理
- Redis 将所有写操作追加到日志文件(
appendonly.aof
)。 - 触发方式:
- 每次写入即记录(append-only)
- 定期进行 AOF 重写(compact 压缩),减少文件大小。
优点
✅ 数据可靠性高(可恢复所有操作,数据丢失风险低)
✅ AOF 文件可读(以文本方式存储 Redis 命令)
✅ 适用于高可靠性需求的业务(如 支付、交易系统)
缺点
❌ AOF 文件较大(比 RDB 体积大)
❌ AOF 恢复比 RDB 慢(需要一条一条回放操作)
❌ 每次写入记录日志,性能稍低
适用场景
- 金融、支付系统(如 交易系统,不能丢数据)
- 高可靠性要求的业务
配置
appendonly yes # 开启 AOF
appendfsync everysec # 每秒同步一次(推荐,平衡性能与可靠性)
3. RDB + AOF(混合模式)
原理
- 同时开启 RDB 和 AOF,利用 RDB 备份大数据,AOF 确保数据可靠性。
- Redis 4.0+ 支持混合持久化(AOF 先存储 RDB 快照,再记录增量 AOF)。
优点
✅ 兼顾数据安全(AOF)和恢复速度(RDB)
✅ Redis 崩溃时,AOF 可恢复最近数据
✅ 相比纯 AOF,文件更小,恢复更快
缺点
❌ 占用磁盘更多
❌ 复杂度更高
适用场景
- 大多数生产环境(如高可用业务)
配置
# 开启 AOF 并使用 RDB+AOF 混合模式
appendonly yes
aof-use-rdb-preamble yes
4. 持久化方式对比
方式 | 数据安全性 | 性能 | 磁盘占用 | 恢复速度 | 适用场景 |
---|---|---|---|---|---|
RDB | 中(定期保存,可能丢失数据) | 高(子进程执行,主线程无影响) | 小(仅存快照) | 快 | 冷备、数据不敏感 |
AOF | 高(记录所有写操作) | 低(每次写入追加日志) | 大(文件不断增大) | 慢(需要回放命令) | 金融、支付、交易 |
RDB + AOF | 最高(结合两者优点) | 中(AOF 记录快照,减少写入) | 中 | 中 | 高可用业务 |
5. 生产环境推荐
需求 | 推荐方案 |
---|---|
数据安全性最高(金融、支付) | AOF(appendfsync everysec) |
快速恢复,冷备份 | RDB |
高可用,兼顾安全与性能 | RDB + AOF 混合模式 |
一般情况下,生产环境推荐 RDB + AOF,兼顾性能与可靠性。
如何设计一个合理的缓存更新策略?
在分布式系统中,缓存更新策略的设计需要在数据一致性、缓存命中率和更新效率之间取得平衡。通常,合理的缓存更新策略主要关注 何时更新(触发时机)和 如何更新(更新方式)。
1. 何时更新缓存(触发时机)
(1)写入时更新
- 触发时机:当数据库数据更新时,同时更新缓存。
- 适用场景:数据一致性要求高,如订单状态、支付信息。
- 示例:
// 更新数据库后,同时更新缓存 updateDatabase(data); redisTemplate.opsForValue().set("key", newValue);
- 优点:
✅ 数据一致性高
✅ 适用于写入频率较低的场景 - 缺点:
❌ 数据更新后立即写入缓存,可能会覆盖并发写入导致脏数据
❌ 缓存更新频繁,可能影响性能
(2)读取时更新(Cache-Aside,旁路缓存)
- 触发时机:应用先查询缓存,若缓存未命中,则从数据库查询,并更新缓存。
- 适用场景:读多写少,如商品详情、用户信息。
- 示例:
String key = "user:1001"; String value = redisTemplate.opsForValue().get(key); if (value == null) { // 缓存未命中 value = queryFromDatabase(); redisTemplate.opsForValue().set(key, value, 60, TimeUnit.SECONDS); } return value;
- 优点:
✅ 提高缓存命中率,减少数据库压力
✅ 不会缓存无效数据(数据库没有数据时不缓存) - 缺点:
❌ 可能导致短时间缓存不一致(数据库更新后缓存未刷新)
❌ 高并发场景可能导致缓存击穿(多个线程同时查询数据库)
(3)定时刷新(TTL 过期 + 定时任务)
- 触发时机:缓存数据到期后,由后台任务定期刷新。
- 适用场景:数据更新有固定周期,如排行榜、热搜榜。
- 示例:
@Scheduled(fixedRate = 60000) // 每 60 秒刷新缓存 public void refreshCache() { String value = queryFromDatabase(); redisTemplate.opsForValue().set("hot_key", value, 60, TimeUnit.SECONDS); }
- 优点:
✅ 避免热点数据过期导致缓存击穿
✅ 适用于数据定期更新的业务 - 缺点:
❌ 可能存在数据过期前的一致性问题
❌ 定时任务可能带来额外的数据库压力
2. 如何更新缓存(更新方式)
(1)更新缓存
- 方法:数据库更新后,同时更新缓存数据。
- 适用场景:一致性要求高,且缓存更新代价较低的场景,如账户余额、库存数据。
- 示例:
updateDatabase(newValue); redisTemplate.opsForValue().set("key", newValue);
- 优点:
✅ 读写数据一致性高 - 缺点:
❌ 可能会缓存无效数据(如数据库未更新但缓存已修改)
(2)删除缓存(推荐)
- 方法:数据库更新后先删除缓存,下次请求时缓存自动加载最新数据。
- 适用场景:数据一致性要求高,且数据读写不均衡的场景。
- 示例:
updateDatabase(newValue); redisTemplate.delete("key"); // 删除缓存
- 优点:
✅ 避免缓存与数据库数据不一致
✅ 适用于高并发场景(配合 Cache-Aside 策略) - 缺点:
❌ 缓存击穿风险(短时间大量查询会打到数据库)
✅ 优化方案:延时双删策略(避免缓存刚删除又被旧数据写回)
updateDatabase(newValue);
redisTemplate.delete("key");
Thread.sleep(500); // 延迟删除,防止并发问题
redisTemplate.delete("key");
3. 解决缓存更新中的问题
(1)防止缓存雪崩
问题
- 大量缓存同时过期,所有请求瞬间访问数据库,导致数据库崩溃。
解决方案
✅ 设置不同的过期时间(TTL 随机化)
int ttl = 300 + new Random().nextInt(60); // 300~360秒随机过期
redisTemplate.opsForValue().set("key", value, ttl, TimeUnit.SECONDS);
✅ 热点数据不过期(手动更新)
✅ 多级缓存(本地缓存 + Redis)
(2)防止缓存穿透
问题
- 查询的 Key 在缓存和数据库中都不存在,导致每次查询都直接打数据库。
解决方案
✅ 缓存空值
if (dbResult == null) {
redisTemplate.opsForValue().set("key", "", 5, TimeUnit.MINUTES); // 缓存空值
}
✅ 布隆过滤器(Bloom Filter)
if (!bloomFilter.mightContain("key")) {
return null; // 直接拦截请求
}
(3)防止缓存击穿
问题
- 热点数据过期后,瞬间大量请求同时访问数据库。
解决方案
✅ 设置热点数据永不过期,后台更新
redisTemplate.opsForValue().set("hot_key", data); // 不设置 TTL
✅ 互斥锁
boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:hot_key", "1", 10, TimeUnit.SECONDS);
if (lock) {
try {
value = queryFromDatabase();
redisTemplate.opsForValue().set("hot_key", value, 60, TimeUnit.SECONDS);
} finally {
redisTemplate.delete("lock:hot_key");
}
}
4. 选择合适的缓存更新策略
业务场景 | 推荐策略 |
---|---|
读多写少,如商品详情、用户信息 | Cache-Aside(旁路缓存) |
高一致性要求(订单、支付) | Write-Through(写穿缓存) |
写操作频繁(日志系统) | Write-Behind(异步写入) |
排行榜、热搜 | TTL + 定时刷新 |
分布式高并发场景 | 布隆过滤器 + 互斥锁 |
5. 生产环境最佳实践
- 普通缓存:
- Cache-Aside + TTL,避免缓存长期无效数据。
- 高一致性数据:
- Write-Through + 事务保证数据库与缓存一致。
- 高并发场景:
- 加互斥锁,避免缓存击穿。
- 热点数据:
- 永不过期,手动更新,保证高可用。
通过 合理选择缓存更新策略,可以提升系统性能,减少数据库压力,并保证数据一致性。
分布式缓存如何保证数据一致性?
在分布式系统中,数据一致性是一个核心问题。由于 缓存(Redis)和数据库(MySQL)是两个独立的存储系统,在高并发场景下可能出现 缓存数据与数据库数据不一致 的情况。常见的不一致问题如下:
1. 缓存与数据库不一致的原因
(1)缓存延迟更新
- 问题:数据库更新后,缓存未及时更新,导致缓存仍然是旧数据。
- 场景:应用程序先更新数据库,然后更新缓存,但 缓存更新失败 导致数据不一致。
(2)缓存删除失败
- 问题:数据库更新后,尝试删除缓存,但因 Redis 宕机或网络异常导致缓存仍然是旧数据。
(3)并发问题
- 问题:多个线程同时更新数据库,导致缓存写入的顺序错乱,引起数据不一致。
(4)数据库回滚
- 问题:数据库更新失败(如事务回滚),但缓存已经更新,导致数据不一致。
2. 保证缓存一致性的策略
(1)延时双删策略(推荐)
原理
- 先删除缓存
- 更新数据库
- 等待短暂时间(如 500ms),再次删除缓存,防止并发问题
示例代码
redisTemplate.delete("key"); // 第一次删除缓存
updateDatabase(); // 更新数据库
Thread.sleep(500); // 等待一小段时间
redisTemplate.delete("key"); // 第二次删除缓存,防止并发问题
优点
✅ 减少数据库与缓存不一致的可能性
✅ 避免数据库更新后,其他线程读取到旧缓存
缺点
❌ 可能会引起缓存击穿(高并发时缓存短暂失效)
❌ 需要优化延时策略,不能过长(影响性能)或过短(无效删除)
(2)先更新数据库,再更新缓存
原理
- 数据库更新成功后立即更新缓存,保证数据一致性。
示例代码
updateDatabase(newValue);
redisTemplate.opsForValue().set("key", newValue);
优点
✅ 数据更新后立即生效,保证一致性
✅ 适用于低并发业务(如账户余额、库存)
缺点
❌ 高并发下可能导致数据库更新与缓存更新顺序错乱
❌ 如果缓存更新失败,仍可能产生不一致问题
(3)基于消息队列的异步更新(推荐)
原理
- 数据库更新成功后,发送消息到 MQ(如 Kafka、RabbitMQ)
- 消费端监听 MQ 事件,更新或删除缓存
示例代码
// 生产者(更新数据库后发送消息)
updateDatabase(newValue);
messageQueue.send("update_cache", "key");
// 消费者(监听 MQ,更新缓存)
public void onMessage(String key) {
String newValue = queryFromDatabase();
redisTemplate.opsForValue().set(key, newValue);
}
优点
✅ 保证最终一致性(缓存异步更新)
✅ 解耦数据库和缓存更新流程
✅ 适用于高并发场景
缺点
❌ 需要额外的消息队列
❌ 可能存在短暂的不一致性(消息延迟)
(4)基于 Binlog 监听数据库变更
原理
- 监听 MySQL Binlog 日志,当数据变更时自动触发缓存更新(如 Canal、Debezium)。
- 避免业务逻辑直接操作缓存,减少不一致问题。
示例
- MySQL 产生 Binlog 日志
- Canal 解析 Binlog 并发送变更事件
- Redis 监听事件并更新缓存
优点
✅ 实时监听数据库变更,保证缓存更新
✅ 自动触发,无需额外开发逻辑
缺点
❌ Binlog 解析可能增加系统开销
❌ 仅适用于 MySQL(其他数据库需要不同方案)
(5)事务 + 缓存一致性
原理
- 数据库更新和缓存更新放入同一个事务,保证原子性。
示例代码
@Transactional
public void updateData(String key, String newValue) {
updateDatabase(newValue); // 更新数据库
redisTemplate.opsForValue().set(key, newValue); // 更新缓存
}
优点
✅ 保证数据库和缓存的一致性
✅ 适用于高一致性要求的业务
缺点
❌ Redis 不支持事务回滚(如果 Redis 更新失败,数据库不会回滚)
❌ 可能影响数据库性能
3. 方案对比
方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
---|---|---|---|---|
延时双删策略 | 高 | 高 | 低 | 读多写少场景(如商品详情) |
更新数据库后更新缓存 | 中 | 高 | 低 | 低并发场景(如账户信息) |
基于 MQ 异步更新 | 高 | 中 | 高 | 高并发(如订单、支付) |
监听 Binlog 更新缓存 | 高 | 高 | 高 | 高一致性(如库存管理) |
事务更新数据库和缓存 | 最高 | 低 | 低 | 订单、支付等高一致性要求业务 |
4. 生产环境最佳实践
(1)普通缓存更新
- 使用延时双删策略
- 热点数据可采用“先更新数据库,再删除缓存”
(2)高并发业务
- 使用 MQ 异步更新缓存,减少数据库和缓存的耦合
- 可以结合 Binlog 监听,实时更新缓存
(3)高一致性业务
- 事务 + 缓存更新(尽可能减少 Redis 失败的风险)
- 必要时配合 MQ 补偿机制,确保最终一致性
通过合理的缓存更新策略,可以保证分布式缓存的一致性,避免缓存污染,提高系统的稳定性和可靠性。
如何防止缓存中的热点数据导致系统崩溃?
在高并发系统中,热点数据(如热门商品、排行榜、明星新闻)可能导致缓存击穿、数据库压力骤增,甚至引发系统崩溃。以下是防止热点数据导致系统崩溃的几种关键策略。
1. 采用多级缓存(本地缓存 + Redis)
原理
- 一级缓存:使用 本地缓存(Ehcache、Caffeine) 存储热点数据,减少对 Redis 的访问。
- 二级缓存:使用 Redis 作为主要缓存,缓解数据库压力。
- 数据库:仅在 Redis 失效时查询数据库。
示例
String key = "hot_key";
String value = caffeineCache.getIfPresent(key);
if (value == null) {
value = redisTemplate.opsForValue().get(key);
if (value != null) {
caffeineCache.put(key, value); // 加载到本地缓存
}
}
优点
✅ 减少 Redis 压力,提高响应速度
✅ 降低网络延迟(本地缓存比 Redis 访问更快)
缺点
❌ 占用应用服务器内存,适用于小规模热点数据
2. 设置热点数据永不过期(手动更新)
原理
- 热点数据设置
TTL = -1
(永不过期),避免频繁缓存失效带来的流量冲击。 - 数据变更时手动更新缓存。
示例
redisTemplate.opsForValue().set("hot_key", value); // 不设置 TTL,永不过期
优点
✅ 缓存数据不会过期,防止缓存击穿
✅ 适用于访问量极高的热点数据
缺点
❌ 需要手动更新缓存,增加维护成本
3. 采用互斥锁防止缓存击穿
原理
- 防止高并发时,多个请求同时查询数据库,导致数据库崩溃。
- 仅允许一个线程查询数据库,其余线程等待缓存更新。
示例
String key = "hot_key";
String value = redisTemplate.opsForValue().get(key);
if (value == null) { // 缓存失效
boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:hot_key", "1", 10, TimeUnit.SECONDS);
if (lock) { // 只有一个线程能获取锁
try {
value = queryFromDatabase();
redisTemplate.opsForValue().set(key, value, 60, TimeUnit.SECONDS);
} finally {
redisTemplate.delete("lock:hot_key"); // 释放锁
}
} else {
Thread.sleep(100);
value = redisTemplate.opsForValue().get(key); // 重新获取缓存
}
}
优点
✅ 防止数据库瞬间压力过载
✅ 适用于热点数据偶尔失效的场景
缺点
❌ 增加一定的代码复杂度
4. 预加载缓存(定时刷新热点数据)
原理
- 后台定时任务主动刷新缓存,避免数据突然失效。
- 适用于排行榜、热搜等定期更新的数据。
示例
@Scheduled(fixedRate = 60000) // 每 60 秒更新缓存
public void refreshHotCache() {
String value = queryFromDatabase();
redisTemplate.opsForValue().set("hot_key", value, 60, TimeUnit.SECONDS);
}
优点
✅ 防止热点数据过期导致大量请求直达数据库
✅ 适用于定期更新的数据
缺点
❌ 可能会缓存不必要的数据,影响性能
5. 使用布隆过滤器(防止缓存穿透)
原理
- 使用布隆过滤器存储所有可能的 Key,避免查询 Redis 和数据库不存在的数据。
示例
if (!bloomFilter.mightContain("hot_key")) {
return null; // 直接返回,避免访问 Redis 和数据库
}
String value = redisTemplate.opsForValue().get("hot_key");
优点
✅ 防止恶意攻击(查询不存在的 Key)
✅ 减少 Redis 负载
缺点
❌ 可能存在误判(布隆过滤器并非 100% 精确)
6. 采用 Redis Cluster + 分片
原理
- Redis Cluster 将数据分布在多个节点,防止单个 Redis 服务器成为瓶颈。
- 负载均衡,多个 Redis 共同存储热点数据。
示例
# 创建 Redis Cluster
redis-cli --cluster create 192.168.1.1:6379 192.168.1.2:6379 192.168.1.3:6379 --cluster-replicas 1
优点
✅ 分散 Redis 负载,避免单点压力过大
✅ 适用于大规模高并发系统
缺点
❌ 需要额外的 Redis 服务器资源
7. 使用热点 Key 负载均衡
原理
- 将一个热点 Key 拆分成多个 Key,降低单 Key 访问压力。
- 例如,
hot_key
拆分为hot_key_1
、hot_key_2
,多个服务器分摊访问。
示例
String realKey = "hot_key_" + (ThreadLocalRandom.current().nextInt(5)); // 随机访问 5 个 Key
String value = redisTemplate.opsForValue().get(realKey);
优点
✅ 均衡访问压力,降低单 Key 访问量
✅ 适用于极端高并发的热点数据
缺点
❌ 需要额外的 Key 设计和查询逻辑
8. 生产环境最佳实践
策略 | 适用场景 | 优缺点 |
---|---|---|
多级缓存(本地缓存 + Redis) | 低延迟热点数据 | ✅ 低延迟,减少 Redis 压力 |
热点数据永不过期 | 访问量极高的数据 | ✅ 无缓存失效问题 ❌ 需手动更新 |
互斥锁防止击穿 | 突发流量热点数据 | ✅ 防止并发数据库访问 ❌ 增加复杂度 |
定时刷新缓存 | 热搜、排行榜 | ✅ 适用于周期性更新数据 |
布隆过滤器 | 防止缓存穿透 | ✅ 防止恶意查询 ❌ 误判率 |
Redis Cluster 分片 | 超大流量场景 | ✅ 负载均衡,分摊压力 |
热点 Key 负载均衡 | 超高并发单 Key | ✅ 拆分访问流量 |
9. 总结
- 小规模高并发热点数据 → 本地缓存 + Redis
- 突发流量热点数据 → 设置热点 Key 永不过期 + 互斥锁
- 定期更新热点数据 → 定时刷新缓存
- 防止缓存穿透 → 布隆过滤器
- 超大规模热点数据 → Redis Cluster + 负载均衡
通过合理设计缓存策略,可以有效防止热点数据导致系统崩溃,确保系统稳定运行。
- Spring Cache 是如何工作的?它支持哪些缓存实现?
Spring Cache 是如何工作的?
1. Spring Cache 工作原理
Spring Cache 提供了一种 声明式缓存(Declarative Caching) 方式,允许开发者在不改动业务逻辑的情况下,使用注解来管理缓存数据。
Spring Cache 主要通过 AOP(切面编程)+ 代理机制 实现:
- 拦截方法调用(通过 Spring AOP 代理)
- 检查缓存是否命中(是否已有缓存数据)
- 决定是否执行目标方法(缓存未命中时执行方法,并存储结果)
2. Spring Cache 核心注解
注解 | 作用 |
---|---|
@Cacheable | 查询缓存:如果缓存中有数据,直接返回;如果没有,执行方法并缓存结果 |
@CachePut | 更新缓存:方法执行后,将返回结果更新到缓存 |
@CacheEvict | 删除缓存:删除缓存数据 |
@Caching | 组合多个 @Cacheable 、@CachePut 、@CacheEvict |
@CacheConfig | 全局缓存配置,可以减少重复的 cacheNames 配置 |
3. Spring Cache 工作流程
以 @Cacheable
为例,Spring Cache 的执行流程如下:
- 拦截方法调用
- 当方法被调用时,Spring AOP 代理会检查该方法是否被
@Cacheable
注解标记。
- 当方法被调用时,Spring AOP 代理会检查该方法是否被
- 查询缓存
- Spring Cache 先查询缓存是否已存在该数据。
- 返回缓存数据(命中)
- 如果缓存中有数据,则直接返回,不执行方法逻辑。
- 执行方法(未命中)
- 如果缓存中没有数据,执行目标方法,并将返回值存入缓存。
- 存入缓存
- 将方法返回值存入指定缓存,以供下次查询时使用。
示例
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("查询数据库...");
return userRepository.findById(id).orElse(null);
}
}
调用逻辑
- 第一次调用
getUserById(1L)
- 缓存未命中,查询数据库并存入缓存。
- 第二次调用
getUserById(1L)
- 缓存命中,直接返回缓存数据,不查询数据库。
Spring Cache 支持的缓存实现
Spring Cache 本身不提供缓存存储,但支持多种缓存实现:
缓存实现 | 说明 |
---|---|
ConcurrentMapCacheManager | 默认实现,使用 ConcurrentHashMap 存储缓存(仅适用于单机) |
Ehcache | Java 本地缓存,支持磁盘存储,适用于 单机应用 |
Caffeine | 高性能本地缓存(比 Ehcache 更快),适用于 低延迟、高并发 场景 |
Redis | 分布式缓存,适用于 微服务、大规模应用 |
Hazelcast | 分布式内存数据存储,适用于 集群 |
JCache(JSR-107) | 兼容多个缓存实现,如 Ehcache、Hazelcast、Caffeine |
如何选择缓存实现?
应用场景 | 推荐缓存 |
---|---|
单机应用,简单缓存 | ConcurrentMapCacheManager |
高性能本地缓存(低延迟) | Caffeine |
持久化缓存,单机/集群 | Ehcache |
分布式缓存(微服务、高并发) | Redis |
跨节点集群缓存 | Hazelcast |
Spring Cache 配置示例
1. 使用 Redis 作为 Spring Cache
添加 Redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 Redis 缓存
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))) // 设置缓存过期时间
.build();
}
}
使用 Redis 缓存
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
总结
- Spring Cache 通过 AOP + 代理实现缓存管理,常用
@Cacheable
、@CachePut
、@CacheEvict
控制缓存行为。 - 支持多种缓存实现,如 本地缓存(Caffeine、Ehcache) 和 分布式缓存(Redis、Hazelcast)。
- 生产环境推荐使用 Redis,因为它支持 高并发、分布式、高可用。
- 本地缓存(如 Caffeine)适用于低延迟应用,可以作为 多级缓存策略(本地缓存 + Redis) 的一部分。
如果你的应用是 微服务架构,建议使用 Redis 作为缓存;如果是单机应用,可以使用 Caffeine 提高查询性能。