SpringBoot(08):Redis 集成——5 分钟给你的项目加上缓存

0 阅读15分钟

SpringBoot(08):Redis 集成——5 分钟给你的项目加上缓存

img

生产环境查一个用户信息要 200ms,数据库 CPU 飙到 80%。加了一层 Redis 缓存,同样的接口降到 5ms,数据库 CPU 降到 20%。改动不到 20 行代码。这不是段子,是大多数项目接入 Redis 缓存后的真实数据。SpringBoot 集成 Redis 用 spring-boot-starter-data-redis 加上几个配置就行。但实际用下来,经常碰到这些问题:为什么有时候缓存没生效?@Cacheable 和直接用 RedisTemplate 有什么区别?序列化方式怎么选?连接池怎么配?集群模式下要注意什么?

问题:不缓存会怎样

一个典型的电商商品详情接口:

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private SkuMapper skuMapper;
    @Autowired
    private SpecMapper specMapper;
    @Autowired
    private CommentMapper commentMapper;

    public ProductDetailVO getProductDetail(Long productId) {
        Product product = productMapper.selectById(productId);
        Category category = categoryMapper.selectById(product.getCategoryId());
        List<Sku> skus = skuMapper.selectByProductId(productId);
        List<Spec> specs = specMapper.selectByProductId(productId);
        List<Comment> comments = commentMapper.selectTopNByProductId(productId, 10);

        return ProductDetailVO.build(product, category, skus, specs, comments);
    }
}

一个接口 5 次数据库查询。QPS 500 的时候,数据库每秒吃 2500 条 SQL。大促的时候 QPS 飙到 5000,数据库直接被打挂。

加上 Redis 缓存后:

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    public ProductDetailVO getProductDetail(Long productId) {
        String cacheKey = "product:detail:" + productId;
        String cached = redisTemplate.opsForValue().get(cacheKey);

        if (cached != null) {
            return JSON.parseObject(cached, ProductDetailVO.class);
        }

        ProductDetailVO detail = loadFromDb(productId);
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(detail), 30, TimeUnit.MINUTES);
        return detail;
    }
}

第一次查数据库,后续请求直接从 Redis 取。5 次查询变 0 次,接口耗时从 200ms 降到 5ms。

但这是手动缓存的写法,代码多而且容易遗漏。Spring 提供了 @Cacheable 注解,加一个注解就自动搞定。

Spring Boot Redis 集成架构

images/redis-architecture.svg Spring Boot 集成 Redis 的核心组件分四层:

  • 应用层 — 你的业务代码,通过 @Cacheable 或 RedisTemplate 操作缓存
  • 抽象层 — Spring Cache 抽象(CacheManager / Cache 接口),屏蔽底层缓存实现
  • 客户端层 — Lettuce(默认)或 Jedis,负责与 Redis Server 通信
  • 存储层 — Redis Server,单机 / 哨兵 / 集群模式

调用链路:@Cacheable → CacheManager.getCache() → RedisCache.get() → Lettuce 连接 → Redis Server

快速开始:5 分钟集成

1. 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 如果要用连接池,需要引入 commons-pool2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

Spring Boot 2.x 之后默认用 Lettuce 作为 Redis 客户端。Lettuce 基于 Netty,支持同步、异步和响应式操作,线程安全,一个连接就能处理并发请求。

如果要换成 Jedis:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

2. 配置连接

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: your_password
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 3000ms

3. 使用 RedisTemplate

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

@Service
public class UserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void setUser(String userId, String userInfo) {
        redisTemplate.opsForValue().set("user:" + userId, userInfo, 30, TimeUnit.MINUTES);
    }

    public String getUser(String userId) {
        return redisTemplate.opsForValue().get("user:" + userId);
    }
}

三步搞定。引入依赖、写配置、注入 RedisTemplate,直接用。

RedisTemplate 详解

RedisTemplate 是 Spring Data Redis 的核心类,封装了 Redis 的所有操作。

五大数据结构操作

@Service
public class RedisService {

    @Autowired
    private StringRedisTemplate redis;

    // ====== String 操作 ======
    public void stringOps() {
        redis.opsForValue().set("name", "张三");
        redis.opsForValue().set("counter", "100");
        redis.opsForValue().increment("counter");           // 101
        redis.opsForValue().set("token", "abc123", 1, TimeUnit.HOURS);

        String name = redis.opsForValue().get("name");
        Long counter = redis.opsForValue().increment("counter", 10); // 111
    }

