一文彻底理解 Java 缓存机制:原理、实现、缓存击穿详解


在实际开发中,我们经常会听到一句话:

“不要让数据库承受不该承受的压力。”

在高并发系统中,如果每一个请求都直接访问数据库,系统性能会迅速下降,甚至导致数据库崩溃。
为了解决这个问题,缓存(Cache)机制应运而生

缓存是高性能系统设计中最核心的技术之一

本文将从以下几个方面逐步讲解:

  1. 什么是缓存
  2. 为什么需要缓存
  3. Java 中常见缓存实现
  4. 缓存工作流程
  5. 缓存穿透 / 击穿 / 雪崩(重点:缓存击穿)
  6. Java 实际使用示例
  7. 面试常考问题总结

一、什么是缓存(Cache)

缓存本质上是一种“空间换时间”的策略。

缓存是指:

将访问频率高的数据存储在访问速度更快的存储介质中,从而减少计算或数据库访问的次数。

例如:

用户请求 -> 缓存 -> 数据库

流程:

1 用户请求数据
2 先查询缓存
3 如果缓存存在 -> 直接返回
4 如果缓存不存在 -> 查询数据库
5 将数据写入缓存
6 返回结果

示意图:

           +--------+
           |  User  |
           +---+----+
               |
               v
           +---+----+
           | Cache  |
           +---+----+
           /       \
      命中 /         \ 未命中
         v           v
      返回数据     查询数据库
                     |
                     v
                写入缓存

二、为什么需要缓存

缓存的核心价值有三个:

1 提升访问速度

内存访问速度远远快于数据库。

大致速度对比:

存储类型 访问速度
CPU缓存 纳秒
内存 微秒
Redis 毫秒
数据库 毫秒~秒

因此:

缓存命中 -> 微秒
数据库查询 -> 毫秒

性能差距 1000倍以上


2 减轻数据库压力

假设一个系统:

每秒请求:10000
数据库承受能力:1000

如果没有缓存:

数据库直接崩溃

如果有缓存:

90%命中缓存
数据库只处理1000请求

系统即可稳定运行。


3 提高系统并发能力

缓存可以将系统的瓶颈从:

数据库IO

转移到:

内存读取

并发能力提升非常明显。


三、Java中的缓存实现

Java中缓存实现主要有三种:

1 本地缓存

缓存存在JVM内存中

常见实现:

工具 说明
HashMap 最简单缓存
ConcurrentHashMap 线程安全
Guava Cache Google缓存库
Caffeine 高性能缓存库

示例:

Map<String, User> cache = new ConcurrentHashMap<>();

public User getUser(String id) {
    User user = cache.get(id);

    if(user == null){
        user = database.query(id);
        cache.put(id, user);
    }

    return user;
}

优点:

速度极快
无网络开销

缺点:

无法多机器共享
重启数据丢失

2 分布式缓存

缓存存储在独立服务器

常见实现:

技术 说明
Redis 最常见
Memcached 高性能缓存

架构:

应用服务器
      |
      v
   Redis缓存
      |
      v
   数据库

优点:

支持分布式
多服务器共享

缺点:

网络开销
需要维护

3 多级缓存

大型系统通常采用:

本地缓存 + Redis

架构:

用户
  |
  v
本地缓存
  |
  v
Redis
  |
  v
数据库

访问流程:

1 查本地缓存
2 没有查Redis
3 没有查数据库

优点:

性能最高
减少Redis压力

四、缓存的典型工作流程

最经典缓存模式:

Cache Aside Pattern(旁路缓存)

流程:

读取流程

1 查询缓存
2 缓存存在 -> 返回
3 缓存不存在 -> 查询数据库
4 写入缓存
5 返回数据

写入流程:

1 更新数据库
2 删除缓存

示例代码:

public User getUser(String id){

    User user = redis.get(id);

    if(user == null){
        user = database.query(id);
        redis.set(id, user);
    }

    return user;
}

五、缓存三大问题

缓存系统中有三个经典问题:

问题 说明
缓存穿透 查询不存在的数据
缓存击穿 热点数据缓存失效
缓存雪崩 大量缓存同时失效

其中:

缓存击穿是面试最常问的问题之一


六、缓存击穿

什么是缓存击穿

缓存击穿指:

某个热点数据在缓存过期的一瞬间,大量请求同时访问数据库。

示例:

某商品数据
缓存过期时间:10分钟

10分钟时:

10000请求同时访问
缓存失效

结果:

10000请求 -> 数据库

数据库瞬间压力爆炸。

示意图:

请求1 \
请求2  \
请求3   ---> 缓存失效 ---> 数据库
请求4  /
请求5 /

七、如何解决缓存击穿

常见解决方案:

1 互斥锁(最常见)

当缓存失效时:

只允许一个线程查询数据库

其他线程等待。

示例:

public User getUser(String id){

    User user = redis.get(id);

    if(user == null){

        synchronized(this){

            user = redis.get(id);

            if(user == null){
                user = database.query(id);
                redis.set(id,user);
            }

        }
    }

    return user;
}

优点:

简单有效

缺点:

性能可能下降

2 热点数据永不过期

对于热点数据:

不设置过期时间

优点:

不会击穿

缺点:

需要手动更新

3 逻辑过期(推荐)

缓存结构:

value + expireTime

示例:

{
  data: user,
  expire: 1710000000
}

读取逻辑:

1 判断逻辑时间
2 过期 -> 异步更新
3 返回旧数据

优点:

不会阻塞
不会击穿

八、缓存穿透

缓存穿透指:

查询数据库中不存在的数据

例如:

用户ID = -1

数据库没有该数据:

缓存 -> 没有
数据库 -> 没有

攻击者大量请求:

数据库被打爆

解决方案:

1 缓存空对象

redis.set(id, null);

2 布隆过滤器

提前判断:

数据是否存在

九、缓存雪崩

缓存雪崩指:

大量缓存同时过期

示例:

10000缓存
同时10点过期

结果:

数据库瞬间压力巨大

解决方案:

1 过期时间随机

redis.set(key, value, 3600 + random);

2 多级缓存

本地缓存 + Redis

十、Java缓存工具推荐

实际项目中推荐使用:

Caffeine(最强本地缓存)

特点:

高性能
支持LRU
支持过期

示例:

Cache<String, User> cache =
        Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

User user = cache.get(id, key -> database.query(key));

十一、面试高频问题

1 什么是缓存?

缓存是:

将热点数据存储在访问更快的存储中
减少数据库访问

2 什么是缓存击穿?

热点数据缓存过期
大量请求同时访问数据库

解决方案:

互斥锁
逻辑过期
热点永不过期

3 缓存穿透和缓存击穿区别

类型 特点
穿透 查询不存在数据
击穿 热点数据过期
雪崩 大量缓存同时过期

4 常见缓存模式

Cache Aside
Read Through
Write Through
Write Back

最常用:

Cache Aside

十二、总结

缓存是高性能系统的核心技术之一。

本文核心知识:

缓存作用

提高性能
减少数据库压力
提升并发能力

缓存实现

本地缓存
Redis缓存
多级缓存

缓存三大问题

缓存穿透
缓存击穿
缓存雪崩
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