SpringBoot 集成 Redis 缓存实践

一、为什么选择 Redis 作为 SpringBoot 的缓存方案?

在高并发业务场景下,数据库往往是性能瓶颈,而 Redis 作为高性能的内存数据库,是 SpringBoot 项目缓存的最优选择之一:

  1. 性能优势:Redis 基于内存操作,单机 QPS 可达 10 万 +,远超传统关系型数据库;
  2. 集群支持:支持主从、哨兵、集群模式,满足高可用生产环境需求;
  3. SpringBoot 原生适配 :通过spring-boot-starter-data-redis快速集成,提供RedisTemplate和缓存注解双重操作方式;
  4. 丰富的过期策略:支持键级别的过期时间,可灵活控制缓存生命周期;
  5. 序列化可控:可自定义序列化规则,解决 JDK 序列化乱码、时间类型解析等问题。

二、核心配置:Redis 集群 + 序列化 + 缓存管理器

1. Maven 依赖配置(pom.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.cnhis.iho</groupId>
    <artifactId>demo-redis</artifactId>
    <name>Demo :: Redis</name>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <main.basedir>${basedir}/..</main.basedir>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Redis 核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Spring缓存抽象层(提供@Cacheable等注解) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.34</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.yml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

2. 应用配置(application.yml)

yaml 复制代码
server:
  port: 10088
spring:
  # Redis 集群配置
  redis:
    timeout: 6000ms
    password: ${REDIS_PASSWORD:123123} # 环境变量优先,默认值123123
    cluster:
      max-redirects: 3 # 集群最大重定向次数
      nodes: # 集群节点列表
        - ${REDIS_NODE0:192.168.1.210:7000}
        - ${REDIS_NODE1:192.168.1.210:7001}
        - ${REDIS_NODE2:192.168.1.210:7002}
        - ${REDIS_NODE3:192.168.1.210:7003}
        - ${REDIS_NODE4:192.168.1.210:7004}
        - ${REDIS_NODE5:192.168.1.210:7005}
    lettuce: # Lettuce连接池(SpringBoot 2.x默认)
      pool:
        max-active: 8   # 最大连接数
        max-idle: 8     # 最大空闲连接数
        min-idle: 0     # 最小空闲连接数
        max-wait: -1    # 连接等待时间(-1表示无限制)
      cluster:
        refresh:
          adaptive: true # 自适应刷新集群节点信息
  # 缓存配置:明确指定类型为Redis(增强可读性)
  cache:
    type: redis

3. 启动类:开启缓存注解

在启动类添加@EnableCaching,激活 Spring 缓存注解(@Cacheable/@CachePut/@CacheEvict):

java 复制代码
package com.demo.redis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching // 开启缓存注解支持
@SpringBootApplication
public class DemoRedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoRedisApplication.class, args);
    }

}

4. Redis 核心配置类(序列化 + 缓存管理器)

解决默认序列化乱码、时间类型解析问题,同时配置RedisCacheManager适配注解式缓存,复用序列化规则:

java 复制代码
package com.demo.redis.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
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.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Redis核心配置类
 * 1. 自定义序列化规则(解决乱码、时间类型解析问题)
 * 2. 配置RedisTemplate/StringRedisTemplate
 * 3. 配置RedisCacheManager(适配注解式缓存)
 */
@Configuration
public class RedisConfigure {
    // 全局序列化器:key用String,value用JSON(统一规则)
    public static final RedisSerializer<String> KEY_SERIALIZER;
    public static final RedisSerializer<Object> VALUE_SERIALIZER;

    static {
        KEY_SERIALIZER = new StringRedisSerializer();
        VALUE_SERIALIZER = new GenericJackson2JsonRedisSerializer(getObjectMapper());
    }

    /**
     * 自定义ObjectMapper:解决JDK8时间类型序列化/反序列化问题
     */
    private static ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 开启所有字段可见性(包括private)
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 开启类型信息存储(反序列化时识别对象类型)
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
                                           ObjectMapper.DefaultTyping.NON_FINAL, 
                                           JsonTypeInfo.As.PROPERTY);
        // 配置JDK8时间类型序列化规则
        JavaTimeModule timeModule = new JavaTimeModule();
        // LocalDate:yyyy-MM-dd
        timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        // LocalDateTime:yyyy-MM-dd HH:mm:ss
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        
        // 关闭时间戳序列化(避免时间转数字)
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 忽略未知属性(反序列化时不报错)
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 注册时间模块
        objectMapper.registerModule(timeModule);
        return objectMapper;
    }

    /**
     * 自定义RedisTemplate:适配<Object, Object>类型操作
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        // 设置key/HashKey序列化器
        redisTemplate.setKeySerializer(KEY_SERIALIZER);
        redisTemplate.setHashKeySerializer(KEY_SERIALIZER);
        // 设置value/HashValue序列化器
        redisTemplate.setValueSerializer(VALUE_SERIALIZER);
        redisTemplate.setHashValueSerializer(VALUE_SERIALIZER);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * StringRedisTemplate:适配<String, String>类型轻量操作
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(redisConnectionFactory);
        stringRedisTemplate.afterPropertiesSet();
        return stringRedisTemplate;
    }

    /**
     * RedisCacheManager:支撑注解式缓存,复用全局序列化规则
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // 1. 基础缓存配置
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(KEY_SERIALIZER))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(VALUE_SERIALIZER))
                .entryTtl(Duration.ofHours(1)) // 默认过期时间1小时
                .disableCachingNullValues() // 不缓存null(防止缓存穿透)
                .prefixCacheNameWith("demo:"); // 缓存key前缀(避免冲突)

        // 2. 构建缓存管理器(支持不同缓存名自定义过期时间)
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(cacheConfig)
                .withCacheConfiguration("user", cacheConfig.entryTtl(Duration.ofSeconds(10))) // user缓存10秒过期
                .build();
    }
}

三、实战开发:两种缓存使用方式

实现手动缓存(RedisTemplate)注解式缓存(@Cacheable) 两种方式,覆盖不同业务场景需求。

1. 定义 User 实体类

java 复制代码
package com.demo.redis.dto;

import lombok.Data;
import java.io.Serializable;

/**
 * 用户实体类(实现Serializable兜底,实际使用JSON序列化)
 */
