一、为什么我们需要分布式缓存?
当系统QPS突破5000大关时,传统的数据库就像春运期间的火车站,开始出现严重的拥堵。某电商平台在去年双十一期间,商品详情页的数据库查询耗时达到800ms,转化率直接跳水40%。这时我们祭出了Redis这把利剑——它将商品数据的响应时间压缩到15ms以内,每秒吞吐量提升30倍。
二、Spring Boot与Redis的闪电联姻
(技术栈:Spring Boot 3.x + Redis 6.x)
2.1 基础集成配置
@Configuration
@EnableCaching
public class RedisConfig {
// 定制序列化方式(解决二进制乱码问题)
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用String序列化键
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// 使用JSON序列化值
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
return template;
}
}
2.2 缓存操作
@Service
public class ProductService {
// 注解式缓存(推荐在80%场景使用)
@Cacheable(value = "products", key = "#productId")
public Product getProduct(Long productId) {
// 模拟数据库查询
return productDao.findById(productId);
}
// 手动操作缓存(处理复杂场景)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void updateProductStock(Long productId, int delta) {
String key = "product:stock:" + productId;
BoundValueOperations<String, Object> ops =
redisTemplate.boundValueOps(key);
// 原子性增减(典型扣库存操作)
ops.increment(delta);
}
}
三、缓存一致性:数据同步的终极奥义
3.1 双写模式实战
@Transactional
public void updateProduct(Product product) {
// 先更新数据库
productDao.update(product);
// 再更新缓存
String key = "products::" + product.getId();
redisTemplate.opsForValue().set(
key,
product,
30, // 30分钟过期
TimeUnit.MINUTES
);
// 处理可能的数据不一致
if (!redisTemplate.hasKey(key)) {
log.error("缓存更新失败,ID:{}", product.getId());
throw new CacheSyncException("缓存同步异常");
}
}
3.2 失效模式进阶版
public Product getProductWithFallback(Long productId) {
String key = "products::" + productId;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 获取分布式锁(防止缓存击穿)
RLock lock = redissonClient.getLock("product_lock:" + productId);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查锁
product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
product = productDao.findById(productId);
redisTemplate.opsForValue().set(key, product, 5, TimeUnit.MINUTES);
}
}
} finally {
lock.unlock();
}
}
return product;
}
四、缓存更新策略:数据保鲜的智慧
4.1 主动更新策略
@Scheduled(fixedRate = 60 * 1000) // 每分钟更新热点数据
public void refreshHotProducts() {
List<Long> hotIds = productDao.findHotProductIds();
hotIds.parallelStream().forEach(id -> {
Product product = productDao.findById(id);
String key = "products::" + id;
redisTemplate.opsForValue().set(
key,
product,
2, // 有效期2分钟
TimeUnit.MINUTES
);
});
}
4.2 消息队列驱动的更新
@RabbitListener(queues = "cache_refresh_queue")
public void handleCacheRefresh(ProductUpdateMsg msg) {
String key = "products::" + msg.getProductId();
switch (msg.getOperationType()) {
case UPDATE:
Product product = productDao.findById(msg.getProductId());
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
break;
case DELETE:
redisTemplate.delete(key);
// 延迟二次删除(处理极端情况)
redisTemplate.expire(key, 10, TimeUnit.SECONDS);
break;
}
}
五、应用场景剖析
5.1 秒杀库存保护
public boolean seckill(Long productId) {
String stockKey = "seckill:stock:" + productId;
// Redis原子操作保证线程安全
Long remain = redisTemplate.opsForValue().decrement(stockKey);
if (remain != null && remain >= 0) {
// 异步写入数据库
mqTemplate.convertAndSend("stock_update_queue", productId);
return true;
} else {
// 库存恢复
redisTemplate.opsForValue().increment(stockKey);
return false;
}
}
5.2 分布式会话管理
@Configuration
public class SessionConfig implements WebMvcConfigurer {
@Bean
public RedisIndexedSessionRepository sessionRepository(
RedisTemplate<String, Object> redisTemplate) {
RedisIndexedSessionRepository repository =
new RedisIndexedSessionRepository(redisTemplate);
repository.setDefaultMaxInactiveInterval(1800); // 30分钟
return repository;
}
}
六、技术选型的博弈论
6.1 Redis的黄金三问
- 数据丢失恐惧症:RDB+AOF混合持久化是解药
# redis.conf配置片段
save 900 1 # 15分钟1次变更
save 300 10 # 5分钟10次变更
appendonly yes # 开启AOF
appendfsync everysec # 折衷的同步策略
- 内存膨胀危机:采用分级存储策略
// 热点数据使用内存存储
@Cacheable(value = "hotProducts", key = "#id")
public Product getHotProduct(Long id) { ... }
// 非热点数据使用Redis
@Cacheable(value = "normalProducts", key = "#id")
public Product getNormalProduct(Long id) { ... }
七、致命陷阱与逃生指南
7.1 缓存击穿的原子弹防护
public Product getProductWithBloom(Long productId) {
// 布隆过滤器检查
if (!bloomFilter.mightContain(productId)) {
return null;
}
// ...原有逻辑...
}
// 初始化布隆过滤器
@PostConstruct
public void initBloomFilter() {
List<Long> allIds = productDao.findAllIds();
allIds.forEach(id -> bloomFilter.put(id));
}
7.2 缓存雪崩的差异化过期
public void setProductCache(Long productId, Product product) {
// 基准过期时间 + 随机扰动
int baseTtl = 30 * 60; // 30分钟
int randomTtl = ThreadLocalRandom.current().nextInt(0, 300);
redisTemplate.opsForValue().set(
"products::" + productId,
product,
baseTtl + randomTtl,
TimeUnit.SECONDS
);
}
八、综合评估与战略选择
8.1 性能测试数据对比
在百万级商品数据的压力测试中:
- 纯数据库方案:QPS 1200,平均响应时间 450ms
- Redis缓存方案:QPS 98000,平均响应时间 8ms
内存占用方面,采用合理的序列化策略后:
- 1MB原始JSON数据
- Java序列化后占用 1.8MB
- Kryo序列化后仅 650KB
九、通向缓存大师之路
通过三年的实战经验总结,这三个真理值得铭记:
- 数据一致性:永远不要在业务高峰时强制刷新全量缓存
- 容量规划:预留30%的内存空间应对突发流量
- 监控体系:建议监控以下核心指标:
- 缓存命中率(建议维持80%以上)
- 内存碎片率(控制在1.5以下)
- 网络带宽使用率(不超过70%)
在未来的云原生时代,Redis可能演变成内存数据库+持久化存储的混合形态。但无论如何演进,理解缓存一致性本质、掌握数据同步策略,始终是我们应对高并发场景的看家本领。
评论