1. 为什么需要Spring Cache与Redis结合?
在企业级应用开发中,缓存是提升系统性能的银弹。我经历过一个电商项目,当QPS突破5000时,数据库开始频繁告警。当时我们尝试了各种优化手段,最终发现80%的请求都在重复读取相同商品信息——这正是缓存该出手的地方。
Spring Cache作为框架层面的抽象层,提供了一套声明式的缓存解决方案。它最大的价值在于:开发者只需通过简单的注解(如@Cacheable)就能实现缓存逻辑,无需关心底层实现细节。而Redis作为内存数据库,相比传统Memcached具有更丰富的数据结构和持久化能力,特别适合作为Spring Cache的存储后端。
这种组合带来的直接好处是:
- 代码侵入性极低:原本需要手动编写的缓存逻辑现在用注解就能搞定
- 维护成本大幅降低:更换缓存方案只需修改配置,业务代码零改动
- 性能提升立竿见影:实测某核心接口响应时间从200ms降至20ms
2. 环境准备与基础配置
2.1 Redis安装与验证
在Windows环境下,我推荐使用官方提供的Redis for Windows版本。虽然生产环境通常部署在Linux上,但开发时Windows版本足够用。安装后通过以下命令验证:
bash复制redis-cli ping
# 应返回 PONG
对于Linux环境,通过包管理器安装更便捷:
bash复制# Ubuntu/Debian
sudo apt install redis-server
# CentOS/RHEL
sudo yum install redis
2.2 Spring Boot项目配置
在pom.xml中添加必要依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml配置示例:
yaml复制spring:
cache:
type: redis
redis:
time-to-live: 600000 # 默认缓存10分钟
redis:
host: localhost
port: 6379
password:
database: 0
注意:生产环境务必设置password并启用SSL,我曾因疏忽这点导致数据泄露事故
3. 核心注解实战解析
3.1 @Cacheable 缓存查询
这是最常用的注解,适用于查询方法:
java复制@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 模拟数据库查询
return productRepository.findById(id).orElse(null);
}
几个关键点:
- value:指定缓存名称(对应Redis的key前缀)
- key:SpEL表达式,决定缓存键的生成规则
- 方法返回值不能为void
我遇到过的一个坑:当返回值为Optional时,Redis序列化会出问题。解决方案是像上面示例那样提前解包。
3.2 @CachePut 更新缓存
用于保证缓存与数据库同步:
java复制@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
特别注意:@CachePut总会执行方法体,与@Cacheable的行为完全不同。曾经有同事误用导致性能问题。
3.3 @CacheEvict 删除缓存
商品下架时的典型用法:
java复制@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
清除整个缓存区域:
java复制@CacheEvict(value = "products", allEntries = true)
public void refreshAllProducts() {
// 通常无需实现方法体
}
4. 高级配置与优化
4.1 自定义序列化方案
默认的JDK序列化有两大问题:
- 可读性差,Redis中查看是二进制
- 不同JVM版本可能不兼容
改用Jackson序列化的配置:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
4.2 缓存穿透防护
当查询不存在的商品ID时,可能会被恶意攻击利用。解决方案:
java复制@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProductWithNullCheck(Long id) {
Product product = productRepository.findById(id).orElse(null);
if(product == null) {
// 记录异常查询
metricService.logInvalidQuery(id);
}
return product;
}
配合Redis的布隆过滤器效果更佳,但实现较复杂,需要自定义CacheManager。
4.3 缓存雪崩预防
设置不同的TTL可以有效避免缓存集体失效:
java复制@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("products",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10 + new Random().nextInt(5))));
return RedisCacheManager.builder(factory)
.withInitialCacheConfigurations(configs)
.build();
}
5. 监控与问题排查
5.1 Redis监控命令
关键命令:
INFO stats:查看命令统计MONITOR:实时观察所有命令(慎用,影响性能)SLOWLOG GET:获取慢查询
5.2 Spring Actuator集成
添加依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置启用缓存端点:
yaml复制management:
endpoints:
web:
exposure:
include: caches
访问/actuator/caches可查看所有缓存区域及命中情况。
5.3 常见问题解决方案
缓存不一致:采用Cache-Aside模式,先更新数据库再删除缓存。我曾遇到双写导致的数据不一致,最终通过引入消息队列异步处理解决。
内存溢出:为不同缓存区域设置合理的TTL和最大数量。某次大促前,我们通过CONFIG SET maxmemory-policy allkeys-lru避免了OOM。
连接泄漏:确保正确关闭RedisTemplate的Connection。建议使用try-with-resources:
java复制try(RedisConnection conn = redisTemplate.getConnectionFactory().getConnection()) {
// 操作
}
6. 性能调优实战
6.1 Pipeline批量操作
对比普通操作与Pipeline的性能差异:
java复制// 普通方式:100次set耗时约200ms
for(int i=0; i<100; i++) {
redisTemplate.opsForValue().set("key"+i, "value"+i);
}
// Pipeline方式:100次set耗时约20ms
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for(int i=0; i<100; i++) {
connection.set(("key"+i).getBytes(), ("value"+i).getBytes());
}
return null;
});
6.2 Lua脚本应用
实现原子性的库存扣减:
lua复制-- decrement.lua
local current = redis.call('GET', KEYS[1])
if not current or tonumber(current) < tonumber(ARGV[1]) then
return 0
end
return redis.call('DECRBY', KEYS[1], ARGV[1])
Java调用方式:
java复制DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/decrement.lua")));
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList("stock:"+productId),
String.valueOf(quantity));
6.3 连接池优化
推荐配置(根据实际压力调整):
yaml复制spring:
redis:
lettuce:
pool:
max-active: 50 # 最大连接数
max-idle: 20 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 2000 # 获取连接最大等待时间(ms)
我曾将max-active从默认8调到50,系统吞吐量提升了3倍,但要注意不要超过Redis的maxclients配置。
7. 分布式场景下的特殊处理
7.1 缓存预热策略
系统启动时自动加载热点数据:
java复制@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
List<Long> hotProductIds = productRepository.findHotProductIds();
hotProductIds.parallelStream().forEach(id -> {
getProductById(id); // 触发缓存
});
}
7.2 分布式锁实现
基于Redis的RedLock算法:
java复制public boolean tryLock(String lockKey, long expireSeconds) {
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
if(Boolean.TRUE.equals(acquired)) {
// 成功获取锁
RedisLockRegistry.registerLockHolder(lockKey, lockValue);
return true;
}
return false;
}
public void unlock(String lockKey) {
String currentValue = redisTemplate.opsForValue().get(lockKey);
String heldValue = RedisLockRegistry.getLockHolder(lockKey);
if(heldValue != null && heldValue.equals(currentValue)) {
redisTemplate.delete(lockKey);
RedisLockRegistry.unregisterLockHolder(lockKey);
}
}
重要:必须检查锁的持有者,避免误删其他实例的锁。我们曾因此导致订单重复处理。
7.3 多级缓存架构
结合本地Caffeine与Redis:
java复制@Configuration
@EnableCaching
public class MultiLevelCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
CaffeineCacheManager localCacheManager = new CaffeineCacheManager();
localCacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(1000));
RedisCacheManager redisCacheManager = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)))
.build();
return new CompositeCacheManager(localCacheManager, redisCacheManager);
}
}
这种架构下,请求会先查本地缓存,未命中再查Redis。适合读多写少且数据变化不频繁的场景。
