spring boot集成redis缓存

什么场景下需要redis缓存

高并发场景下,读多写少的场景,如商品详情页,用户信息,首页推荐等,避免重复,频繁的查询数据库,使用rendis缓存临时数据,避免所有请求压力都发送到mysql中
接口响应要求高的:要求接口毫秒级返回,直接查询db慢,将热点数据放到缓存,绕过数据库
分布式系统共享数据:多台微服务实例,本地缓存不共享,通过redis做分布式共享缓存
限流,防重,分布式锁:接口限流,验证码防刷,订单重复提交,分布式事务锁可以使用redis
临时数据,过期数据:短信验证码,登录token,临时令牌,下单倒计时,购物陈临时数据,自带过期淘汰机制
排行耪,计数器,点在浏览量:Redis的Zset,String技术器,hash结构天然适配,比mongo DB 性能强

什么时候不建议使用:

写多读少,数据频繁变更:每次都要更新缓存,维护成本高,缓存命中率极低,没有必要加
强一致性要求极高:金融核心业务,核心账务,不能容忍缓存和DB短暂不一致,慎用普通旁路缓存
数据量超大,冷数据多:全量塞进redis成本高,适合ES,对象存储,数据库分库分表

怎么实现redis缓存

实际情况还要添加mysql的等数据库以及对应的jpa依赖和连接用来提取数据放入到缓存中
pom.xml引入相关依赖(maven版本)

<!-- Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- Spring Data JPA(操作数据库) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Redis 缓存 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Redis 连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

配置连接信息(yml文件)

spring:
  # 1. MySQL 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  # 2. JPA 配置
  jpa:
    hibernate:
      ddl-auto: update # 生产环境建议改为 validate 或 none
    show-sql: true
    properties:
      hibernate:
        format_sql: true

  # 3. Redis 配置 (连接池等基础连接信息)
  data: # 注意:Spring Boot 2.x+ 推荐将 redis 放在 spring.data 下,虽然 spring.redis 也兼容
    redis:
      host: localhost
      port: 6379
      password: # 如果没密码留空即可,不要写 null
      database: 0
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: -1ms

  # 4. Cache 配置 (Spring Cache 抽象层配置)
  cache:
    type: redis # 指定使用 Redis 作为缓存提供者
    redis:
      time-to-live: 3600000  # 默认缓存过期时间 1小时 (毫秒)
      cache-null-values: true # 允许缓存 null 值 (防止缓存穿透)
      # key-prefix: "my_cache:" # 可选:自定义缓存键的前缀
      # use-key-prefix: true   # 可选:是否使用前缀

编写对应的redisConfig类:主要要实现redisTemplate解决序列化乱码问题,RedisCacheManager配置缓存管理器配置:序列化、过期时间、key 前缀、是否缓存 null、禁用默认缓存行为

@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 1. 配置 RedisTemplate
     * 用于手动操作 Redis (如 redisTemplate.opsForValue().set(...))
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // Key 采用 String 序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // Value 采用 JSON 序列化
        Jackson2JsonRedisSerializer<Object> jsonSerializer = createJsonSerializer();

        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 2. 配置 CacheManager
     * 用于 @Cacheable, @CachePut, @CacheEvict 注解
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 创建 JSON 序列化器
        Jackson2JsonRedisSerializer<Object> jsonSerializer = createJsonSerializer();

        // 配置缓存策略
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置默认过期时间 (建议与 YAML 保持一致,或者这里统一管理)
                .entryTtl(Duration.ofHours(1)) 
                // 禁用空值缓存 (根据业务需求,如果允许缓存穿透则设为 true)
                .disableCachingNullValues() 
                // 设置 Key 序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置 Value 序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer))
                // 计算前缀: 格式为 "cacheName::key" (Spring Data Redis 默认行为)
                .computePrefixWith(cacheName -> cacheName + "::");

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }

    /**
     * 提取公共的 JSON 序列化器创建逻辑,确保 RedisTemplate 和 CacheManager 使用相同的序列化规则
     */
    private Jackson2JsonRedisSerializer<Object> createJsonSerializer() {
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        
        // 【重要】解决多态类型序列化的安全问题,替代已废弃的 activateDefaultTyping
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        
        serializer.setObjectMapper(om);
        return serializer;
    }
}

定义好缓存之后就是mysql部分的数据库建表,建表后的实体对象DAO并创建对应的reporsity接口,在service层调用该接口进行数据查询

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer age;

    // getter setter 构造器
}
public interface UserRepository extends JpaRepository<User, Long> {
}

