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

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

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

问题:不缓存会怎样

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

ini 复制代码
@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 缓存后:

kotlin 复制代码
@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 集成架构

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. 引入依赖

xml 复制代码
<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:

xml 复制代码
<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. 配置连接

yaml 复制代码
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

typescript 复制代码
@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 的所有操作。

五大数据结构操作

typescript 复制代码
@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
删除 Key redisTemplate.delete(key) 主动失效
批量删除 redisTemplate.delete(keys) 批量清理
判断存在 redisTemplate.hasKey(key) 防止重复操作
设置值+过期 opsForValue().set(key, value, timeout, unit) 最常用
自增 opsForValue().increment(key, delta) 计数器、限流
分布式锁(简易版) setIfAbsent(key, value, timeout, unit) 防重复提交

分布式锁示例

typescript 复制代码
@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 里的值是一堆二进制乱码:

scss 复制代码
\xac\xed\x00\x05sr\x00(com.example.User\x8a...

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

ini 复制代码
@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 提供了基于注解的缓存抽象,加一个注解就搞定。

开启缓存

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

配置 CacheManager

arduino 复制代码
@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;
    }
}

四个缓存注解

typescript 复制代码
@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 执行流程

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

Key 的 SpEL 表达式

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

condition 和 unless

kotlin 复制代码
// 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);
}

缓存常见问题

1. 缓存穿透

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

解决方案一:缓存空值

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

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

解决方案二:布隆过滤器

kotlin 复制代码
@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 过期的瞬间,大量并发请求同时打到数据库。

解决方案:互斥锁

kotlin 复制代码
@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 在同一时间过期,瞬间所有请求打到数据库。

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

java 复制代码
@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

less 复制代码
// 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:redisTemplatestringRedisTemplate
  • @ConditionalOnMissingBean 意味着你自定义了 RedisTemplate,默认的就不会创建

2. Lettuce 连接工厂:LettuceConnectionFactory

scala 复制代码
// 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 最大的区别:

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

3. RedisTemplate 执行流程

csharp 复制代码
// 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

java 复制代码
// 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 的实现

typescript 复制代码
// 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

实战:完整的缓存方案

场景:商品详情页缓存

typescript 复制代码
@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);
            }
        }
    }
}

场景:接口限流

typescript 复制代码
@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();
    }
}

场景:排行榜

typescript 复制代码
@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);
    }
}

连接池与集群配置

单机配置(开发环境)

yaml 复制代码
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

哨兵配置(高可用)

yaml 复制代码
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

集群配置(高并发)

yaml 复制代码
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-active 8 最大活跃连接数
max-idle 8 最大空闲连接数
min-idle 0 最小空闲连接数
max-wait -1(无限等待) 获取连接最大等待时间
timeout 2000ms Redis 命令超时时间

调优建议

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

最佳实践

1. Key 命名规范

arduino 复制代码
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 管理

java 复制代码
@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

scss 复制代码
// 不好:把整个列表存成一个 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

typescript 复制代码
@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 详细对比

对比项 Lettuce Jedis
线程安全
连接方式 单连接多线程复用 连接池
异步 支持 不支持
响应式 支持(Reactive) 不支持
集群 支持 支持
哨兵 支持 支持
Pipeline 支持 支持
事务 支持 支持
底层 Netty Socket
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 的切面执行链路。理解了这两条链路,遇到缓存不生效、序列化乱码、连接超时这些问题,就知道从哪里查。

相关推荐
LiuMingXin1 小时前
意图与代码之间:AI编程范式全景解读
前端·后端·面试
用户34232323763172 小时前
边缘计算与云边协同——当采集不再只是“上传“
后端
壹方秘境2 小时前
ApiCatcher支持抓包HTTP传输大文件的实现原理分享
前端·后端·客户端
神奇小汤圆2 小时前
2026最新·最全·最实用|Java岗面试真题(已收录GitHub)
后端
神奇小汤圆2 小时前
面试官当场让我手写Java线程安全工具类,我写完直接拿到了35K offer
后端
久美子3 小时前
Qoder 使用指南:从配置到落地
后端
tyung3 小时前
Go 手写 Wait-Free MPSC 无界队列:SwapPointer 实现多生产者无锁入队
后端·go
张不才3 小时前
CPU 100% 了怎么办?Java 性能排障的标准化操作
java·后端
鱼人4 小时前
Redis、网关负载均衡为什么不能用普通取模哈希?
后端