    // ====== Hash 操作 ======
    public void hashOps() {
        redis.opsForHash().put("user:1001", "name", "张三");
        redis.opsForHash().put("user:1001", "age", "28");
        redis.opsForHash().put("user:1001", "email", "zhangsan@example.com");

        String name = (String) redis.opsForHash().get("user:1001", "name");
        Map<Object, Object> user = redis.opsForHash().entries("user:1001");
        redis.opsForHash().delete("user:1001", "email");
    }

    // ====== List 操作 ======
    public void listOps() {
        redis.opsForList().leftPush("task:queue", "task1");
        redis.opsForList().leftPush("task:queue", "task2");
        redis.opsForList().leftPush("task:queue", "task3");

        String task = redis.opsForList().rightPop("task:queue"); // task1(FIFO)
        List<String> all = redis.opsForList().range("task:queue", 0, -1);
        Long size = redis.opsForList().size("task:queue");
    }

    // ====== Set 操作 ======
    public void setOps() {
        redis.opsForSet().add("tag:article:1", "Java", "Spring", "Redis");
        redis.opsForSet().add("tag:article:2", "Java", "MySQL", "MyBatis");

        Set<String> intersection = redis.opsForSet().intersect("tag:article:1", "tag:article:2"); // [Java]
        Set<String> union = redis.opsForSet().union("tag:article:1", "tag:article:2");
        Boolean isMember = redis.opsForSet().isMember("tag:article:1", "Java"); // true
    }

    // ====== ZSet(有序集合)操作 ======
    public void zSetOps() {
        redis.opsForZSet().add("rank:score", "张三", 95.0);
        redis.opsForZSet().add("rank:score", "李四", 88.0);
        redis.opsForZSet().add("rank:score", "王五", 92.0);

        Set<String> top3 = redis.opsForZSet().reverseRange("rank:score", 0, 2); // [张三, 王五, 李四]
        Double score = redis.opsForZSet().score("rank:score", "张三"); // 95.0
        Long rank = redis.opsForZSet().reverseRank("rank:score", "李四"); // 2(第3名)
    }
}

常用操作汇总

操作方法使用场景
设置过期时间redisTemplate.expire(key, timeout, unit)缓存、Session
删除 KeyredisTemplate.delete(key)主动失效
批量删除redisTemplate.delete(keys)批量清理
判断存在redisTemplate.hasKey(key)防止重复操作
设置值+过期opsForValue().set(key, value, timeout, unit)最常用
自增opsForValue().increment(key, delta)计数器、限流
分布式锁(简易版)setIfAbsent(key, value, timeout, unit)防重复提交

分布式锁示例

@Service
public class OrderService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
        Boolean result = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }

    public void unlock(String lockKey, String requestId) {
        String value = redisTemplate.opsForValue().get(lockKey);
        if (requestId.equals(value)) {
            redisTemplate.delete(lockKey);
        }
    }

    public void processOrder(String orderId) {
        String lockKey = "lock:order:" + orderId;
        String requestId = UUID.randomUUID().toString();

        if (!tryLock(lockKey, requestId, 30)) {
            throw new BusinessException("订单正在处理中,请稍后再试");
        }

        try {
            doProcess(orderId);
        } finally {
            unlock(lockKey, requestId);
        }
    }
}

这是基于 Redis 的简易分布式锁。生产环境建议用 Redisson,它提供了可重入锁、读写锁、红锁等更完善的实现。

序列化配置

Spring Boot 默认的 RedisTemplate 用 JDK 序列化,存到 Redis 里的值是一堆二进制乱码:

\xac\xed\x00\x05sr\x00(com.example.User\x8a...

改成 JSON 序列化,Redis 里存的就是可读的 JSON 字符串:

@Configuration
public class RedisConfig {

    @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 =
                new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonFormat.Feature.EXCEPT_FOR_ANNOTATIONS
        );
        jsonSerializer.setObjectMapper(om);

        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}
序列化方式优点缺点适用场景
JdkSerializationRedisSerializer默认,无需配置不可读、体积大、有安全风险不推荐
StringRedisSerializer可读、紧凑只能存字符串Key、简单值
Jackson2JsonRedisSerializer可读、跨语言需要配置 ObjectMapper通用推荐
GenericJackson2JsonRedisSerializer自动带类型信息体积稍大需要反序列化回原类型