@Data
public class User implements Serializable {
    private static final long serialVersionUID = 6643397313702951631L;

    private Long id;          // 用户ID
    private String name;      // 姓名
    private Integer age;      // 年龄
    private String email;     // 邮箱
    private String phone;     // 手机号
    private String address;   // 地址
    private Integer gender;   // 性别(1-男,0-女)
    private String username;  // 用户名
}

2. 定义业务接口

java 复制代码
package com.demo.redis.service;

import com.demo.redis.dto.User;

/**
 * 缓存演示业务接口
 */
public interface IDemoService {
    // 手动缓存(RedisTemplate)
    User getUser(Long userId);

    // 注解式缓存:查询
    User getUserById(Long userId);

    // 注解式缓存:更新
    User updateUser(User user);

    // 注解式缓存:删除
    void deleteUser(Long userId);
}

3. 业务实现类(核心)

java 复制代码
package com.demo.redis.service.impl;

import com.demo.redis.dto.User;
import com.demo.redis.service.IDemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * 缓存业务实现类
 * 1. 手动缓存:通过RedisTemplate操作
 * 2. 注解式缓存:通过@Cacheable/@CachePut/@CacheEvict
 */
@Slf4j
@Service
public class DemoServiceImpl implements IDemoService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 方式1:手动缓存(RedisTemplate)
     * 适合需要精细化控制缓存的场景
     */
    @Override
    public User getUser(Long userId) {
        String key = "user:" + userId;

        // 1. 优先从缓存获取
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            log.info("从缓存获取用户:{}", userId);
            return user;
        }

        // 2. 缓存未命中,查询数据库
        user = queryDb(userId);

        // 3. 存入缓存(30秒过期)
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
            log.info("用户数据存入缓存:{}", userId);
        }

        return user;
    }

    /**
     * 方式2:注解式缓存(@Cacheable)
     * 优先查缓存,无则查库并自动缓存
     * cacheNames="user" → 最终key:demo:user:userId
     */
    @Cacheable(cacheNames = "user", key = "#userId")
    @Override
    public User getUserById(Long userId) {
        return queryDb(userId);
    }

    /**
     * 注解式缓存:更新(@CachePut)
     * 执行方法后自动更新缓存,保证缓存与数据一致
     */
    @CachePut(cacheNames = "user", key = "#user.id")
    @Override
    public User updateUser(User user) {
        log.info("更新用户缓存:{}", user.getId());
        return user;
    }

    /**
     * 注解式缓存:删除(@CacheEvict)
     * 执行方法后自动清除对应缓存
     */
    @CacheEvict(cacheNames = "user", key = "#userId")
    @Override
    public void deleteUser(Long userId) {
        log.info("删除用户缓存:{}", userId);
    }

    /**
     * 模拟数据库查询(实际项目替换为MyBatis/JPA)
     */
    private User queryDb(Long userId) {
        log.info("查询数据库:{}", userId);
        User user = new User();
        user.setId(userId);
        user.setName("用户" + userId);
        user.setAge(25);
        user.setEmail("user" + userId + "@example.com");
        user.setPhone("1380013800" + (userId % 100));
        user.setAddress("北京市");
        user.setGender(1);
        user.setUsername("username" + userId);
        return user;
    }
}

四、测试验证:缓存功能完整性

编写测试用例,验证手动缓存和注解式缓存的核心功能:

java 复制代码
package com.demo.redis;

import cn.hutool.json.JSONUtil;
import com.demo.redis.dto.User;
import com.demo.redis.service.IDemoService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest(classes = DemoRedisApplication.class)
public class DemoServiceTest {

    @Autowired
    private IDemoService iDemoService;

