【Java实战㊱】Spring Boot邂逅Redis:缓存加速的奇妙之旅

目录

  • [一、Redis 概述与环境准备](#一、Redis 概述与环境准备)
    • [1.1 Redis 核心特性](#1.1 Redis 核心特性)
    • [1.2 Redis 环境搭建](#1.2 Redis 环境搭建)
    • [1.3 Redis 客户端工具](#1.3 Redis 客户端工具)
  • [二、Spring Boot 整合 Redis](#二、Spring Boot 整合 Redis)
    • [2.1 整合依赖导入](#2.1 整合依赖导入)
    • [2.2 Redis 配置](#2.2 Redis 配置)
    • [2.3 RedisTemplate 使用](#2.3 RedisTemplate 使用)
    • [2.4 StringRedisTemplate 与 RedisTemplate 对比](#2.4 StringRedisTemplate 与 RedisTemplate 对比)
  • [三、Spring Boot 缓存机制](#三、Spring Boot 缓存机制)
    • [3.1 缓存注解使用](#3.1 缓存注解使用)
    • [3.2 缓存管理器配置](#3.2 缓存管理器配置)
    • [3.3 缓存实战案例](#3.3 缓存实战案例)
    • [3.4 缓存问题解决](#3.4 缓存问题解决)

一、Redis 概述与环境准备

1.1 Redis 核心特性

Redis 作为一款高性能的键值存储数据库,在当今的软件开发领域中占据着举足轻重的地位。它基于内存存储数据,这使得它在读写速度上远远超越了传统的磁盘数据库。根据官方基准测试,Redis 的读写性能可达 10 万 +/ 秒 ,如此高效的数据处理能力,为高并发场景下的应用提供了坚实的支持。

在数据结构方面,Redis 的表现十分出色。它支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等。以电商应用为例,商品信息可以用哈希结构存储,其中商品 ID 作为键,商品的名称、价格、库存等属性作为哈希的字段和值;而用户的购物车列表则可以使用列表结构来实现,用户 ID 作为键,商品 ID 依次存入列表中。这样的设计,使得 Redis 能够灵活地满足各种复杂业务场景的需求。

持久化机制是 Redis 的另一大亮点。它提供了 RDB(Redis Database)和 AOF(Append Only File)两种持久化方式。RDB 通过快照的方式,将内存中的数据保存到磁盘上,生成的快照文件是一个紧凑的二进制文件,适合用于数据备份和恢复;AOF 则是通过记录每次写操作的命令来实现持久化,这种方式可以保证数据的完整性和一致性,即使在系统崩溃的情况下,也能通过重放 AOF 文件中的命令来恢复数据。

1.2 Redis 环境搭建

在 Windows 系统下搭建 Redis 环境,首先需要从 Redis 的 GitHub 地址(https://github.com/tporadowski/redis/releases)下载安装包。下载完成后,将压缩包解压到指定的目录,此时 Redis 就初步安装完成了。接下来,启动 Redis 的 Server,直接双击 redis-server.exe,当出现 "Server initialized" 和 "Ready to accept connections" 提示时,说明 Redis 的服务已正常启动。为了验证 redis-server 是否正常启动,可以双击 redis-cli.exe,然后输入 "PING" 命令,如果收到 "PONG" 的响应,则表示 Redis 服务运行正常。为了更方便地使用命令启动 Redis,还可以配置 Redis 的环境变量。右键点击 "此电脑",选择 "属性",再点击 "高级系统设置",在弹出的窗口中点击 "环境变量" 按钮。新建一个系统变量,变量名为 "REDIS_HOME",变量值为 Redis 的安装目录,比如 "C:\software\Redis-x64-5.0.14.1" (需根据实际安装目录填写)。然后编辑系统变量 "Path",新建一个环境变量 "% REDIS_HOME%" ,点击确定保存设置。

在 Linux 系统中安装 Redis,以 Ubuntu 系统为例,首先要更新本地包信息,执行命令 "sudo apt-get update"。然后使用 apt 包管理器安装 Redis,执行 "sudo apt-get install redis-server redis-tools" ,安装完成后,Redis 服务会自动启动。可以通过命令 "sudo systemctl status redis-server.service" 来查看 Redis 服务的状态。如果需要通过源码编译安装,首先要下载 Redis 源码,访问 Redis 官方网站找到下载地址,比如使用命令 "wget https://download.redis.io/releases/redis-5.0.7.tar.gz"。下载完成后,使用 "tar -zxvf redis-5.0.7.tar.gz" 命令解压源码,进入解压后的目录,执行 "make" 命令进行编译。编译完成后,使用 "make install PREFIX=/usr/local/redis" 命令将 Redis 安装到指定目录。

1.3 Redis 客户端工具

Redis Desktop Manager(RDM)是一款广受欢迎的 Redis 客户端工具,它为用户提供了直观、便捷的图形化界面,大大简化了对 Redis 数据库的管理和操作。

在使用 RDM 连接 Redis 服务器时,首先要确保 Redis 服务器已经启动并运行。打开 RDM 软件后,点击界面左上角的 "添加连接" 按钮,在弹出的连接窗口中,需要填写以下关键信息:连接名称可以自定义,方便用户识别不同的连接;主机地址通常为 Redis 服务器的 IP 地址,如果是本地连接,一般为 "127.0.0.1" ;端口号默认是 6379,这是 Redis 的默认端口;如果 Redis 设置了密码,还需要在 "密码" 字段中输入正确的密码。填写完这些信息后,点击 "测试连接" 按钮,若显示连接成功,再点击 "保存" 按钮即可完成连接设置。

连接成功后,就可以通过 RDM 对 Redis 进行各种操作。在左侧的导航栏中,可以看到 Redis 数据库的信息,点击某个数据库后,右侧会显示该数据库中的键(key)。通过右键点击键,可以进行查看、编辑、删除等操作。例如,要查看某个键的值,只需点击该键,右侧窗口就会显示出对应的值;若要删除某个键,右键点击该键并选择 "Delete" 选项即可。RDM 还支持执行 Redis 命令,点击界面下方的 "命令行" 选项卡,即可直接输入 Redis 命令,如输入 "KEYS *" 可以获取所有的键,输入 "SET mykey "Hello, Redis!"" 可以新建一个键值对。

二、Spring Boot 整合 Redis

2.1 整合依赖导入

在 Spring Boot 项目中,若想整合 Redis,首先要在项目的构建文件(如 pom.xml,针对 Maven 项目)中导入spring-boot-starter-data-redis依赖。这个依赖是 Spring Boot 对 Redis 操作的核心依赖,它提供了一系列的工具类和接口,使得我们可以在 Spring Boot 应用中便捷地操作 Redis 数据库,极大地简化了开发流程。

在pom.xml文件中,添加如下依赖配置:

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

当添加了这个依赖后,Maven 会自动下载该依赖及其所有相关的子依赖。在下载过程中,Maven 会从配置的远程仓库(如中央仓库)中获取所需的 JAR 包,并将它们添加到项目的类路径中。这就为后续在项目中使用 Redis 相关的功能奠定了基础,使得我们能够在代码中引入 Redis 的操作类和接口。

2.2 Redis 配置

完成依赖导入后,需要在application.yml文件中配置 Redis 的连接信息和序列化方式。下面是一个具体的配置示例:

java 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms
    timeout: 3000ms
    serializer:
      key: org.springframework.data.redis.serializer.StringRedisSerializer
      value: org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer

在这个配置中:

  • host指定了 Redis 服务器的主机地址,这里配置为localhost,表示本地连接。如果 Redis 服务器部署在远程,需要填写对应的 IP 地址。
  • port指定了 Redis 服务器的端口号,默认是 6379。
  • password用于设置 Redis 的访问密码,如果 Redis 没有设置密码,这里可以留空。
  • database指定了要使用的 Redis 数据库索引,Redis 默认有 16 个数据库,编号从 0 到 15,这里选择使用第 0 个数据库。
  • lettuce.pool配置了 Lettuce 连接池的相关参数。max-active表示最大活跃连接数,这里设置为 8,即连接池最多可以同时创建 8 个活跃连接;max-idle表示最大空闲连接数,为 8;min-idle表示最小空闲连接数,是 0;max-wait表示最大等待时间,-1ms表示无限制等待。
  • timeout设置了连接超时时间,这里是 3000ms,即 3 秒。如果在 3 秒内无法建立与 Redis 服务器的连接,将会抛出异常。
  • serializer.key和serializer.value分别指定了键和值的序列化方式。键使用StringRedisSerializer进行序列化,保证键以字符串的形式存储在 Redis 中;值使用GenericJackson2JsonRedisSerializer进行序列化,将对象转换为 JSON 格式的字符串存储,这样在读取时能够方便地反序列化为对象。

2.3 RedisTemplate 使用

RedisTemplate是 Spring Data Redis 提供的用于操作 Redis 的核心类,它提供了丰富的方法来操作 Redis 中的各种数据结构。下面通过代码示例展示如何使用RedisTemplate对 String、Hash、List 等数据结构进行操作。

假设在 Spring Boot 项目中已经配置好了RedisTemplate,并通过依赖注入的方式将其引入到一个服务类中:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // String类型数据操作
    public void setString(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public String getString(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    // Hash类型数据操作
    public void putHash(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public Object getHash(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    public Map<Object, Object> entriesHash(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List类型数据操作
    public void leftPushList(String key, Object value) {
        redisTemplate.opsForList().leftPush(key, value);
    }

    public Object rightPopList(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    public List<Object> rangeList(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }
}

在上述代码中:

  • setString方法使用opsForValue来设置 String 类型的数据,opsForValue专门用于操作 Redis 中的字符串类型数据,set方法将键值对存储到 Redis 中。
  • getString方法通过opsForValue的get方法获取指定键的字符串值。
  • putHash方法使用opsForHash来向 Hash 结构中存入数据,put方法将一个键值对(hashKey - value)存入到指定的 Hash 键(key)中。
  • getHash方法用于获取 Hash 结构中指定hashKey的值。
  • entriesHash方法可以获取整个 Hash 结构中的所有键值对,返回一个Map对象。
  • leftPushList方法使用opsForList将元素从列表的左侧插入,leftPush方法将一个元素添加到列表的头部。
  • rightPopList方法从列表的右侧弹出一个元素,rightPop方法移除并返回列表的最后一个元素。
  • rangeList方法用于获取列表中指定范围的元素,range方法返回从start索引到end索引(包括end)的所有元素。

2.4 StringRedisTemplate 与 RedisTemplate 对比

StringRedisTemplate和RedisTemplate都是 Spring Data Redis 提供的用于操作 Redis 的模板类,但它们在适用场景上存在一些差异。

StringRedisTemplate默认使用StringRedisSerializer进行序列化,这意味着它只能操作键值对都是字符串类型的数据。它在处理简单的字符串数据时非常方便,例如存储一些配置信息、简单的计数器等。由于它的序列化方式简单,直接将字符串存储到 Redis 中,所以在 Redis 客户端中查看数据时,数据是可读的明文。例如:

java 复制代码
@Autowired
private StringRedisTemplate stringRedisTemplate;

public void setStringByStringRedisTemplate(String key, String value) {
    stringRedisTemplate.opsForValue().set(key, value);
}

public String getStringByStringRedisTemplate(String key) {
    return stringRedisTemplate.opsForValue().get(key);
}

RedisTemplate默认使用JdkSerializationRedisSerializer进行序列化,它可以操作各种类型的数据,包括复杂的 Java 对象。当需要存储 Java 对象时,RedisTemplate会将对象序列化为二进制字节数组存储到 Redis 中。在 Redis 客户端中查看时,数据是不可读的二进制形式。例如:

java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

public void setObjectByRedisTemplate(String key, Object value) {
    redisTemplate.opsForValue().set(key, value);
}

public Object getObjectByRedisTemplate(String key) {
    return redisTemplate.opsForValue().get(key);
}

一般来说,如果项目中只涉及到简单的字符串数据的存储和读取,使用StringRedisTemplate可以提高开发效率,并且数据的可读性更好;而当需要存储复杂的 Java 对象,如自定义的实体类、集合等时,RedisTemplate则更合适,它能够满足对复杂数据结构的操作需求。但需要注意的是,使用RedisTemplate时,由于默认的序列化方式会导致数据在 Redis 中以二进制形式存储,可能会影响数据的可读性和跨语言操作的便利性,必要时可以自定义序列化方式,如使用 JSON 序列化器,将对象序列化为 JSON 格式的字符串存储 ,这样既能够存储复杂对象,又能保证一定的可读性。

三、Spring Boot 缓存机制

3.1 缓存注解使用

在 Spring Boot 中,缓存注解为我们提供了一种便捷的方式来实现缓存功能,大大提高了应用程序的性能和响应速度。这些注解主要包括@EnableCaching、@Cacheable、@CachePut和@CacheEvict ,它们各自有着独特的作用和用法。

@EnableCaching是开启缓存功能的关键注解,它需要被添加到 Spring Boot 的主应用类或者配置类上。例如,在主应用类中添加如下注解:

java 复制代码
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableCaching
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

当 Spring 容器启动时,会扫描到这个注解,并根据它来配置缓存相关的组件,为后续使用缓存注解奠定基础。

@Cacheable主要用于标记查询方法,它会在方法执行前检查缓存中是否已经存在该方法的返回值。如果存在,就直接从缓存中获取数据并返回,避免了方法的重复执行;如果不存在,才会执行方法,并将方法的返回值存入缓存中。它有几个常用的属性:

  • value或cacheNames:指定缓存的名称,可以是一个字符串或者字符串数组,用于标识缓存的区域。例如:@Cacheable(value = "userCache") ,表示将方法的返回值缓存到名为userCache的缓存区域中。
  • key:用于指定缓存的键。默认情况下,Spring 会使用方法的参数作为键,但也可以通过 SpEL(Spring Expression Language)表达式来自定义键。比如:@Cacheable(value = "userCache", key = "#id") ,这里使用方法的id参数作为缓存的键。
  • condition:通过 SpEL 表达式指定缓存的条件。只有当条件满足时,才会进行缓存操作。例如:@Cacheable(value = "userCache", condition = "#id > 0") ,表示只有当id参数大于 0 时,才会缓存方法的返回值。

假设有一个用户服务类UserService,其中有一个根据用户 ID 查询用户信息的方法:

java 复制代码
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 模拟从数据库查询用户信息
        User user = new User(id, "张三", 20);
        System.out.println("从数据库查询用户信息:" + user);
        return user;
    }
}

当第一次调用getUserById方法时,会执行方法体从数据库查询用户信息,并将结果存入userCache缓存中,键为传入的id值。后续再次调用该方法,并且传入相同的id时,就会直接从缓存中获取用户信息,而不会再次执行数据库查询操作,从而提高了查询效率。

@CachePut主要用于更新缓存,它会在方法执行后,将方法的返回值存入缓存中。与@Cacheable不同的是,无论缓存中是否已经存在数据,@CachePut标注的方法都会执行。它的属性与@Cacheable类似,value和key的用法相同。例如,在UserService类中添加一个更新用户信息的方法:

java 复制代码
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @CachePut(value = "userCache", key = "#user.id")
    public User updateUser(User user) {
        // 模拟更新数据库中的用户信息
        user.setName("李四");
        user.setAge(22);
        System.out.println("更新数据库中的用户信息:" + user);
        return user;
    }
}

当调用updateUser方法时,会先执行方法体更新用户信息,然后将更新后的用户对象存入userCache缓存中,键为用户对象的id。这样,下次查询该用户信息时,从缓存中获取的就是更新后的信息。

@CacheEvict用于清除缓存,可以指定清除某个缓存区域中的某个键对应的数据,或者清除整个缓存区域。它的常用属性有:

  • value或cacheNames:指定要清除的缓存区域。
  • key:指定要清除的缓存键。如果不指定key,则默认清除整个缓存区域。
  • allEntries:布尔类型,当设置为true时,表示清除整个缓存区域。例如:@CacheEvict(value = "userCache", allEntries = true) ,会清除userCache缓存区域中的所有数据。
  • beforeInvocation:布尔类型,默认值为false。当设置为true时,表示在方法调用前就清除缓存;设置为false时,表示在方法调用后清除缓存。

假设在UserService类中添加一个删除用户的方法:

java 复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @CacheEvict(value = "userCache", key = "#id")
    public void deleteUser(Long id) {
        // 模拟从数据库中删除用户信息
        System.out.println("从数据库中删除用户信息,id:" + id);
    }
}

当调用deleteUser方法时,会先执行方法体从数据库中删除用户信息,然后清除userCache缓存区域中键为id的缓存数据,保证了缓存与数据库数据的一致性。

3.2 缓存管理器配置

在 Spring Boot 中,配置 Redis 作为缓存容器,需要创建并配置RedisCacheManager。RedisCacheManager是 Spring Data Redis 提供的用于管理 Redis 缓存的管理器,它负责创建、配置和管理 Redis 缓存实例。

首先,确保项目中已经引入了spring-boot-starter-data-redis依赖。然后,在配置类中创建RedisCacheManager的 Bean。以下是一个配置示例:

java 复制代码
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 创建默认的RedisCacheConfiguration,并设置全局缓存过期时间
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
               .entryTtl(Duration.ofMinutes(5)) // 默认全局缓存过期时间为5分钟
               .disableCachingNullValues(); // 禁止缓存null值

        // 为特定缓存配置不同的过期策略
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        // 配置名为"shortLivedCache"的缓存,设置过期时间为1分钟
        cacheConfigurations.put("shortLivedCache",
                RedisCacheConfiguration.defaultCacheConfig()
                       .entryTtl(Duration.ofMinutes(1)) // 设置缓存的TTL为1分钟
                       .disableCachingNullValues()); // 禁止缓存null值

        // 配置名为"longLivedCache"的缓存,设置过期时间为1小时
        cacheConfigurations.put("longLivedCache",
                RedisCacheConfiguration.defaultCacheConfig()
                       .entryTtl(Duration.ofHours(1)) // 设置缓存的TTL为1小时
                       .disableCachingNullValues()); // 禁止缓存null值

        // 创建RedisCacheManager,加载自定义的缓存配置
        return RedisCacheManager.builder(redisConnectionFactory)
               .cacheDefaults(defaultCacheConfig) // 设置默认的缓存配置
               .withInitialCacheConfigurations(cacheConfigurations) // 加载不同缓存区域的配置
               .build();
    }
}

在上述配置中:

  • RedisCacheConfiguration.defaultCacheConfig()创建了一个默认的缓存配置对象,它包含了一些默认的设置,如序列化方式等。
  • .entryTtl(Duration.ofMinutes(5))设置了默认的缓存过期时间为 5 分钟。可以根据实际需求调整这个时间,以平衡缓存的有效性和内存使用。
  • .disableCachingNullValues()禁止缓存null值,避免在缓存中存储无效数据,浪费内存空间。

对于cacheConfigurations,它是一个Map,用于存储不同缓存区域的自定义配置。通过put方法,可以为特定的缓存区域设置不同的过期时间、序列化方式等。例如,shortLivedCache设置了较短的过期时间,适用于一些时效性较强的数据;longLivedCache设置了较长的过期时间,适合存储相对稳定的数据。

最后,通过RedisCacheManager.builder(redisConnectionFactory)创建一个RedisCacheManager的构建器,传入RedisConnectionFactory,它是用于连接 Redis 服务器的工厂。然后,使用.cacheDefaults(defaultCacheConfig)设置默认的缓存配置,.withInitialCacheConfigurations(cacheConfigurations)加载自定义的缓存配置,最后调用.build()方法构建出RedisCacheManager实例。这个实例会被 Spring 容器管理,用于后续的缓存操作。

3.3 缓存实战案例

在实际业务中,Spring Boot 的缓存机制能够显著提升系统的性能和响应速度。以下以用户信息缓存和商品列表缓存为例,详细展示其应用及性能提升效果。

用户信息缓存

在一个用户管理系统中,经常需要根据用户 ID 查询用户信息。假设存在一个UserService类,其中包含一个根据用户 ID 查询用户信息的方法getUserById。在未使用缓存之前,每次调用该方法都需要从数据库中查询数据,当并发请求较多时,数据库的压力会很大,查询性能也会受到影响。

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User getUserById(Long id) {
        // 模拟从数据库查询用户信息
        User user = new User(id, "张三", 20);
        System.out.println("从数据库查询用户信息:" + user);
        return user;
    }
}

使用 Spring Boot 的缓存机制后,在getUserById方法上添加@Cacheable注解:

java 复制代码
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 模拟从数据库查询用户信息
        User user = new User(id, "张三", 20);
        System.out.println("从数据库查询用户信息:" + user);
        return user;
    }
}

当第一次调用getUserById方法时,会执行方法体从数据库查询用户信息,并将结果存入名为userCache的缓存中,键为用户 ID。后续再次调用该方法,并且传入相同的用户 ID 时,会直接从缓存中获取用户信息,不再执行数据库查询操作。这样不仅减轻了数据库的压力,还大大提高了查询的响应速度。通过性能测试工具测试,在高并发情况下,使用缓存后的响应时间从原来的平均 200ms 降低到了 5ms 以内,性能提升效果显著。

商品列表缓存

在电商系统中,商品列表是用户经常访问的页面,展示了各种商品的信息。假设存在一个ProductService类,其中的getProductList方法用于获取商品列表数据。在未使用缓存时,每次请求商品列表都需要从数据库中查询所有商品信息,这对于数据库来说是一个较大的负担,尤其是在高并发场景下,可能会导致数据库性能下降,页面加载缓慢。

java 复制代码
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductService {

    public List<Product> getProductList() {
        // 模拟从数据库查询商品列表
        List<Product> productList = new ArrayList<>();
        Product product1 = new Product(1L, "商品1", 100.0);
        Product product2 = new Product(2L, "商品2", 200.0);
        productList.add(product1);
        productList.add(product2);
        System.out.println("从数据库查询商品列表:" + productList);
        return productList;
    }
}

使用缓存后,在getProductList方法上添加@Cacheable注解:

java 复制代码
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductService {

    @Cacheable(value = "productCache")
    public List<Product> getProductList() {
        // 模拟从数据库查询商品列表
        List<Product> productList = new ArrayList<>();
        Product product1 = new Product(1L, "商品1", 100.0);
        Product product2 = new Product(2L, "商品2", 200.0);
        productList.add(product1);
        productList.add(product2);
        System.out.println("从数据库查询商品列表:" + productList);
        return productList;
    }
}

第一次调用getProductList方法时,会执行数据库查询操作,并将商品列表数据存入productCache缓存中。后续再次请求商品列表时,直接从缓存中获取数据,无需查询数据库。经性能测试,在高并发情况下,页面加载时间从原来的平均 1 秒缩短到了 0.1 秒以内,大大提升了用户体验,同时也减轻了数据库的负载压力。

3.4 缓存问题解决

在使用缓存的过程中,可能会遇到缓存穿透、缓存击穿和缓存雪崩等问题,这些问题如果不及时解决,会对系统的性能和稳定性造成严重影响。下面将详细分析这些问题的产生原因及解决方案。

缓存穿透

缓存穿透是指查询一个在缓存和数据库中都不存在的数据,导致每次请求都绕过缓存直接访问数据库。如果有恶意用户利用这一点,大量发送不存在数据的查询请求,就会使数据库承受巨大的压力,甚至可能导致数据库崩溃。

产生原因主要有两种:一是恶意攻击,攻击者故意构造大量不存在的数据请求,以消耗数据库资源;二是业务逻辑中存在无法避免的查询不存在数据的情况,例如用户输入了错误的查询条件。

解决方案如下

  • 缓存空结果:当查询的数据在数据库中不存在时,也将空结果缓存起来,并设置一个较短的过期时间。这样,下次再查询相同的数据时,直接从缓存中获取空结果,避免了对数据库的无效查询。例如,在查询用户信息的方法中:
java 复制代码
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        // 模拟从数据库查询用户信息
        User user = null;
        // 假设这里查询数据库后没有找到用户
        System.out.println("从数据库查询用户信息,未找到:" + id);
        return user;
    }
}

这里使用@Cacheable注解的unless属性,当方法返回结果为null时,不缓存结果。但在实际业务中,可以在方法内判断结果为null时,手动将null值存入缓存,设置较短的过期时间,如 1 分钟。

  • 使用布隆过滤器:布隆过滤器是一种概率型数据结构,它可以高效地判断一个元素是否存在于集合中。在缓存穿透场景中,可以在查询数据前,先通过布隆过滤器判断数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不再查询数据库和缓存;如果判断数据可能存在,再进行正常的缓存和数据库查询。布隆过滤器的优势在于它的空间效率和查询效率都很高,能有效防止大量无效请求穿透到数据库。可以使用 Google Guava 库中的BloomFilter来实现布隆过滤器。例如:
java 复制代码
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Service
public class ProductService {

    private BloomFilter<String> bloomFilter;

    @PostConstruct
    public void initBloomFilter() {
        // 假设从数据库中获取所有商品ID
        List<String> productIds = getProductIdsFromDatabase();
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), productIds.size(), 0.01);
        for (String productId : productIds) {
            bloomFilter.put(productId);
        }
    }

    public Product getProductById(String productId) {
        if (!bloomFilter.mightContain(productId)) {
            System.out.println("布隆过滤器判断数据不存在,直接返回");
            return null;
        }
        // 进行正常的缓存和数据库查询
        Product product = getProductFromCacheOrDatabase(productId);
        return product;
    }
相关推荐
四谎真好看22 分钟前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程28 分钟前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t28 分钟前
ZIP工具类
java·zip
lang201509281 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan2 小时前
第10章 Maven
java·maven
百锦再2 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说2 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多3 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
百锦再3 小时前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven
DokiDoki之父3 小时前
Spring—注解开发
java·后端·spring