建议:Key 一律用 StringRedisSerializer,Value 用 Jackson2JsonRedisSerializer。如果只是存取字符串,直接用 StringRedisTemplate,不用额外配置。

@Cacheable 注解缓存

手动操作 RedisTemplate 虽然灵活,但每个方法都要写"查缓存→查数据库→写缓存"的三段式代码。Spring Cache 提供了基于注解的缓存抽象,加一个注解就搞定。

开启缓存

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

配置 CacheManager

@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();

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

    private Map<String, RedisCacheConfiguration> getCacheConfigurations() {
        Map<String, RedisCacheConfiguration> map = new HashMap<>();
        map.put("product", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)));
        map.put("user", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)));
        map.put("config", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1)));
        return map;
    }
}

四个缓存注解

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    // @Cacheable:查缓存,没有就查数据库并写入缓存
    @Cacheable(value = "product", key = "#id")
    public Product getProduct(Long id) {
        return productMapper.selectById(id);
    }

    // @CachePut:总是执行方法,把结果更新到缓存
    @CachePut(value = "product", key = "#product.id")
    public Product updateProduct(Product product) {
        productMapper.updateById(product);
        return product;
    }

    // @CacheEvict:删除缓存
    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        productMapper.deleteById(id);
    }

    // @CacheEvict:清空整个缓存区域
    @CacheEvict(value = "product", allEntries = true)
    public void clearProductCache() {
    }

    // 组合使用:列表查询也加缓存
    @Cacheable(value = "product", key = "'list:' + #category + ':' + #page + ':' + #size")
    public Page<Product> listProducts(String category, int page, int size) {
        return productMapper.selectByCategory(category, page, size);
    }
}

@Cacheable 执行流程

images/redis-cache-flow.svg

  1. 方法被调用,Spring Cache 切面拦截
  2. 根据 value(缓存名)和 key(SpEL 表达式)生成 Redis Key
  3. 查 Redis,缓存名和 key 拼接后的格式是 缓存名::key,例如 product::1001
  4. 命中 → 直接返回缓存值,不执行方法
  5. 未命中 → 执行方法,把返回值写入 Redis(带 TTL)

Key 的 SpEL 表达式

表达式含义示例
#id方法参数 idproduct::1001
#p0第一个参数product::1001
#user.id参数的属性product::1001
#result.id返回值的属性(仅 @CachePut)product::1001
'list:' + #category字符串拼接product::list:electronics

condition 和 unless

// condition:满足条件才缓存(方法执行前判断)
@Cacheable(value = "product", key = "#id", condition = "#id > 100")
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

// unless:满足条件不缓存(方法执行后判断)
@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

// 组合:id > 100 且结果不为空才缓存
@Cacheable(value = "product", key = "#id",
        condition = "#id > 100",
        unless = "#result == null || #result.status == 'DELETED'")
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

缓存常见问题

images/redis-cache-problems.svg

1. 缓存穿透

查询一个数据库里不存在的数据,缓存里当然也没有。每次请求都打到数据库。

解决方案一:缓存空值

@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

// 需要配合 CacheManager 配置,允许缓存 null 值
// 默认配置 .disableCachingNullValues() 要去掉

解决方案二:布隆过滤器

@Service
public class ProductService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final BloomFilter<Long> productBloomFilter;

    public ProductService(ProductMapper productMapper) {
        List<Long> allIds = productMapper.selectAllIds();
        productBloomFilter = BloomFilter.create(
                Funnels.longFunnel(), allIds.size() * 10, 0.01);
        allIds.forEach(productBloomFilter::put);
    }

    public Product getProduct(Long id) {
        if (!productBloomFilter.mightContain(id)) {
            return null; // 布隆过滤器说一定不存在,直接返回
        }
        // 可能存在,走正常缓存逻辑
        String cacheKey = "product:" + id;
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return JSON.parseObject(cached, Product.class);
        }
        Product product = productMapper.selectById(id);
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
        return product;
    }
}

2. 缓存击穿

某个热点 Key 过期的瞬间,大量并发请求同时打到数据库。

解决方案:互斥锁