服务层:缓存注解生效的地方

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 查询:先查缓存,没有就查 MySQL,然后存入 Redis
    @Cacheable(value = "user", key = "#id")
    public User getById(Long id) {
        // 只会在第一次执行,之后都走 Redis
        System.out.println("从 MySQL 查询用户:" + id);
        return userRepository.findById(id).orElse(null);
    }

    // 更新:更新 MySQL,同时更新 Redis
    @CachePut(value = "user", key = "#user.id")
    public User update(User user) {
        System.out.println("更新 MySQL,并更新缓存");
        return userRepository.save(user);
    }

    // 删除:删除 MySQL 数据,同时删除 Redis 缓存
    //这里会将key 设置为user::id,可以根据前缀进行配置,.computePrefixWith(cacheName -> cacheName + "::")
    //value就是这里的之前配置的序列化,将用户dao信息进行序列化后存储,读取时也是采用反序列化的方式
    @CacheEvict(value = "user", key = "#id")
    public void delete(Long id) { 
        userRepository.deleteById(id);
    }
}

如果在上述基础上要想实现自定义key的样式则要采用:

Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        // 逻辑:类名_方法名_参数1_参数2
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getSimpleName()).append("_");
        sb.append(method.getName()).append("_");
        
        for (Object param : params) {
            if (param != null) {
                sb.append(param.toString());
            } else {
                sb.append("NULL");
            }
            sb.append("_");
        }
        
        // 去掉最后一个下划线
        if (sb.length() > 0) {
            sb.setLength(sb.length() - 1);
        }
        
        return sb.toString();
    }
}

//然后再注解中使用
@Service
public class UserService {

    // 使用自定义的 Key 生成器
    // Redis Key 最终可能是: user::UserService_getById_1 (依然会有 value 前缀)
    @Cacheable(value = "user", keyGenerator = "customKeyGenerator")
    public User getById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

如果在上述基础上增加spring boot scheduler进行定时缓存有应该如何做
先在配置文件夹下实现 SchedulingConfigurer 接口(避免单线程指定定时任务出现阻塞)
然后在创建定时执行任务的执行代码逻辑,进行执行,如果时分布式场景下,要记得给每个分布式进行加锁,避免并发执行问题

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 创建一个大小为 5 的线程池
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
    }
}
//定时任务 cron指定定时执行任务时间
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class DistributedCacheTask {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String LOCK_KEY = "lock:cache_warmup_task";

    @Scheduled(cron = "0 0 2 * * ?")
    public void warmUpWithLock() {
        // 尝试获取锁,设置过期时间 10 分钟(防止死锁)
        Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "1", 10, TimeUnit.MINUTES);
        
        if (Boolean.TRUE.equals(isLocked)) {
            try {
                System.out.println("获取锁成功,执行缓存预热...");
                // 执行具体的缓存逻辑
                doWarmUp(); 
            } finally {
                // 释放锁
                stringRedisTemplate.delete(LOCK_KEY);
            }
        } else {
            System.out.println("其他实例正在执行,跳过本次任务");
        }
    }
    
    private void doWarmUp() {
        // ... 业务逻辑
    }
}

注意:上述代码编写完成后,要在spring boot的启动类 application上加上enableCache和EnableSchedule注解,保证任务可以正常启动

面试会问道的一些问题

1 如何保证数据库和缓存数据的一致性
为了保证数据库和缓存的一致性,我们通常采用 Cache Aside Pattern(旁路缓存模式)。
写操作:先更新数据库,再删除缓存。
理由:如果先删缓存,在高并发下,读请求可能在数据库更新前将旧数据回填到缓存,导致长时间的脏数据。而‘先更后删’即使出现并发,也只会导致极短时间的旧缓存读取,且随后的读请求会重新从数据库加载最新数据,实现最终一致性。
为什么删除而不是更新:删除可以避免并发写场景下的复杂竞态条件,简化逻辑。
读操作:先读缓存,命中则返回;未命中则读数据库,并将结果写入缓存。
兜底策略:为缓存设置合理的 TTL(过期时间)。
理由:防止因网络异常或代码 bug 导致‘删除缓存’步骤失败,从而产生永久脏数据。TTL 确保即使删除失败,数据也会在一定时间后自动失效,强制从数据库回源。
极端场景:如果对一致性要求极高,可采用延时双删策略,或通过监听 MySQL Binlog 异步清理缓存

导航