《解决 MySQL 与 Redis 双写矛盾:Java 开发日记里的实战思路与避坑要点》
·
解决 MySQL 与 Redis 双写矛盾:Java 开发日记里的实战思路与避坑要点
一、问题现场:当缓存与数据库“打架”
上周线上出现诡异现象:商品库存更新后,前端仍显示旧数据。根本矛盾在于:
- 先更新数据库还是先删缓存?
- 如何保证两步操作的原子性?
- 缓存穿透/雪崩如何规避?
// 错误示范:非原子操作导致脏数据
public void updateProduct(Product product) {
mysql.update(product); // 步骤1:更新数据库
redis.del("product:" + product.getId()); // 步骤2:删除缓存
// 若步骤2失败,后续请求将读取到旧缓存
}
二、双写一致性核心方案
方案1:延迟双删策略(强一致性场景)
public void updateWithDelayDelete(Product product) {
// 1. 先删除缓存
redis.del("product:" + product.getId());
// 2. 更新数据库
mysql.update(product);
// 3. 延迟500ms再次删除缓存(处理并发读产生的脏数据)
Thread.sleep(500);
redis.del("product:" + product.getId());
}
适用场景:金融、库存等强一致性业务
避坑点:
- 延迟时间需根据业务压力动态调整
- 必须配合消息队列重试机制(防止删除失败)
方案2:异步监听 Binlog(最终一致性场景)

通过 Canal 监听 MySQL Binlog,异步更新缓存:
// Canal 客户端伪代码
public void onBinlogEvent(BinlogEvent event) {
if (event.isUpdate() && event.getTable().equals("product")) {
Product product = parse(event.getData());
redis.set("product:" + product.getId(), product);
}
}
优势:
- 解耦业务代码与缓存逻辑
- 天然支持主从延迟场景
坑点: - 需处理消息积压问题(Kafka 分区策略优化)
- 避免循环更新(过滤无关表变更)
三、Java 实战避坑清单
坑1:缓存穿透的防御姿势
// 查询商品时:缓存空值避免穿透
public Product getProduct(long id) {
String key = "product:" + id;
Product product = redis.get(key, Product.class);
if (product == null) {
product = mysql.query(id);
if (product == null) {
// 缓存空值(设置短过期时间)
redis.setex(key, 300, "NULL");
} else {
redis.set(key, product);
}
} else if ("NULL".equals(product)) {
return null; // 识别空值标记
}
return product;
}
坑2:缓存雪崩的分布式锁
// 使用 Redisson 防止缓存重建风暴
public Product getProductWithLock(long id) {
String key = "product:" + id;
Product product = redis.get(key, Product.class);
if (product == null) {
RLock lock = redisson.getLock("lock:" + key);
try {
lock.lock(); // 获取分布式锁
// 双重检查(防止重复查询DB)
product = redis.get(key, Product.class);
if (product == null) {
product = mysql.query(id);
redis.set(key, product, 60 + random(30)); // 随机过期时间
}
} finally {
lock.unlock();
}
}
return product;
}
四、性能优化关键参数
| 组件 | 参数 | 推荐值 | 作用 |
|---|---|---|---|
| Redis | maxTotal | 业务连接数 × 2 | 控制连接池大小 |
| timeout | 3000ms | 防止线程阻塞 | |
| MySQL | innodb_flush_log_at_trx_commit | 2 | 平衡安全与性能 |
| Canal | parallelThreads | CPU核数 × 2 | 提升Binlog解析速度 |
五、总结:双写一致性选择指南
- 强一致性:延迟双删 + 事务消息补偿
- 最终一致性:Binlog监听 + 异步更新
- 特殊场景:
- 读多写少 → 优先更新DB再删缓存
- 写多读少 → 直接操作DB,缓存设短TTL
关键认知:没有银弹方案! 需根据业务容忍度选择策略。
下期预告:《分布式锁的七种武器:从Redis到ZooKeeper的深度对比》
(本文代码实测于JDK11+SpringBoot3.0环境,关注公众号获取完整Demo)
更多推荐
所有评论(0)