@Service
public class ProductService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductMapper productMapper;

    public Product getProduct(Long id) {
        String cacheKey = "product:" + id;
        String cached = redisTemplate.opsForValue().get(cacheKey);

        if (cached != null) {
            return JSON.parseObject(cached, Product.class);
        }

        // 缓存不存在,用分布式锁防止并发穿透
        String lockKey = "lock:product:" + id;
        try {
            Boolean locked = redisTemplate.opsForValue()
                    .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
            if (Boolean.TRUE.equals(locked)) {
                // 拿到锁,查数据库并写缓存
                Product product = productMapper.selectById(id);
                if (product != null) {
                    redisTemplate.opsForValue().set(cacheKey,
                            JSON.toJSONString(product), 30, TimeUnit.MINUTES);
                }
                return product;
            } else {
                // 没拿到锁,等一下再试
                Thread.sleep(100);
                return getProduct(id);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取产品信息失败", e);
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

3. 缓存雪崩

大量 Key 在同一时间过期,瞬间所有请求打到数据库。

解决方案:过期时间加随机值

@Service
public class ProductService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final Random random = new Random();

    public void cacheProduct(Product product) {
        String cacheKey = "product:" + product.getId();
        int baseMinutes = 30;
        int randomMinutes = random.nextInt(10); // 0~10 分钟随机
        redisTemplate.opsForValue().set(cacheKey,
                JSON.toJSONString(product),
                baseMinutes + randomMinutes, TimeUnit.MINUTES);
    }
}

三种问题对比

问题原因解决方案
缓存穿透查不存在的数据缓存空值 / 布隆过滤器
缓存击穿热点 Key 过期互斥锁 / 永不过期
缓存雪崩大量 Key 同时过期过期时间加随机值 / 多级缓存

源码分析:Spring Data Redis 的底层实现

1. 自动配置:RedisAutoConfiguration

// org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

关键点:

  • 只要在 classpath 下有 RedisOperations 类(spring-data-redis 里的),自动配置就生效
  • 默认创建了两个 Bean:redisTemplate 和 stringRedisTemplate
  • @ConditionalOnMissingBean 意味着你自定义了 RedisTemplate,默认的就不会创建

2. Lettuce 连接工厂:LettuceConnectionFactory

// org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
public class LettuceConnectionFactory extends AbstractRedisConnectionFactory
        implements InitializingBean, DisposableBean {

    private LettuceClientConfiguration clientConfiguration;
    private LettuceConnectionProvider connectionProvider;
    private RedisStandaloneConfiguration standaloneConfig;

    @Override
    public RedisConnection getConnection() {
        LettuceConnection connection = new LettuceConnection(
                getSharedConnection(), getConnectionProvider(), getTimeout(), getDatabase());
        return connection;
    }

    @Override
    protected RedisConnection getSharedConnection() {
        // Lettuce 的特点:共享一个线程安全的连接
        StatefulRedisConnection<byte[], byte[]> connection = 
                ((LettuceConnectionProvider) this.connectionProvider)
                        .getConnection();
        return new LettuceConnection(connection, getTimeout());
    }
}

Lettuce 和 Jedis 最大的区别:

对比项LettuceJedis
连接方式单连接多线程共享每个线程一个连接
线程安全
异步支持支持(基于 Netty)不支持
连接池需要(用于扩容和冗余)必须(线程不安全)
性能高并发下更优简单场景足够

3. RedisTemplate 执行流程

// org.springframework.data.redis.core.RedisTemplate
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V> {

    private RedisSerializer<?> keySerializer = new JdkSerializationRedisSerializer();
    private RedisSerializer<?> valueSerializer = new JdkSerializationRedisSerializer();

    @Override
    public V opsForValue() {
        if (valueOps == null) {
            valueOps = new DefaultValueOperations<>(this);
        }
        return (V) valueOps;
    }

    // DefaultValueOperations.set() 内部实现
    // org.springframework.data.redis.core.DefaultValueOperations
    public void set(K key, V value, long timeout, TimeUnit unit) {
        execute((RedisCallback<Void>) connection -> {
            byte[] rawKey = rawKey(key);           // Key 序列化
            byte[] rawValue = rawValue(value);     // Value 序列化
            connection.set(rawKey, rawValue);      // 发送 SET 命令
            if (timeout > 0) {
                connection.expire(rawKey, unit.toSeconds(timeout));  // 设置过期
            }
            return null;
        }, true);
    }
}

执行链路:redisTemplate.opsForValue().set() → DefaultValueOperations.set() → RedisTemplate.execute() → LettuceConnection.set() → Redis Server

4. @Cacheable 的切面:CacheInterceptor

// org.springframework.cache.interceptor.CacheInterceptor
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        CacheOperationInvoker aopInvoker = () -> {
            try {
                return invocation.proceed();
            } catch (Throwable ex) {
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
            }
        };

        // 获取方法的缓存操作(@Cacheable 等)
        CacheOperationSource cacheOperationSource = getCacheOperationSource();
        if (cacheOperationSource != null) {
            Collection<CacheOperation> operations = 
                    cacheOperationSource.getCacheOperations(method, invocation.getThis().getClass());
            if (!CollectionUtils.isEmpty(operations)) {
                return execute(aopInvoker, invocation.getThis(), method, new Object[]{}, operations);
            }
        }
        return aopInvoker.invoke();
    }
}

// 父类 CacheAspectSupport 的核心逻辑
public abstract class CacheAspectSupport {

    @Nullable
    protected Object execute(CacheOperationInvoker invoker, Object target, Method method,
            Object[] args, Collection<CacheOperation> operations) {

        // 1. 解析缓存注解属性
        CacheOperationContexts contexts = createOperationContexts(operations, method, args, target);

        // 2. 检查缓存(@Cacheable)
        Object cachedValue = findCachedItem(contexts);
        if (cachedValue != null) {
            return cachedValue; // 缓存命中,直接返回
        }

        // 3. 缓存未命中,执行方法
        Object result = invoker.invoke();

        // 4. 把结果写入缓存(@Cacheable / @CachePut)
        cacheResult(contexts, result);

        // 5. 清除缓存(@CacheEvict)
        processEvict(contexts);

        return result;
    }
}

这段源码把 @Cacheable 的完整流程写清楚了:

  1. 解析方法上的缓存注解
  2. 根据注解属性生成 Cache Key
  3. 从 CacheManager 获取对应的 Cache 实现(RedisCache)
  4. 查 Redis:命中就直接返回,不执行方法
  5. 未命中就执行方法,把结果写入 Redis

5. RedisCache 的实现

// org.springframework.data.redis.cache.RedisCache
public class RedisCache implements Cache {

    private final String name;
    private final RedisCacheWriter cacheWriter;
    private final RedisCacheConfiguration cacheConfig;

    @Override
    public byte[] get(Object key) {
        byte[] cacheKey = createCacheKey(key);
        return cacheWriter.get(name, cacheKey);
    }

    @Override
    public void put(Object key, @Nullable byte[] value) {
        byte[] cacheKey = createCacheKey(key);
        cacheWriter.put(name, cacheKey, value, cacheConfig.getTtl());
    }

    @Override
    public byte[] createCacheKey(Object key) {
        // 实际的 Redis Key 格式:缓存名::key
        // 例如:product::1001
        String convertedKey = convertKey(key);
        if (!cacheConfig.usePrefix()) {
            return convertedKey.getBytes(StandardCharsets.UTF_8);
        }
        return cacheConfig.getKeyPrefixFor(name).getBytes(StandardCharsets.UTF_8)
                + convertedKey.getBytes(StandardCharsets.UTF_8);
    }
}

实际存到 Redis 里的 Key 格式是 缓存名::key。比如 @Cacheable(value = "product", key = "#id"),id=1001 时,Redis 里的 Key 就是 product::1001

实战:完整的缓存方案

场景:商品详情页缓存

@Service
public class ProductDetailService {

    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedisTemplate<String, Object> jsonRedisTemplate;

    private static final String PRODUCT_KEY = "product:detail:";
    private static final long CACHE_TTL_MINUTES = 30;

    public ProductDetailVO getProductDetail(Long productId) {
        String cacheKey = PRODUCT_KEY + productId;

        // 1. 查缓存
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return JSON.parseObject(cached, ProductDetailVO.class);
        }

        // 2. 缓存未命中,查数据库
        ProductDetailVO detail = loadFromDatabase(productId);

