一、缓存这杯奶茶到底该什么时候续杯
当代互联网服务就像24小时营业的奶茶店,顾客(客户端)随时要喝新鲜奶茶(数据),店员(数据库)既要保证制作速度(响应时间)又要维持口感(数据一致性)。这时候后厨的备料台(缓存)就成了关键角色。当有人点单「波霸奶茶」时,你首先要考虑的是:
- 直接从备料台取波霸?(缓存命中)
- 波霸不够了马上现煮?(缓存穿透)
- 新煮好的波霸要不要立即补满备料台?(缓存更新)
这就是缓存策略的核心命题。今天我们将聚焦MySQL场景下最典型的两种缓存更新策略,就像分析奶茶店的中央厨房管理系统一样,看看Cache-Aside和Write-Through各自的绝活和软肋。
二、Cache-Aside策略:精明掌柜的生意经
1. 基本原理
这个策略的中文名叫「缓存旁路」,就像茶馆里跑堂的小二,永远遵循客人吩咐:
- 读数据时:「客官稍等,我先看看柜台上有没有备好的茶点」(查缓存)
- 写数据时:「掌柜的,后厨的西湖龙井到货了,需要现在摆上柜台吗?」(只更新数据库)
2. 标准操作流程
// 技术栈:Java + Spring Boot + MyBatis + Caffeine缓存
// 用户服务类
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
// 如果缓存未命中,执行数据库查询
System.out.println("==> 执行数据库查询:" + userId);
return userMapper.selectById(userId);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
// 先更新数据库
userMapper.updateById(user);
// 缓存交由注解自动清除
}
}
// 在启动类添加缓存配置
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(1000));
return manager;
}
}
这段代码展示了典型的Cache-Aside模式:
- 查询时自动优先查缓存
- 更新时先改数据库再删缓存
- 下次查询未命中时自动回源
3. 策略亮点
- 灵活经济:就像只在旺季多雇临时工,资源使用弹性大
- 天然防雪崩:每个缓存项独立失效,不会集体崩溃
- 版本控制简单:缓存与源数据解耦,更新像换菜单一样方便
4. 三大致命陷阱
- 缓存击穿连环案(某个热点数据失效瞬间被万人捶)
// 缓存空值解决方案
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUserWithNullCache(Long userId) {
User user = userMapper.selectById(userId);
if(user == null){
// 创建占位对象
return new NullUser();
}
return user;
}
- 双写一致性迷局(数据库更新成功但缓存删除失败)
- 冷启动雪崩(系统重启后缓存集体罢工)
三、Write-Through策略:强迫症管家的完美主义
1. 运行原理
这种策略像严谨的英国管家,坚持所有操作必须同步完成:
- 写数据时:必须同时更新缓存和数据库,否则宁可不做
- 读数据时:永远信任缓存里的数据是新鲜的
2. 示范性实现
// 技术栈:Java + MyBatis + Redis
// 自定义缓存拦截器
@Component
@Intercepts({
@Signature(type= Executor.class, method="update", args={MappedStatement.class,Object.class})
})
public class WriteThroughInterceptor implements Interceptor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取更新参数
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 执行原始更新操作
Object result = invocation.proceed();
// 如果更新成功,同步更新缓存
if (result instanceof Integer && (Integer)result > 0) {
if (parameter instanceof User) {
User user = (User) parameter;
String cacheKey = "user:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, user);
System.out.println("==> 同步更新缓存:" + cacheKey);
}
}
return result;
}
}
// 用户数据访问接口
public interface UserMapper {
@Update("UPDATE users SET name=#{name} WHERE id=#{id}")
int updateUser(User user);
}
这个实现特点:
- 通过MyBatis拦截器实现AOP
- 自动捕获所有update操作
- 确保数据库与缓存原子更新
3. 先天优势
- 数据一致性VIP:像银行金库般严格
- 读写性能平稳:避免剧烈波动
- 架构简洁:减少逻辑分支
4. 三大现实挑战
- 写放大效应:简单修改用户昵称也要全量更新用户对象
- 冷数据拖累:半年没人看的用户信息霸占缓存
- 事务复杂度:双写操作需要考虑分布式事务
四、双雄会武:六大维度终极PK
| 对比维度 | Cache-Aside | Write-Through |
|---|---|---|
| 一致性保障 | 最终一致性(可能有暂态不一致) | 强一致性 |
| 系统复杂度 | 中(需要处理各种异常情况) | 高(需保证双写原子性) |
| 适用场景 | 读多写少 | 写密集型 |
| 性能波动 | 查询延迟不稳定 | 读写延迟可预测 |
| 数据新鲜度 | 可能存在旧数据 | 总是最新 |
| 实现成本 | 低(大多数框架原生支持) | 高(需要自定义实现) |
五、选择困难症的救命指南
1. 何时该选Cache-Aside?
- 社交平台动态流:允许短暂的不一致(用户晚几秒看到新评论无所谓)
- 商品详情页:可接受缓存击穿时短暂回源
- 运营活动页面:突发流量下的弹性伸缩
2. Write-Through的黄金搭档
- 金融账户系统:余额变动必须立即可见
- 医疗诊疗系统:检查报告更新必须实时同步
- 物联网控制台:设备状态需要精确同步
3. 混合模式案例
// 关键业务订单状态更新
public class OrderService {
@Transactional
public void payOrder(Long orderId) {
// 1. 数据库操作
orderMapper.updateStatus(orderId, PAID);
// 2. 同步更新缓存
redisTemplate.opsForValue().set("order:"+orderId, getLatestOrder(orderId));
// 3. 异步刷新周边缓存
mqSender.sendCacheRefreshEvent("order_detail:"+orderId);
}
}
这种组合拳:
- 核心状态强一致(Write-Through)
- 周边数据最终一致(Cache-Aside)
- 用消息队列异步更新
六、避坑者的自我修养
1. 通用生存法则
- 缓存TTL设置建议:
// 采用随机抖动避免集体失效 Duration ttl = Duration.ofMinutes(30).plusSeconds(new Random().nextInt(300)); - 监控缓存命中率(建议维持80%以上)
- 分级缓存策略(本地缓存+分布式缓存)
2. 特别警示
- 禁止魔法值删除:缓存key必须有完整命名空间
- 防御性编程:缓存操作必须包裹熔断器
- 容量规划:缓存使用率不超过70%
七、技术栈选型核心参考
- Redis派:适合分布式场景,但要注意网络延迟
- Memcached派:超高频写入场景首选
- 本地缓存派(Caffeine/Guava):微服务内部缓存最优选
八、未来战场前瞻
- 新一代缓存中间件支持自动策略切换
- 机器学习预测缓存失效时间
- 持久化内存带来的革新
评论