缓存是提升接口性能的 “利器”—— 通过将频繁访问的数据暂存在内存中,减少对数据库或远程服务的依赖,从而降低响应时间、提高吞吐量。但缓存设计不当会导致 “数据不一致”“缓存雪崩” 等问题,合理的策略需平衡 “命中率” 与 “一致性”,实现高效的数据复用。
缓存的核心价值与适用场景
为什么需要缓存?
- 降低数据库压力:减少重复查询(如首页商品列表每天被查询 10 万次,缓存后可减少 90% 的数据库访问)
- 提升响应速度:内存访问(微秒级)比磁盘 IO(毫秒级)快 100-1000 倍
- 应对流量峰值:缓存可扛住更高并发(如 Redis 单机可支持 10 万 QPS,远超普通数据库)
适合缓存的数据类型
-
高频访问 + 低频修改:如商品基本信息(ID、名称、价格)、地区编码表
-
计算成本高的数据:如复杂报表(需聚合多表数据)、接口聚合结果
-
热点数据:如秒杀商品库存、热门文章详情
不适合缓存的场景:
- 实时性要求极高:如股票价格(每秒变动多次,缓存易失效)
- 低频访问数据:如一年才查一次的历史订单(缓存利用率低,浪费空间)
- 数据量过大:如 10GB 的日志文件(内存放不下,缓存成本高)
缓存的实现方案与最佳实践
1. 本地缓存:轻量快速的内存缓存
适合单机场景或无需集群共享的缓存(如配置信息),常用 Caffeine、Guava:
// 引入Caffeine依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
// 配置本地缓存
@Configuration
public class LocalCacheConfig {
// 商品信息缓存(过期时间10分钟,最大容量1000条)
@Bean
public Cache<Long, Product> productLocalCache() {
return Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.maximumSize(1000) // 最大缓存1000条
.recordStats() // 记录缓存统计(命中率等)
.build();
}
}
// 业务中使用缓存
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private Cache<Long, Product> productLocalCache;
public Product getProductById(Long id) {
// 1. 先查缓存
Product product = productLocalCache.getIfPresent(id);
if (product != null) {
return product;
}
// 2. 缓存未命中,查数据库
product = productMapper.selectById(id);
if (product != null) {
// 3. 存入缓存
productLocalCache.put(id, product);
}
return product;
}
}
本地缓存优势:
-
无网络开销:直接访问内存,速度最快
-
实现简单:无需额外部署服务
局限性:
- 集群环境下缓存不共享(如 A 机器更新了数据,B 机器缓存仍是旧值)
- 受单机内存限制,无法存储大量数据
2. 分布式缓存:集群共享的缓存方案
适合分布式系统(多机部署),常用 Redis、Memcached:
// Spring Boot集成Redis
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 商品缓存配置(过期时间5分钟)
RedisCacheConfiguration productCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 不同缓存名对应不同配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("productCache", productCacheConfig);
configMap.put("userCache", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1)));
return RedisCacheManager.builder(factory)
.withInitialCacheConfigurations(configMap)
.build();
}
}
// 用注解简化缓存操作
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 查询时自动缓存结果,缓存名productCache,key为id
@Cacheable(value = "productCache", key = "#id")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
// 更新时自动刷新缓存
@CachePut(value = "productCache", key = "#product.id")
public Product updateProduct(Product product) {
productMapper.updateById(product);
return product;
}
// 删除时自动清除缓存
@CacheEvict(value = "productCache", key = "#id")
public void deleteProduct(Long id) {
productMapper.deleteById(id);
}
}
分布式缓存优势:
-
集群共享:多台机器访问同一缓存,数据一致
-
容量大:可部署 Redis 集群,支持海量数据存储
注意事项:
- 网络开销:需通过网络访问 Redis, latency 比本地缓存高(但仍远低于数据库)
- 序列化:确保缓存对象可序列化(如用 Jackson 序列化 JSON)
3. 缓存更新策略:保证数据一致性
缓存与数据库的数据一致性是核心难题,常用策略:
-
Cache Aside(旁路缓存) :
-
读:先查缓存,未命中则查库并更新缓存
-
写:先更新数据库,再删除缓存(避免缓存脏数据)
-
// 写操作示例(更新数据库后删除缓存)
public void updateProductPrice(Long id, BigDecimal newPrice) {
// 1. 更新数据库
productMapper.updatePrice(id, newPrice);
// 2. 删除缓存(下次查询会从数据库加载新值)
redisTemplate.delete("productCache::" + id);
}
- 过期时间兜底:即使缓存未及时删除,设置合理的过期时间(如 5 分钟)也能最终保证一致性
缓存的常见问题与解决方案
1. 缓存穿透:查询不存在的数据导致缓存失效
问题:恶意请求查询不存在的 ID(如id=-1),缓存未命中,每次都查库,导致数据库压力过大。
解决方案:缓存空值(如id=-1的查询结果缓存为null,设置短期过期时间):
public Product getProductById(Long id) {
Product product = productLocalCache.getIfPresent(id);
if (product != null) {
// 区分空值和真实数据(如用特殊对象标记空值)
return product == NullProduct.INSTANCE ? null : product;
}
product = productMapper.selectById(id);
if (product == null) {
// 缓存空值,过期时间设短一点(如1分钟)
productLocalCache.put(id, NullProduct.INSTANCE, Duration.ofMinutes(1));
} else {
productLocalCache.put(id, product);
}
return product;
}
2. 缓存雪崩:大量缓存同时过期导致数据库压力骤增
问题:某一时刻大量缓存同时过期,所有请求都涌向数据库,导致数据库崩溃。
解决方案:过期时间加随机值(避免同时过期):
// 缓存时添加随机过期时间(如5-10分钟随机)
int randomSeconds = new Random().nextInt(300) + 300; // 300-600秒
redisTemplate.opsForValue().set("product:" + id, product, randomSeconds, TimeUnit.SECONDS);
3. 缓存击穿:热点数据过期瞬间被高并发请求穿透
问题:某热门商品缓存过期瞬间,1000 个并发请求同时查库,导致数据库压力过大。
解决方案:互斥锁(只允许一个请求查库,其他请求等待缓存更新):
public Product getHotProduct(Long id) {
String key = "product:" + id;
// 1. 查缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 缓存未命中,获取锁
String lockKey = "lock:product:" + id;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 3. 拿到锁,查库并更新缓存
product = productMapper.selectById(id);
redisTemplate.opsForValue().set(key, product, 5, TimeUnit.MINUTES);
} finally {
// 4. 释放锁
redisTemplate.delete(lockKey);
}
return product;
} else {
// 5. 未拿到锁,等待后重试
Thread.sleep(100);
return getHotProduct(id); // 递归重试
}
}
避坑指南
-
缓存不是越多越好:缓存会占用内存,需根据访问频率和数据大小权衡
-
避免缓存大对象:如 1MB 的大 JSON,序列化 / 传输成本高,可拆分缓存
-
监控缓存命中率:命中率低于 70% 时需优化(如调整缓存策略、增加热点数据)
-
缓存降级:系统压力大时,可关闭非核心缓存,优先保证核心业务可用
缓存策略的核心是 “用空间换时间”,但空间是有限的,需精准选择缓存对象、合理设置过期时间、妥善处理一致性问题。一个设计良好的缓存体系,能让接口性能提升 10 倍甚至 100 倍,这是后端性能优化的 “必经之路”。