        // 3. 写缓存(随机过期时间防雪崩)
        if (detail != null) {
            int ttl = CACHE_TTL_MINUTES + new Random().nextInt(10);
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(detail), ttl, TimeUnit.MINUTES);
        } else {
            // 缓存空值防穿透,但过期时间短一点
            redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
        }

        return detail;
    }

    private ProductDetailVO loadFromDatabase(Long productId) {
        Product product = productMapper.selectById(productId);
        if (product == null) {
            return null;
        }
        List<Sku> skus = productMapper.selectSkusByProductId(productId);
        List<Spec> specs = productMapper.selectSpecsByProductId(productId);
        return ProductDetailVO.build(product, skus, specs);
    }

    // 更新商品时删除缓存
    public void updateProduct(Product product) {
        productMapper.updateById(product);
        String cacheKey = PRODUCT_KEY + product.getId();
        redisTemplate.delete(cacheKey);
    }

    // 删除商品时删除缓存
    public void deleteProduct(Long productId) {
        productMapper.deleteById(productId);
        String cacheKey = PRODUCT_KEY + productId;
        redisTemplate.delete(cacheKey);
    }

    // 批量预热缓存
    public void warmUpCache(List<Long> productIds) {
        for (Long id : productIds) {
            try {
                getProductDetail(id);
            } catch (Exception e) {
                // 预热失败不影响流程,记录日志即可
                log.warn("预热商品缓存失败, productId={}", id, e);
            }
        }
    }
}

场景:接口限流

@Service
public class RateLimitService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String userId, String api, int maxCount, int windowSeconds) {
        String key = "rate:" + userId + ":" + api;
        Long count = redisTemplate.opsForValue().increment(key);

        if (count != null && count == 1) {
            redisTemplate.expire(key, windowSeconds, TimeUnit.SECONDS);
        }

        return count != null && count <= maxCount;
    }
}

@RestController
@RequestMapping("/api")
public class ApiController {

    @Autowired
    private RateLimitService rateLimitService;

    @PostMapping("/sendSms")
    public Result sendSms(@RequestParam String phone, @RequestHeader String userId) {
        if (!rateLimitService.isAllowed(userId, "sendSms", 5, 60)) {
            return Result.fail("操作太频繁,请稍后再试");
        }
        smsService.send(phone);
        return Result.success();
    }
}

场景:排行榜

@Service
public class RankService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String RANK_KEY = "rank:score";

    public void addScore(String userId, double score) {
        redisTemplate.opsForZSet().add(RANK_KEY, userId, score);
    }

    public void incrementScore(String userId, double delta) {
        redisTemplate.opsForZSet().incrementScore(RANK_KEY, userId, delta);
    }

    public List<RankVO> getTopN(int n) {
        Set<ZSetOperations.TypedTuple<String>> tuples =
                redisTemplate.opsForZSet().reverseRangeWithScores(RANK_KEY, 0, n - 1);

        if (tuples == null) {
            return Collections.emptyList();
        }

        List<RankVO> result = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            RankVO vo = new RankVO();
            vo.setRank(rank++);
            vo.setUserId(tuple.getValue());
            vo.setScore(tuple.getScore());
            result.add(vo);
        }
        return result;
    }

    public Long getUserRank(String userId) {
        return redisTemplate.opsForZSet().reverseRank(RANK_KEY, userId);
    }

    public Double getUserScore(String userId) {
        return redisTemplate.opsForZSet().score(RANK_KEY, userId);
    }
}

连接池与集群配置

单机配置(开发环境)

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: your_password
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 3000ms
      shutdown-timeout: 100ms

哨兵配置(高可用)

spring:
  redis:
    password: your_password
    sentinel:
      master: mymaster
      nodes: 192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

集群配置(高并发)

spring:
  redis:
    password: your_password
    cluster:
      nodes: 192.168.1.101:6379,192.168.1.102:6379,192.168.1.103:6379,
             192.168.1.104:6379,192.168.1.105:6379,192.168.1.106:6379
      max-redirects: 3
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 10

连接池参数说明

参数默认值说明
max-active8最大活跃连接数
max-idle8最大空闲连接数
min-idle0最小空闲连接数
max-wait-1(无限等待)获取连接最大等待时间
timeout2000msRedis 命令超时时间

调优建议

  • max-active 设为应用最大并发数的 1.5 倍
  • max-idle 和 max-active 保持一致,避免连接频繁创建销毁
  • max-wait 设为 3~5 秒,超过就快速失败
  • 生产环境 min-idle 设为 max-active 的一半

最佳实践

1. Key 命名规范