    /**
     * 测试手动缓存(RedisTemplate)
     * 验证:缓存命中 → 过期 → 重新查库
     */
    @Test
    public void testGetUser() throws InterruptedException {
        // 第一次:查库 + 存缓存
        User user = iDemoService.getUser(100L);
        log.info("第一次查询:{}", JSONUtil.toJsonStr(user));
        
        // 第二次:缓存命中
        user = iDemoService.getUser(100L);
        log.info("第二次查询:{}", JSONUtil.toJsonStr(user));
        
        // 等待30秒,缓存过期
        Thread.sleep(30 * 1000);
        
        // 第三次:重新查库 + 存缓存
        user = iDemoService.getUser(100L);
        log.info("第三次查询:{}", JSONUtil.toJsonStr(user));
    }

    /**
     * 测试注解式缓存(@Cacheable/@CachePut/@CacheEvict)
     */
    @Test
    public void testUser() throws InterruptedException {
        // 1. 第一次查:库 → 缓存
        User user = iDemoService.getUserById(100L);
        log.info("getUserById 1:{}", JSONUtil.toJsonStr(user));
        
        // 2. 第二次查:缓存
        user = iDemoService.getUserById(100L);
        log.info("getUserById 2:{}", JSONUtil.toJsonStr(user));
        
        // 3. 等待10秒,user缓存过期(配置的10秒)
        Thread.sleep(10 * 1000);
        
        // 4. 第三次查:库 → 缓存
        user = iDemoService.getUserById(100L);
        log.info("getUserById 3:{}", JSONUtil.toJsonStr(user));
        
        // 5. 更新用户:同步缓存
        user.setAddress("深圳市");
        user = iDemoService.updateUser(user);
        log.info("updateUser 1:{}", user.getAddress());
        
        // 6. 验证更新后缓存
        user = iDemoService.getUserById(100L);
        log.info("getUserById 4:{}", user.getAddress());
        
        // 7. 删除用户:清除缓存
        iDemoService.deleteUser(100L);
        log.info("deleteUser 1:完成");
        
        // 8. 删除后查:库 → 缓存
        user = iDemoService.getUserById(100L);
        log.info("getUserById 5:{}", JSONUtil.toJsonStr(user));
    }

    /**
     * 测试手动缓存与注解式缓存共存
     */
    @Test
    public void testGet() throws InterruptedException {
        // 手动缓存存值
        User user = iDemoService.getUser(100L);
        log.info("getUser 1:{}", JSONUtil.toJsonStr(user));
        
        // 注解式缓存读取(注意:key不同,手动是user:100,注解是demo:user:100)
        user = iDemoService.getUserById(100L);
        log.info("getUserById 2:{}", JSONUtil.toJsonStr(user));
    }
}

测试结果分析

  1. 手动缓存

    • 第一次查询:打印查询数据库:100 + 用户数据存入缓存:100
    • 第二次查询:打印从缓存获取用户:100
    • 30 秒后第三次查询:重新打印查询数据库:100
  2. 注解式缓存

    • 前两次getUserById:仅第一次打印查询数据库:100
    • 10 秒后第三次:重新打印查询数据库:100(缓存过期);
    • updateUser后:getUserById获取到更新后的address=深圳市
    • deleteUser后:getUserById重新打印查询数据库:100

五、生产环境注意事项

1. 缓存问题防护

  • 缓存穿透 :禁用cacheNullValues(本文配置),结合参数校验 / 布隆过滤器;
  • 缓存击穿:热点 key 设置永不过期,或通过互斥锁(SETNX)控制缓存更新;
  • 缓存雪崩 :给不同缓存名设置不同过期时间(如user10 秒、order1 小时),避免集中过期;
  • 缓存一致性 :更新操作使用@CachePut,删除操作使用@CacheEvict,保证缓存与数据库同步。

2. Redis 集群优化

  • 配置lettuce.cluster.refresh.adaptive=true,自适应刷新集群节点信息;
  • 合理设置连接池参数(max-active/max-idle),避免连接数耗尽;
  • 生产环境建议配置 Redis 密码、超时时间,避免集群节点不可用导致服务阻塞。

3. 序列化注意事项

  • 实体类建议实现Serializable接口(兜底);
  • 时间类型必须自定义序列化规则,避免默认时间戳格式;
  • 禁用FAIL_ON_UNKNOWN_PROPERTIES,避免业务字段扩展导致反序列化失败。
相关推荐
怎么就重名了9 分钟前
Kivy的属性系统
java·前端·数据库
daidaidaiyu10 分钟前
一文入门 Spring Security with 单点登录(jasig)
java·spring
哈哈老师啊24 分钟前
Springboot就业管理系统bk5uv(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·spring boot·spring
chao18984425 分钟前
基于C#实现Modbus通信及CRC校验
java·开发语言·c#
hunjinYang26 分钟前
源码配置——基于Gradle搭建spring-framework-6.2.15版本阅读环境
java·后端·spring
编程饭碗28 分钟前
【Spring全局异常处理 早抛晚捕】
java·数据库·spring
咸鱼2.030 分钟前
【java入门到放弃】Elasticsearch概念
java·elasticsearch·jenkins
毕设源码-邱学长1 小时前
【开题答辩全过程】以 基于JSP论坛系统设计与实现为例,包含答辩的问题和答案
java·开发语言
找不到、了1 小时前
系统常用的限流方案实践
java
langsiming1 小时前
Redis底层实现
数据库·redis·缓存