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,避免业务字段扩展导致反序列化失败。
相关推荐
末央&26 分钟前
【天机论坛】项目环境搭建和数据库设计
java·数据库
枫叶落雨22241 分钟前
ShardingSphere 介绍
java
花花鱼1 小时前
Spring Security 与 Spring MVC
java·spring·mvc
言慢行善2 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星2 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟2 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z2 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可2 小时前
Java 中的实现类是什么
java·开发语言
He少年2 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新2 小时前
myeclipse的pojie
java·ide·myeclipse