Springboot 多级缓存设计与实现

🏷️个人主页牵着猫散步的鼠鼠

🏷️系列专栏Java全栈-专栏

🏷️个人学习笔记,若有缺误,欢迎评论区指正
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站AI学习网站

目录

前言

冗余设计理念

多级缓存概述

开启浏览器缓存

[① 配置 Cache-Control](#① 配置 Cache-Control)

[② 配置 Expires](#② 配置 Expires)

[③ 配置 ETag](#③ 配置 ETag)

[④ 配置 Last-Modified](#④ 配置 Last-Modified)

整体配置

[2.2 开启 Nginx 缓存](#2.2 开启 Nginx 缓存)

[① 定义缓存配置](#① 定义缓存配置)

[② 启用缓存](#② 启用缓存)

[③ 设置缓存有效期](#③ 设置缓存有效期)

[④ 配置反向代理](#④ 配置反向代理)

[⑤ 重新加载配置](#⑤ 重新加载配置)

[2.3 使用分布式缓存](#2.3 使用分布式缓存)

[① 添加依赖](#① 添加依赖)

[② 配置 Redis 连接信息](#② 配置 Redis 连接信息)

[③ 启动缓存](#③ 启动缓存)

[④ 使用缓存](#④ 使用缓存)

[2.4 使用本地缓存](#2.4 使用本地缓存)

[① 添加依赖](#① 添加依赖)

[② 配置 Caffeine 缓存](#② 配置 Caffeine 缓存)

[③ 自定义 Caffeine 配置类(可选步骤)](#③ 自定义 Caffeine 配置类(可选步骤))

[④ 开启缓存](#④ 开启缓存)

[⑤ 使用注解进行缓存操作](#⑤ 使用注解进行缓存操作)


前言

对于高并发系统来说,有三个重要的机制来保障其高效运行,它们分别是:缓存、限流和熔断。而缓存是排在最前面也是高并发系统之所以高效运行的关键手段,那么问题来了:缓存只使用 Redis 就够了吗?

冗余设计理念

当然不是,不要把所有鸡蛋放到一个篮子里,成熟的系统在关键功能实现时一定会考虑冗余设计,注意这里的冗余设计不是贬义词。

冗余设计是在系统或设备完成任务起关键作用的地方,增加一套以上完成相同功能的功能通道(or 系统)、工作元件或部件,以保证当该部分出现故障时,系统或设备仍能正常工作,以减少系统或者设备的故障概率,提高系统可靠性。

例如,飞机的设计,飞机正常运行只需要两个发动机,但在每台飞机的设计中可能至少会设计四个发动机,这就有冗余设计的典型使用场景,这样设计的目的是为了保证极端情况下,如果有一个或两个发动机出现故障,不会因为某个发动机的故障而引起重大的安全事故。

多级缓存概述

缓存功能的设计也是一样,我们在高并发系统中通常会使用多级缓存来保证其高效运行,其中的多级缓存就包含以下这些:

  1. 浏览器缓存:它的实现主要依靠 HTTP 协议中的缓存机制,当浏览器第一次请求一个资源时,服务器会将该资源的相关缓存规则(如 Cache-Control、Expires 等)一同返回给客户端,浏览器会根据这些规则来判断是否需要缓存该资源以及该资源的有效期。
  2. Nginx 缓存:在 Nginx 中配置中开启缓存功能。
  3. 分布式缓存:所有系统调用的中间件都是分布式缓存,如 Redis、MemCached 等。
  4. 本地缓存:JVM 层面,单系统运行期间在内存中产生的缓存,例如 Caffeine、Google Guava 等。

以下是它们的具体使用。

开启浏览器缓存

在 Java Web应用中,实现浏览器缓存可以使用 HttpServletResponse 对象来设置与缓存相关的响应头,以开启浏览器的缓存功能,它的具体实现分为以下几步。

① 配置 Cache-Control

Cache-Control 是 HTTP/1.1 中用于控制缓存策略的主要方式。它可以设置多个指令,如 max-age(定义资源的最大存活时间,单位秒)、no-cache(要求重新验证)、public(指示可以被任何缓存区缓存)、private(只能被单个用户私有缓存存储)等,设置如下:

java 复制代码
response.setHeader("Cache-Control", "max-age=3600, public"); // 缓存一小时
② 配置 Expires

设置一个绝对的过期时间,超过这个时间点后浏览器将不再使用缓存的内容而向服务器请求新的资源,设置如下:

java 复制代码
response.setDateHeader("Expires", System.currentTimeMillis() + 3600 * 1000); // 缓存一小时
③ 配置 ETag

ETag(实体标签)一种验证机制,它为每个版本的资源生成一个唯一标识符。当客户端发起请求时,会携带上先前接收到的 ETag,服务器根据 ETag 判断资源是否已更新,若未更新则返回 304 Not Modified 状态码,通知浏览器继续使用本地缓存,设置如下:

java 复制代码
String etag = generateETagForContent(); // 根据内容生成ETag
response.setHeader("ETag", etag);
④ 配置 Last-Modified

指定资源最后修改的时间戳,浏览器下次请求时会带上 If-Modified-Since 头,服务器对比时间戳决定是否返回新内容或发送 304 状态码,设置如下:

java 复制代码
long lastModifiedDate = getLastModifiedDate();
response.setDateHeader("Last-Modified", lastModifiedDate);
整体配置

在 Spring Web 框架中,可以通过 HttpServletResponse 对象来设置这些头信息。例如,在过滤器中设置响应头以启用缓存:

java 复制代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
   HttpServletResponse httpResponse = (HttpServletResponse) response;
   // 设置缓存策略
   httpResponse.setHeader("Cache-Control", "max-age=3600");

   // 其他响应头设置...
   chain.doFilter(request, response);
}

以上就是在 Java Web 应用程序中利用 HTTP 协议特性控制浏览器缓存的基本方法。

开启 Nginx 缓存

Nginx 中开启缓存的配置总共有以下 5 步。

① 定义缓存配置

在 Nginx 配置中定义一个缓存路径和配置,通过 proxy_cache_path 指令完成,例如,以下配置:

java 复制代码
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

其中:

  • /path/to/cache:这是缓存文件的存放路径。
  • levels=1:2:定义缓存目录的层级结构。
  • keys_zone=my_cache:10m:定义一个名为 my_cache 的共享内存区域,大小为 10MB。
  • max_size=10g:设置缓存的最大大小为 10GB。
  • inactive=60m:如果在 60 分钟内没有被访问,缓存将被清理。
  • use_temp_path=off:避免在文件系统中进行不必要的数据拷贝。
② 启用缓存

在 server 或 location 块中,使用 proxy_cache 指令来启用缓存,并指定要使用的 keys zone,例如,以下配置:

java 复制代码
server {  
    ...  
    location / {  
        proxy_cache my_cache;  
        ...  
    }  
}
③ 设置缓存有效期

使用 proxy_cache_valid 指令来设置哪些响应码的缓存时间,例如,以下配置:

java 复制代码
location / {  
    proxy_cache my_cache;  
    proxy_cache_valid 200 304 12h;  
    proxy_cache_valid any 1m;  
    ...  
}
④ 配置反向代理

确保你已经配置了反向代理,以便 Nginx 可以将请求转发到后端服务器。例如,以下配置:

java 复制代码
location / {  
    proxy_pass http://backend_server;  
    ...  
}
⑤ 重新加载配置

保存并关闭 Nginx 配置文件后,使用 nginx -s reload 命令重新加载配置,使更改生效。

Redis+Caffeine实现应用层二级缓存

在SpringBoot中实现多级缓存需要解决两个关键问题:缓存数据的读取顺序和数据的一致性。以下是实现多级缓存的步骤:

导入依赖:
java 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>
编写redis相关配置:
java 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0
本地缓存配置类
java 复制代码
/**
 * 本地缓存Caffeine配置类
 */
@Configuration
public class LocalCacheConfiguration {
 
    @Bean("localCacheManager")
    public Cache<String, Object> localCacheManager() {
        return Caffeine.newBuilder()
                //写入或者更新5s后,缓存过期并失效, 实际项目中肯定不会那么短时间就过期,根据具体情况设置即可
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(50)
                // 缓存的最大条数,通过 Window TinyLfu算法控制整个缓存大小
                .maximumSize(500)
            	//打开数据收集功能
                .recordStats()
                .build();
    }
 
}
Redis客户端配置类:
java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //关联
        template.setConnectionFactory(factory);
        //设置key的序列化方式
//        template.setKeySerializer();
        //设置value的序列化方式
//        template.setValueSerializer();
        return template;
    }
}
编写测试用的服务类接口:
java 复制代码
public interface UserService {
    void add(User user);
 
    User getById(String id);
 
    User update(User user);
 
    void deleteById(String id);
}
编写测试用的服务类:

这里本地缓存也可以用注解式缓存来实现,这里就不细写啦~

java 复制代码
import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.Cache;
import com.wsh.springboot_caffeine.entity.User;
import com.wsh.springboot_caffeine.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
 
@Service
public class UserServiceImpl implements UserService {
    /**
     * 模拟数据库存储数据
     */
    private static HashMap<String, User> userMap = new HashMap<>();
    private final RedisTemplate<String, Object> redisTemplate;
    private final Cache<String, Object> caffeineCache;
 
    @Autowired
    public UserServiceImpl(RedisTemplate<String, Object> redisTemplate,
                           @Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
    }
 
    static {
        userMap.put("1", new User("1", "zhangsan"));
        userMap.put("2", new User("2", "lisi"));
        userMap.put("3", new User("3", "wangwu"));
        userMap.put("4", new User("4", "zhaoliu"));
    }
 
 
    @Override
    public void add(User user) {
        // 1.保存Caffeine缓存
        caffeineCache.put(user.getId(), user);
 
        // 2.保存redis缓存
        redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
 
        // 3.保存数据库(模拟)
        userMap.put(user.getId(), user);
    }
 
    @Override
    public User getById(String id) {
        // 1.先从Caffeine缓存中读取
        Object o = caffeineCache.getIfPresent(id);
        if (Objects.nonNull(o)) {
            System.out.println("从Caffeine中查询到数据...");
            return (User) o;
        }
 
        // 2.如果缓存中不存在,则从Redis缓存中查找
        String jsonString = (String) redisTemplate.opsForValue().get(id);
        User user = JSON.parseObject(jsonString, User.class);
        if (Objects.nonNull(user)) {
            System.out.println("从Redis中查询到数据...");
 
            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);
            return user;
        }
 
        // 3.如果Redis缓存中不存在,则从数据库中查询
        user = userMap.get(id);
        if (Objects.nonNull(user)) {
            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);
 
            // 保存Redis缓存,20s后过期
            redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
        }
        System.out.println("从数据库中查询到数据...");
        return user;
    }
 
    @Override
    public User update(User user) {
        User oldUser = userMap.get(user.getId());
        oldUser.setName(user.getName());
        // 1.更新数据库
        userMap.put(oldUser.getId(), oldUser);
 
        // 2.更新Caffeine缓存
        caffeineCache.put(oldUser.getId(), oldUser);
 
        // 3.更新Redis数据库
        redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
        return oldUser;
    }
 
    @Override
    public void deleteById(String id) {
        // 1.删除数据库
        userMap.remove(id);
 
        // 2.删除Caffeine缓存
        caffeineCache.invalidate(id);
 
        // 3.删除Redis缓存
        redisTemplate.delete(id);
    }
 
}

总结

多级缓存是提升高并发系统性能的关键策略之一。它不仅能够减少系统的响应时间,提高用户体验,还能有效降低后端系统的负载,防止系统过载。在实际应用中,开发者应根据系统的具体需求和资源情况,灵活设计和调整多级缓存策略,以达到最佳的性能表现。大部分情况下我们使用redis作为缓存是可以满足需求的,加入本地缓存后虽然带来了部分性能提升,但是存在数据一致性的问题,一定程度上添加了维护难度。

相关推荐
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
岁月变迁呀2 小时前
Redis梳理
数据库·redis·缓存
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc