原创

Spring Boot 使用 Redis 实战指南

为什么在 Spring Boot 中使用 Redis

在 Spring Boot 项目里接入 Redis,通常不是为了“把数据再存一份”,而是为了利用 Redis 的高性能和丰富数据结构,解决下面几类问题:

  • 缓存热点数据,减轻数据库压力
  • 保存会话信息,支持分布式登录
  • 实现分布式锁
  • 做计数器、排行榜、限流
  • 发布订阅、延迟任务、简单消息场景

如果只是“会用”,你可能只会 opsForValue().set()。但在实际项目里,更重要的是知道:Spring Boot 应该怎么配 Redis、怎么序列化、哪些场景适合、哪些坑最常见。

Spring Boot 集成 Redis 的基本方式

Spring Boot 使用 Redis,最常见的是通过 spring-boot-starter-data-redis,底层默认客户端通常是 Lettuce

1. 引入依赖

Maven:

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

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

    <!-- Jackson,用于 JSON 序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind'

2. 配置 Redis 连接

application.yml

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 
      database: 0
      timeout: 3000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: -1ms

如果是旧版本 Spring Boot,也可能使用:

spring:
  redis:
    host: 127.0.0.1
    port: 6379

实际开发中要根据你项目所用的 Spring Boot 版本选择正确的配置前缀。

最常用的两种使用方式

Spring Boot 中操作 Redis,主要有两种思路:

  1. 直接使用 RedisTemplate
  2. 使用 Spring Cache 注解

这两种都很常见,但定位不一样。


方式一:使用 RedisTemplate

RedisTemplate 更灵活,适合需要明确控制 key、value、过期时间、数据结构的场景。

1. 配置 RedisTemplate

默认的 RedisTemplate 序列化方式不够友好,常见问题是:

  • key 可读性差
  • value 被序列化成二进制
  • 不同服务之间不方便互通

通常会自定义成:key 用字符串,value 用 JSON

package com.example.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        Jackson2JsonRedisSerializer<Object> jacksonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(),
                ObjectMapper.DefaultTyping.NON_FINAL
        );
        jacksonSerializer.setObjectMapper(objectMapper);

        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

2. 基本读写示例

package com.example.demo.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

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

    public void setWithExpire(String key, Object value, long seconds) {
        redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(seconds));
    }

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

    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }
}

3. 测试接口

package com.example.demo.controller;

import com.example.demo.service.RedisService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/redis")
public class RedisController {

    private final RedisService redisService;

    public RedisController(RedisService redisService) {
        this.redisService = redisService;
    }

    @PostMapping("/set")
    public String set(@RequestParam String key, @RequestParam String value) {
        redisService.setWithExpire(key, value, 300);
        return "ok";
    }

    @GetMapping("/get")
    public Object get(@RequestParam String key) {
        return redisService.get(key);
    }

    @DeleteMapping("/delete")
    public Boolean delete(@RequestParam String key) {
        return redisService.delete(key);
    }
}

方式二:使用 Spring Cache + Redis

如果你的 Redis 主要是用来做缓存,那么直接用 Spring Cache 更省事。

1. 开启缓存

package com.example.demo;

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

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

2. 配置缓存管理器

package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
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.*;

import java.time.Duration;

@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializationContext.SerializationPair<String> keyPair =
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());

        RedisSerializationContext.SerializationPair<Object> valuePair =
                RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer(new ObjectMapper())
                );

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(keyPair)
                .serializeValuesWith(valuePair)
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues();

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

3. 使用缓存注解

package com.example.demo.service;

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id")
    public String getUserById(Long id) {
        System.out.println("查询数据库...");
        return "user-" + id;
    }

    @CachePut(value = "userCache", key = "#id")
    public String updateUser(Long id) {
        System.out.println("更新数据库...");
        return "updated-user-" + id;
    }

    @CacheEvict(value = "userCache", key = "#id")
    public void deleteUser(Long id) {
        System.out.println("删除数据库...");
    }
}

4. 三个常用注解的区别

注解 作用
@Cacheable 先查缓存,没有才执行方法并写入缓存
@CachePut 执行方法,并强制刷新缓存
@CacheEvict 删除缓存

在“查多改少”的业务里,这种方式比手写 RedisTemplate 更省代码。

Redis 常见数据结构在 Spring Boot 中的使用

Redis 不只是字符串缓存,它的数据结构很适合业务建模。

String:最常用

适合:

  • 验证码
  • token
  • 简单对象缓存
  • 计数器
redisTemplate.opsForValue().set("code:13800000000", "834521", 5, TimeUnit.MINUTES);

Hash:适合对象字段存储

适合:

  • 用户信息片段
  • 配置项
  • 商品属性
redisTemplate.opsForHash().put("user:1001", "name", "Tom");
redisTemplate.opsForHash().put("user:1001", "age", 18);
Object name = redisTemplate.opsForHash().get("user:1001", "name");

List:适合消息队列、最新记录

redisTemplate.opsForList().leftPush("news:list", "article1");
redisTemplate.opsForList().leftPush("news:list", "article2");

Set:适合去重场景

redisTemplate.opsForSet().add("user:sign:202604", "1001", "1002", "1003");

ZSet:适合排行榜

redisTemplate.opsForZSet().add("rank:score", "zhangsan", 98);
redisTemplate.opsForZSet().add("rank:score", "lisi", 99);

var top = redisTemplate.opsForZSet().reverseRange("rank:score", 0, 9);

Spring Boot 使用 Redis 的典型场景

1. 查询缓存