public class RedisKeyConstants {
    // 业务:实体:ID
    public static final String PRODUCT_DETAIL = "product:detail:";
    public static final String USER_INFO = "user:info:";
    public static final String ORDER_STATUS = "order:status:";

    // 业务:操作:参数
    public static final String RATE_LIMIT = "rate:%s:%s";
    public static final String LOCK = "lock:";

    // 项目前缀(多项目共用 Redis 时)
    public static final String PREFIX = "myapp:";
}

用冒号 : 分隔层级,和 Redis 的 keyspace 通知机制配合使用。

2. 统一 TTL 管理

@Configuration
public class CacheTTLConfig {
    public static final Duration PRODUCT_TTL = Duration.ofMinutes(30);
    public static final Duration USER_TTL = Duration.ofMinutes(10);
    public static final Duration CONFIG_TTL = Duration.ofHours(24);
    public static final Duration LOCK_TTL = Duration.ofSeconds(30);
}

不要在代码里到处写魔法数字。TTL 集中管理,改起来方便。

3. 选择 RedisTemplate 还是 @Cacheable

对比项RedisTemplate@Cacheable
灵活性高,支持所有 Redis 命令低,只支持 get/put/evict
侵入性高,业务代码里混着缓存逻辑低,一个注解搞定
控制粒度精细(字段级、条件缓存)粗(方法级)
适用场景分布式锁、限流、排行榜CRUD 缓存
学习成本需要了解 Redis 命令只需理解注解

简单 CRUD 缓存用 @Cacheable,复杂场景用 RedisTemplate。两者可以共存。

4. 防止大 Key

// 不好:把整个列表存成一个 Key
redisTemplate.opsForValue().set("order:list", JSON.toJSONString(thousandsOfOrders));

// 好:分页存储
for (int i = 0; i < pages; i++) {
    List<Order> page = orders.subList(i * pageSize, (i + 1) * pageSize);
    redisTemplate.opsForValue().set("order:list:" + i, JSON.toJSONString(page), 30, TimeUnit.MINUTES);
}

单个 Key 的 Value 不要超过 10KB。大 Key 会导致 Redis 阻塞,影响所有请求。

5. 批量操作用 Pipeline

@Service
public class BatchService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void batchSet(Map<String, String> kvMap) {
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
            for (Map.Entry<String, String> entry : kvMap.entrySet()) {
                stringRedisConn.set(entry.getKey(), entry.getValue());
            }
            return null;
        });
    }

    public List<String> batchGet(List<String> keys) {
        return redisTemplate.executePipelined((RedisCallback<String>) connection -> {
            StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
            for (String key : keys) {
                stringRedisConn.get(key);
            }
            return null;
        });
    }
}

Pipeline 把多个命令打包一次发送,减少网络往返。批量操作必须用 Pipeline,否则 1000 次 get 就是 1000 次网络往返。

Lettuce vs Jedis 详细对比

对比项LettuceJedis
线程安全
连接方式单连接多线程复用连接池
异步支持不支持
响应式支持(Reactive)不支持
集群支持支持
哨兵支持支持
Pipeline支持支持
事务支持支持
底层NettySocket
Spring Boot 默认
适用场景高并发、异步操作简单场景、老项目

Spring Boot 默认选 Lettuce,大部分情况不用换。

总结

知识点要点
核心组件RedisTemplate、StringRedisTemplate、CacheManager
连接客户端Lettuce(默认,线程安全)/ Jedis
序列化Key 用 String,Value 用 JSON(Jackson2JsonRedisSerializer)
注解缓存@Cacheable / @CachePut / @CacheEvict / @Caching
缓存问题穿透(空值+布隆过滤器)、击穿(互斥锁)、雪崩(随机过期)
源码链路RedisAutoConfiguration → LettuceConnectionFactory → RedisTemplate → LettuceConnection
@Cacheable 链路CacheInterceptor → CacheAspectSupport → RedisCache → RedisCacheWriter
数据结构String(缓存)、Hash(对象)、List(队列)、Set(去重)、ZSet(排行榜)
最佳实践Key 命名规范、TTL 统一管理、防大 Key、批量用 Pipeline

Redis 集成就这些内容。核心是搞清楚两条链路:RedisTemplate 的底层调用链路和 @Cacheable 的切面执行链路。理解了这两条链路,遇到缓存不生效、序列化乱码、连接超时这些问题,就知道从哪里查。