文章目录
前言:面试官的灵魂拷问——缓存一致性这道送命题怎么破?
“说说缓存和数据库一致性怎么保证?”
当面试官抛出这道经典问题时,会议室空气突然凝固。你额头渗出细密的汗珠,脑海中闪过以下场景:
1. 初级程序员版本:
“更…更新数据库后删缓存?”
面试官冷笑:“并发请求下确定不会出问题?”
2. 中级程序员版本:
“可以用延迟双删!”
面试官挑眉:“延迟时间怎么定?网络抖动怎么办?”
3. 高级程序员版本:
“建议结合binlog+消息队列…”
面试官突然打断:“如果消息积压了怎么处理?”
本文将为你揭秘:数据库和缓存数据一致性方案
一、双写策略
1.1 标准模板
public void updateProduct(Product product) {
// 先写数据库
productDao.update(product);
// 再删缓存
cache.delete(product.getId());
}
1.2 面试官の死亡连问
- Q1:如果删缓存失败了怎么办?
解决方案:
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100))
public void safeDelete(String key) {
try {
cache.delete(key);
} catch (Exception e) {
// 记录到补偿队列
repairQueue.add(new RepairTask(key, OperationType.DELETE));
}
}
- Q2:两个线程并发更新怎么办?
解决方案:
public void updateWithVersion(Product product) {
long version = System.currentTimeMillis(); // 用数据库版本号更佳
productDao.update(product);
cache.update(product.getId(), product, version);
// 缓存端比较版本号更新
}
- Q3:这样性能不会炸吗?
太极推手:
“适合低频修改场景,比如用户基本信息维护”
二、延迟双删
2.1 标准姿势
public void updateProduct(Product product) {
// 第一次删除
cache.delete(product.getId());
// 更新数据库
productDao.update(product);
// 提交延迟任务
delayQueue.add(new DelayTask(() -> {
cache.delete(product.getId()); // 二次删除
}, 1000)); // 延迟1秒
}
2.2 面试官の灵魂暴击
- Q1:延迟时间怎么确定?
延时时间很难确定,其计算逻辑源于对数据库操作全链路的系统性观测和分析:
理论延迟时间 = 主从同步耗时 + SQL执行耗时 + 网络传输抖动缓冲
≈ 平均SQL执行时间 × 2 + 200ms
- Q2:消息丢失怎么办?
// 使用支持持久化的延迟队列
public class PersistentDelayQueue {
private final RocksDB rocksDB; // 本地持久化
private final DelayQueue<DelayTask> memoryQueue;
public void addTask(DelayTask task) {
rocksDB.put(task.getId(), serialize(task));
memoryQueue.add(task);
}
}
- Q3:极端情况下还是不一致怎么办?
“结合定时任务全量比对,我们系统设置凌晨3点做数据巡检”
三、订阅数据库变更
3.1 Binlog监听大法
// 使用Debezium监听MySQL
@Bean
public DebeziumEngine<ChangeEvent<String, String>> debeziumEngine() {
Configuration config = Configuration.create()
.with("connector.class", "io.debezium.connector.mysql.MySqlConnector")
.with("database.history", "io.debezium.relational.history.FileDatabaseHistory")
.build();
return DebeziumEngine.create(Connect.class)
.using(config)
.notifying(record -> {
// 解析变更事件
processChangeEvent(record);
}).build();
}
private void processChangeEvent(SourceRecord record) {
// 这里处理缓存更新逻辑
cache.update(record.key(), record.value());
}
3.2 面试官の必杀技
- Q1:消息顺序如何保证?
“采用分区有序消息队列,同一主键的变更路由到相同分区” - Q2:历史数据怎么处理?
public void handleSnapshot() {
// 全量数据快照处理
jdbcTemplate.query("SELECT * FROM products", rs -> {
cache.update(rs.getString("id"), rs.getString("data"));
});
}
- Q3:怎么防止雪崩?
防御性编程:
// 缓存更新限流
RateLimiter limiter = RateLimiter.create(1000); // 每秒1000次
public void safeCacheUpdate(String key, Object value) {
if (limiter.tryAcquire()) {
cache.update(key, value);
} else {
// 进入降级队列
downgradeQueue.add(new UpdateTask(key, value));
}
}
- Q4:如果消息积压了怎么处理?
Binlog消息积压本质是生产者-消费者速率失衡问题,应急处理的话首先会立即扩容消费者组,同时启用消息过滤降低处理负载。同时积压数量到阈值时,自动触发降级。
四、分布式锁
4.1 Redisson实现版
public void updateWithLock(Product product) {
RLock lock = redissonClient.getLock("product_lock:" + product.getId());
try {
lock.lock();
// 1. 查询最新数据
Product latest = productDao.get(product.getId());
// 2. 业务校验
if (latest.getStock() < 0) {
throw new BusinessException("库存不足");
}
// 3. 更新数据库
productDao.update(product);
// 4. 删除缓存
cache.delete(product.getId());
} finally {
lock.unlock();
}
}
4.2 面试官の终极大招
- Q1:锁失效了怎么办?
“采用看门狗机制自动续期,设置锁超时时间为业务耗时×3,避免锁长期无效占用” - Q2:集群环境下怎么保证?
“使用RedLock红锁算法,需要半数以上节点加锁成功” - Q3:性能瓶颈怎么办?
调优秘籍:大部分加锁场景我们都可以细化锁粒度
// 细粒度锁优化
public void updateStock(String productId, int delta) {
// 只锁具体商品ID
RLock lock = redissonClient.getLock("stock_lock:" + productId);
// ...
}
方案选型决策树

总结
本文系统剖析了保障数据库与缓存一致性的四大核心方案:
双写策略通过事务锁实现强一致性却面临并发覆盖风险,适合金融等高敏感场景;延迟双删以异步二次删除规避旧数据复活,需配合熔断机制应对缓存击穿,常用于电商读多写少业务;订阅变更(CDC) 依托数据库日志实现准实时同步,虽扩展性强但需处理消息积压,适合微服务架构;分布式锁确保原子操作却可能引发性能瓶颈,需结合细粒度锁优化,适用于秒杀等高并发场景。所有方案均需配套三位一体监控(命中率、延迟、不一致告警)、混沌工程验证及动态熔断策略,技术选型本质是在业务容忍度、实现成本与系统复杂度间的精准权衡,没有银弹,唯有对症下药。
170万+

被折叠的 条评论
为什么被折叠?