最典型。比如商品详情、用户资料、配置数据。

常见模式:

  • 查缓存
  • 缓存未命中则查数据库
  • 回写 Redis
  • 设置过期时间

这类场景优先考虑 @Cacheable

2. 验证码和短信码

这类数据时效性强,Redis 很适合。

redisTemplate.opsForValue().set("sms:code:13800000000", "123456", 300, TimeUnit.SECONDS);

3. 登录状态 / Token

可以把登录态写入 Redis,适合多实例部署。

例如:

  • login:token:xxx -> userInfo
  • 设置 30 分钟过期
  • 用户活跃时刷新 TTL

4. 分布式锁

例如防止重复下单、定时任务重复执行。

简单写法:

Boolean success = redisTemplate.opsForValue()
        .setIfAbsent("lock:order:1001", "1", 10, TimeUnit.SECONDS);

拿到锁后执行业务,结束后删除锁。

但生产环境里要注意:

  • 不能无脑 delete
  • 需要校验锁的持有者
  • 锁续期、异常释放都要考虑

复杂场景建议使用 Redisson,而不是手写。

5. 接口限流

可以基于 Redis 计数器做简单限流。

Long count = redisTemplate.opsForValue().increment("rate:user:1001");
if (count != null && count == 1) {
    redisTemplate.expire("rate:user:1001", 60, TimeUnit.SECONDS);
}
if (count != null && count > 100) {
    throw new RuntimeException("访问过于频繁");
}

表示某个用户 60 秒内最多请求 100 次。

实战中最容易踩的坑

1. 序列化问题

这是最常见的坑。

默认 JDK 序列化会导致:

  • Redis 里内容不可读
  • 跨语言服务不兼容
  • 占用空间更大

建议:

  • key 统一使用 StringRedisSerializer
  • value 优先考虑 JSON 序列化

2. 缓存穿透

查询一个根本不存在的数据,每次都会打到数据库。

解决方式:

  • 缓存空值
  • 布隆过滤器
  • 参数合法性校验

示例:

@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
public User getUser(Long id) {
    return userMapper.selectById(id);
}

如果你希望把空结果也缓存,需要自己设计空对象或手工处理。

3. 缓存击穿

某个热点 key 在失效瞬间,大量请求同时打到数据库。

解决方式:

  • 热点数据永不过期
  • 互斥锁
  • 逻辑过期 + 后台刷新

4. 缓存雪崩

大量 key 同时过期,数据库瞬间被压垮。

解决方式:

  • 过期时间加随机值
  • 多级缓存
  • 熔断降级

例如:

int ttl = 600 + new Random().nextInt(300);
redisTemplate.opsForValue().set("product:1001", product, ttl, TimeUnit.SECONDS);

5. key 设计混乱

项目越大,越容易出现:

  • key 冲突
  • 难以排查
  • 删除困难

建议统一规范:

业务:模块:主键
user:info:1001
order:detail:202604170001
sms:code:13800000000
lock:pay:order123

6. 大 key 问题

一个 key 里塞太多内容,比如:

  • 一个 Hash 里几十万 field
  • 一个 List 存几百万条
  • 一个 String 存超大 JSON

这会影响:

  • 网络传输
  • Redis 阻塞
  • 删除性能

建议拆分数据,避免超大对象。

RedisTemplate 和 Spring Cache 怎么选

适合 RedisTemplate 的场景

  • 需要操作 Redis 多种数据结构
  • 需要精确控制过期时间
  • 需要做分布式锁、排行榜、限流
  • 需要自定义复杂缓存逻辑

适合 Spring Cache 的场景

  • 主要目的是“给方法结果做缓存”
  • 业务模型简单
  • 希望少写模板代码
  • 重点是提升查询性能而不是玩 Redis 数据结构

可以这么理解:

  • Spring Cache 更偏业务缓存抽象
  • RedisTemplate 更偏 Redis 原生能力操作

一个更合理的项目实践建议

在实际项目里,通常不是二选一,而是组合使用:

  • 查询缓存:用 Spring Cache
  • 验证码、分布式锁、排行榜、限流:用 RedisTemplate
  • 复杂分布式能力:考虑 Redisson

这种分层方式最常见,也最清晰。

推荐的工程化写法

为了避免在业务代码里到处写 Redis 细节,建议做一层封装。

例如:

package com.example.demo.component;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void set(String key, Object value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

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

    public Boolean setIfAbsent(String key, Object value, long timeout) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
    }

    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }
}

这样业务层只关心:

  • 存什么
  • 取什么
  • 过期多久

而不是到处写重复的模板代码。

总结

Spring Boot 使用 Redis,本质上有两条主线:

  • 把 Redis 当缓存用:优先考虑 Spring Cache
  • 把 Redis 当数据结构服务用:优先考虑 RedisTemplate

真正决定实现方式的,不是“能不能连上 Redis”,而是你要解决什么问题:

  • 只是缓存查询结果:用缓存注解最快
  • 需要验证码、锁、限流、排行榜:直接上 RedisTemplate
  • 需要稳定的分布式锁和高级特性:考虑 Redisson

写 Spring Boot + Redis 代码时,最该关注的不是 API 会不会调,而是这几件事:

  • 序列化是否合理
  • key 设计是否规范
  • 过期策略是否清晰
  • 是否考虑缓存穿透、击穿、雪崩
  • 是否避免大 key 和混乱数据结构

把这些问题处理好,Redis 才会真正成为性能工具,而不是故障来源。

正文到此结束
评论插件初始化中...
Loading...