Springboot 多级缓存设计与实现

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Springboot 多级缓存设计与实现


前言

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


冗余设计理念

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


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


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

多级缓存概述

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


浏览器缓存:它的实现主要依靠 HTTP 协议中的缓存机制,当浏览器第一次请求一个资源时,服务器会将该资源的相关缓存规则(如 Cache-Control、Expires 等)一同返回给客户端,浏览器会根据这些规则来判断是否需要缓存该资源以及该资源的有效期。

Nginx 缓存:在 Nginx 中配置中开启缓存功能。

分布式缓存:所有系统调用的中间件都是分布式缓存,如 Redis、MemCached 等。

本地缓存:JVM 层面,单系统运行期间在内存中产生的缓存,例如 Caffeine、Google Guava 等。

以下是它们的具体使用。

开启浏览器缓存

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

① 配置 Cache-Control

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

response.setHeader("Cache-Control", "max-age=3600, public"); // 缓存一小时


② 配置 Expires

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

response.setDateHeader("Expires", System.currentTimeMillis() + 3600 * 1000); // 缓存一小时


③ 配置 ETag

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

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

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

long lastModifiedDate = getLastModifiedDate();
response.setDateHeader("Last-Modified", lastModifiedDate);


整体配置

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

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 指令完成,例如,以下配置:

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,例如,以下配置:

server {  
    ...  
    location / {  
        proxy_cache my_cache;  
        ...  
    }  
}
③ 设置缓存有效期

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

location / {  
    proxy_cache my_cache;  
    proxy_cache_valid 200 304 12h;  
    proxy_cache_valid any 1m;  
    ...  
}


④ 配置反向代理

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

location / {  
    proxy_pass http://backend_server;  
    ...  
}
⑤ 重新加载配置

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

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

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

导入依赖:
<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相关配置:
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
本地缓存配置类
/**
 * 本地缓存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客户端配置类:
@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;
    }
}


编写测试用的服务类接口:
public interface UserService {
    void add(User user);
 
    User getById(String id);
 
    User update(User user);
 
    void deleteById(String id);
}
编写测试用的服务类:

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

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作为缓存是可以满足需求的,加入本地缓存后虽然带来了部分性能提升,但是存在数据一致性的问题,一定程度上添加了维护难度。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
15天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
21天前
|
存储 缓存 监控
多级缓存有哪些级别?
【10月更文挑战第24天】多级缓存有哪些级别?
26 1
|
21天前
|
存储 缓存 监控
多级缓存
【10月更文挑战第24天】多级缓存
29 1
|
27天前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
37 3
|
2月前
|
缓存 NoSQL 应用服务中间件
SpringCloud基础8——多级缓存
JVM进程缓存、Lua语法、OpenResty、Nginx本地缓存、缓存同步、Canal
SpringCloud基础8——多级缓存
|
3月前
|
存储 缓存 数据库
微服务+多级缓存:性能起飞的秘籍
【8月更文挑战第29天】在当今快速迭代的软件开发领域,高性能与可扩展性是企业应用不可或缺的两大支柱。微服务架构与多级缓存策略的完美结合,正是这一追求下的璀璨明珠。今天,我们将深入探讨这一组合如何助力系统性能“起飞”,并在实际工作学习中成为技术升级的关键推手。
57 0
|
3月前
|
缓存 NoSQL Java
【Azure Redis 缓存】定位Java Spring Boot 使用 Jedis 或 Lettuce 无法连接到 Redis的网络连通性步骤
【Azure Redis 缓存】定位Java Spring Boot 使用 Jedis 或 Lettuce 无法连接到 Redis的网络连通性步骤
|
3月前
|
缓存 Java Spring
Java本地高性能缓存实践问题之在Spring Boot中启用缓存支持的问题如何解决
Java本地高性能缓存实践问题之在Spring Boot中启用缓存支持的问题如何解决
|
5月前
|
缓存 Java 数据库
springboot数据库及缓存常用依赖及配置
springboot数据库及缓存常用依赖及配置