04day
1.项目-用户注册组件库解决缓存穿透详细说一下
缓存穿透查询一个缓存和数据库都不存在的数据,导致每次请求都到数据库,数据库压力增大
缓存空对象:先查缓存 缓存没有 锁 重建缓存 查询数据库 缓存null 设置过期5分钟
布隆过滤器:布隆过滤器先做个判断 可能存在 和 一定不存在 数据 多个hash函数运算 对应数组位1 取的时候如果都为1 则可能存在 如果都是0 一定不存在
2.redis和数据库的一致性
考点:Cache-Aside 异常。
标准答:
① 写:先删缓存 → 再写 DB → 延迟双删(异步 500 ms 再删一次);
② 读:缓存未命中查库并 SET 新值 + 随机 TTL 防并发重建;
③ 用 Canal 监听 binlog 做兜底补偿。
背诵:先删后写再延迟删,Canal 兜底
T1:A 写,删缓存
T2:B 读,缓存 miss,读旧 DB
T3:B 把旧值 SET 回缓存
T4:A 写 DB 成功
我们当前阶段只用延迟双删,代码 10 行搞定;
如果以后主从延迟或量级上去,随时加 Canal 做实时补偿,业务无侵入,复杂度可控
@Transactional
public void updateItem(Long id, String newVal) {
redis.del(key(id)); // ① 先删
itemMapper.update(id, newVal); // ② 写 DB
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
CompletableFuture.delayedExecutor(500, MILLISECONDS)
.execute(() -> redis.del(key(id))); // ③ 延迟删
}
});
}
第二次删失败?
→ 钥匙失效/Redis 抖动,由 Canal 兜底或 3 min 对账任务补偿
@Component
@Slf4j
public class CacheReconcileJob {
@Autowired
private StringRedisTemplate redis;
@Autowired
private ItemMapper itemMapper; // MyBatis 接口
private static final long SCAN_MINUTES = 3; // 最近 3 分钟
private static final Duration LOCK_TTL = Duration.ofMinutes(5);
private static final String LOCK_KEY = "job:cache:reconcile";
@Scheduled(fixedDelayString = "${job.reconcile:180000}") // 3 min 跑一次
public void run() {
// 分布式锁(可选):防止多实例重复扫
Boolean locked = redis.opsForValue()
.setIfAbsent(LOCK_KEY, "1", LOCK_TTL);
if (!Boolean.TRUE.equals(locked)) {
log.info("对账任务已存在,本次跳过");
return;
}
try {
// 1. 查最近更新过的记录
LocalDateTime start = LocalDateTime.now().minusMinutes(SCAN_MINUTES);
List<Item> list = itemMapper.listUpdatedAfter(start);
for (Item item : list) {
String cacheKey = "item:" + item.getId();
String cacheValue = redis.opsForValue().get(cacheKey);
// 2. 缓存缺失或值不一致 → 重新 SET
if (cacheValue == null || !cacheValue.equals(item.getValue())) {
long ttl = ThreadLocalRandom.current().nextInt(300, 601);
redis.opsForValue().set(cacheKey, item.getValue(), ttl, TimeUnit.SECONDS);
log.warn("对账修复 key={}, ttl={}s", cacheKey, ttl);
continue;
}
// 3. TTL 太短(<30s)也重新 SET,防止瞬间又 miss
Long ttl = redis.getExpire(cacheKey, TimeUnit.SECONDS);
if (ttl != null && ttl < 30) {
long newTtl = ThreadLocalRandom.current().nextInt(300, 601);
redis.opsForValue().set(cacheKey, item.getValue(), newTtl, TimeUnit.SECONDS);
log.warn("对账刷新 TTL key={}, newTtl={}s", cacheKey, newTtl);
}
}
} finally {
redis.delete(LOCK_KEY);
}
}
}
异常场景复盘
-
第二次删失败?
→ 钥匙失效/Redis 抖动,由 Canal 兜底或 3 min 对账任务补偿。 -
主从延迟 > 500 ms?
java
→ 把 sleep 改成 1 500 ms;或强制读主库:复制
@Select("SELECT * FROM item WHERE id = #{id} FOR SHARE") Item getFromMaster(@Param("id") Long id); -
窗内并发重建?
java
→ 加分布式锁(Redisson)只允许一个线程回源:复制
RLock lock = redisson.getLock("item:rebuild:" + id); if (lock.tryLock(0, TimeUnit.SECONDS)) { try { if (redis.get(key) == null) { // 双重判定 Item item = itemMapper.getFromMaster(id); redis.opsForValue().set(key, JSON.toJSONString(item), RandomUtil.nextInt(300, 600), TimeUnit.SECONDS); } } finally { lock.unlock(); } }
六、面试 30 秒口诀
“先删缓存再写库,事务提交后睡五百毫秒再删一次;窗口内允许脏,窗后一定新;延迟改主库或 Canal 兜底,最终一致。”
终极兜底:Canal 监听 binlog
架构:
MySQL 主库 → binlog → Canal Server → MQ → 缓存服务
流程:
-
Canal 伪装成从库,实时拉 binlog;
-
解析到
update items set val = ? where id = ?; -
把消息扔到 RocketMQ/Kafka;
-
缓存服务消费 → 直接
del cacheKey(id)
-
窗口不再靠“猜”,真正以主库变更为准;
-
延迟降到毫秒级;
-
即使延迟双删第二次失败,Canal 也会最终补偿
三、标准解法 1:延迟双删(De-Del)
思路:
给「读脏」留一个时间窗,窗口内允许脏存在,窗口结束再删一次,最终一致。
时间窗多长?
T = 主从延迟 + 业务处理耗时 + 网络抖动,一般500 ms 起步,可配置。
代码(Spring 事务模板):
java
复制
@Transactional
public void updateItem(Long id, String newVal) {
// ① 先删缓存
redis.del(cacheKey(id));
// ② 写 DB(主库)
itemMapper.update(id, newVal);
// ③ 提交事务后,异步延迟再删一次
transactionManager.commit(status);
CompletableFuture.runAsync(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) { /*ignore*/ }
redis.del(cacheKey(id)); // 兜底删
}, delayExecutor);
}
注意:
-
第二次删必须在事务提交之后,否则删完又立刻被旧值回填;
-
如果主从延迟>500 ms,就要调大窗口或直接用 Canal。
二、when:窗口到底多长?
窗口 T = 主从同步延迟 + 业务SQL耗时 + 网络抖动
线上经验值:
表格
复制
| 场景 | T |
|---|---|
| 单机/主库读写 | 300 ms |
| 一主一从,无大事务 | 500 ms |
| 一主多从+大事务 | 1 000 ms |
口诀:先测 Seconds_Behind_Master 峰值,再乘 2 即可
29万+

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



