根据上面总结一下 是不是可以互斥锁 逻辑锁 也可以是后台定时任务更新 也可以biglog日志 + 消息队列 详细讲解一下 并总结一下
1. 互斥锁方案(Mutex Lock)
核心原理
使用分布式锁保证同一时间只有一个线程可以查询数据库并更新缓存。
详细流程
java
复制
下载
public Object getData(String key) {
// 1. 查询缓存
Object data = redis.get(key);
if (data != null) {
return data;
}
// 2. 缓存未命中,尝试获取锁
String lockKey = "lock:" + key;
try {
if (tryLock(lockKey)) { // 使用SETNX实现
// 3. 获取锁成功,二次检查缓存(防止其他线程已经更新)
data = redis.get(key);
if (data != null) {
return data;
}
// 4. 查询数据库
data = db.query(key);
// 5. 写入缓存
redis.setex(key, 300, data); // 设置300秒过期
} else {
// 6. 获取锁失败,等待后重试
Thread.sleep(100);
return getData(key); // 递归重试
}
} finally {
unlock(lockKey);
}
return data;
}
适用场景
-
数据一致性要求高的场景
-
写操作不频繁的业务
-
防止缓存击穿
2. 逻辑过期方案(Logical Expiration)
核心原理
数据永不过期,通过内部字段判断逻辑过期,异步更新。
详细流程
java
复制
下载
public Object getDataWithLogicalExpire(String key) {
// 1. 查询缓存(永不过期的key)
String json = redis.get(key);
if (json == null) {
// 缓存不存在,走互斥锁流程
return getDataWithMutex(key);
}
// 2. 解析逻辑过期对象
RedisData redisData = parseJson(json);
Object data = redisData.getData();
long expireTime = redisData.getExpireTime();
// 3. 判断是否逻辑过期
if (System.currentTimeMillis() > expireTime) {
// 4. 已过期,尝试获取锁进行异步更新
String lockKey = "lock:refresh:" + key;
if (tryLock(lockKey)) {
// 异步更新,不阻塞当前请求
CompletableFuture.runAsync(() -> {
try {
Object newData = db.query(key);
RedisData newRedisData = new RedisData(newData,
System.currentTimeMillis() + 30 * 60 * 1000);
redis.set(key, toJson(newRedisData)); // 永不过期
} finally {
unlock(lockKey);
}
});
}
}
// 5. 返回数据(可能是旧数据)
return data;
}
适用场景
-
高并发读场景
-
对一致性要求不极致的业务
-
热点数据更新
3. 后台定时任务更新(Scheduled Refresh)
核心原理
通过定时任务主动更新缓存,保证缓存永不过期。
详细流程
java
复制
下载
@Component
public class CacheWarmTask {
@Scheduled(fixedRate = 5 * 60 * 1000) // 每5分钟执行
public void refreshHotData() {
// 1. 获取需要刷新的key列表
List<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
try {
// 2. 查询最新数据
Object newData = db.queryByKey(key);
// 3. 更新缓存(永不过期或长过期时间)
redis.set(key, newData);
// 或者设置较长TTL:redis.setex(key, 3600, newData);
} catch (Exception e) {
log.error("刷新缓存失败 key: {}", key, e);
}
}
}
// 另一种方案:在访问时延长过期时间
public Object getDataWithRefresh(String key) {
Object data = redis.get(key);
if (data != null) {
// 每次访问都重置过期时间,实现"滑动过期"
redis.expire(key, 300);
}
return data;
}
}
适用场景
-
热点数据
-
数据量不大的场景
-
可预测的访问模式
4. Binlog + 消息队列方案
核心原理
通过数据库的Binlog监听数据变化,通过消息队列异步更新缓存。
详细流程
图表
代码
下载
写操作
生成Binlog
解析数据变更
消费消息
删除/更新缓存
读操作
业务应用
MySQL数据库
Canal监听服务
消息队列 RocketMQ/Kafka
缓存更新服务
Redis缓存
业务应用
具体组件职责:
-
Canal服务:伪装成MySQL从库,读取Binlog日志
-
消息队列:解耦和削峰填谷
-
缓存服务:消费消息,更新Redis
java
复制
下载
// Canal解析Binlog示例
public class BinlogParser {
public void processChange(String binlogData) {
// 解析表名、操作类型、数据内容
TableChange change = parseBinlog(binlogData);
if ("user".equals(change.getTableName())) {
// 发送到消息队列
messageQueue.send("cache-refresh", change);
}
}
}
// 缓存更新消费者
@Component
public class CacheUpdateConsumer {
@MQListener(topic = "cache-refresh")
public void consume(Message message) {
TableChange change = parseMessage(message);
// 根据操作类型处理缓存
switch (change.getOperationType()) {
case INSERT:
case UPDATE:
// 延迟双删策略
redis.delete(change.getCacheKey());
// 可选:异步重建缓存
rebuildCacheAsync(change.getCacheKey());
break;
case DELETE:
redis.delete(change.getCacheKey());
break;
}
}
private void rebuildCacheAsync(String cacheKey) {
CompletableFuture.runAsync(() -> {
Object newData = db.query(cacheKey);
redis.setex(cacheKey, 300, newData);
});
}
}
适用场景
-
数据一致性要求极高的场景
-
写操作频繁的系统
-
复杂的缓存维护场景
四种方案对比总结
| 方案 | 优点 | 缺点 | 适用场景 | 数据一致性 |
|---|---|---|---|---|
| 互斥锁 | 实现简单,保证强一致性 | 性能瓶颈,可能死锁 | 写少读多,防缓存击穿 | 强一致性 |
| 逻辑过期 | 高性能,无阻塞读取 | 短暂数据不一致,实现复杂 | 高并发读,容忍延迟 | 最终一致性 |
| 定时任务 | 缓存命中率高,稳定 | 实时性差,资源浪费 | 热点数据,可预测访问 | 弱一致性 |
| Binlog+MQ | 完全解耦,实时性强 | 架构复杂,维护成本高 | 写操作频繁,高一致性要求 | 强一致性 |
综合选择建议
-
初创项目/简单业务:优先选择互斥锁方案,实现简单可靠
-
高并发读业务:选择逻辑过期方案,保证系统响应速度
-
热点数据场景:结合定时任务预热+逻辑过期更新
-
大型分布式系统:采用Binlog+消息队列,保证数据强一致性
-
混合策略:实际生产中通常多种方案结合使用,比如:
-
大部分数据使用互斥锁
-
热点数据使用逻辑过期
-
核心数据使用Binlog同步
-
最佳实践示例
java
复制
下载
public class CacheStrategyFactory {
public CacheService getStrategy(String bizType) {
switch (bizType) {
case "user_profile": // 用户资料,一致性要求高
return new MutexLockCacheService();
case "product_detail": // 商品详情,高并发读
return new LogicalExpireCacheService();
case "hot_ranking": // 热门榜单,定时更新
return new ScheduledCacheService();
case "inventory": // 库存信息,强一致性
return new BinlogCacheService();
default:
return new MutexLockCacheService();
}
}
}
这四种方案覆盖了从简单到复杂的各种缓存更新场景,在实际架构设计中可以根据业务特点灵活选择和组合使用。
上述这些 也是mysql和redis的一致性解决方案吧
您说得完全正确! 上述四种方案正是解决 MySQL与Redis数据一致性 的核心方案。我之前的讲解就是从"缓存更新"的角度切入,本质上解决的就是数据库与缓存之间的数据一致性问题。
重新梳理:作为MySQL-Redis一致性解决方案
问题本质
当MySQL数据发生变化时,如何保证Redis中的缓存数据与MySQL保持一致,这就是经典的缓存一致性问题。
四种一致性解决方案对比
| 方案 | 核心思想 | 一致性强度 | 延迟 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 互斥锁 | 通过锁保证串行操作 | 强一致性 | 高 | 低 | 对一致性要求高的核心业务 |
| 逻辑过期 | 异步更新,容忍旧数据 | 最终一致性 | 低 | 中 | 高并发读,容忍短暂不一致 |
| 定时任务 | 定期全量/增量同步 | 弱一致性 | 高 | 中 | 数据变化不频繁,可预测 |
| Binlog+MQ | 数据库变更驱动更新 | 准实时一致性 | 低 | 高 | 写频繁,高一致性要求 |
作为一致性方案的具体实现要点
1. 互斥锁方案 - 强一致性保障
java
复制
下载
/**
* 更新数据时的互斥锁方案
*/
public void updateUser(User user) {
String lockKey = "lock:user:" + user.getId();
try {
if (tryLock(lockKey)) {
// 1. 先更新数据库
userMapper.update(user);
// 2. 再删除缓存
redis.delete("user:" + user.getId());
// 或者:3. 更新缓存(在复杂对象时可能选择删除更安全)
}
} finally {
unlock(lockKey);
}
}
一致性分析:通过锁保证"更新DB → 删除缓存"的原子性,避免并发更新导致的数据错乱。
2. 逻辑过期方案 - 高性能最终一致性
java
复制
下载
/**
* 数据更新时,只更新数据库,缓存异步刷新
*/
public void updateProduct(Product product) {
// 1. 只更新数据库
productMapper.update(product);
// 2. 不直接操作缓存,依靠逻辑过期机制异步刷新
// 后续读取时会发现逻辑过期,触发异步更新
}
一致性分析:接受短暂的不一致,但保证最终会同步,适合对实时性要求不极致的场景。
3. 定时任务方案 - 批量同步
java
复制
下载
/**
* 定时扫描数据变更,批量更新缓存
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行
public void syncChangedData() {
// 1. 查询最近变更的数据
List<Product> changedProducts = productMapper.selectRecentlyChanged();
// 2. 批量更新到Redis
for (Product product : changedProducts) {
redis.set("product:" + product.getId(), product);
}
}
一致性分析:有固定的同步延迟,但实现简单,适合数据变化不频繁的场景。
4. Binlog+MQ方案 - 准实时强一致性
java
复制
下载
/**
* 基于Binlog的缓存同步消费者
*/
@MQListener(topic = "db-change-product")
public void handleProductChange(ProductChangeEvent event) {
// 1. 解析变更事件
String operation = event.getOperation(); // INSERT/UPDATE/DELETE
Long productId = event.getProductId();
// 2. 根据操作类型处理缓存
switch (operation) {
case "UPDATE":
case "INSERT":
// 查询最新数据并更新缓存
Product product = productMapper.selectById(productId);
redis.set("product:" + productId, product);
break;
case "DELETE":
redis.delete("product:" + productId);
break;
}
}
一致性分析:毫秒级延迟,接近强一致性,架构复杂但效果最好。
一致性问题的核心挑战与解决方案
经典缓存一致性困境
java
复制
下载
// 问题代码:并发情况下可能出现数据不一致
public void updateUser(User user) {
// 1. 更新数据库
userMapper.update(user);
// 2. 删除缓存 ← 在这两个步骤之间,其他线程可能读取到旧数据并重建缓存
redis.delete("user:" + user.getId());
}
各方案的解决思路
| 方案 | 解决思路 | 优缺点 |
|---|---|---|
| 互斥锁 | 通过锁消除并发竞争 | 简单可靠,但影响性能 |
| 延迟双删 | 先删缓存→更新DB→延迟再删 | 减少不一致时间窗口 |
| 逻辑过期 | 接受不一致,异步更新 | 高性能,但数据不是最新 |
| Binlog | 从数据库层面保证顺序 | 强一致性,架构复杂 |
生产环境推荐策略
1. 读写分离策略
java
复制
下载
public class CacheConsistencyService {
// 读策略:缓存优先
public User getUser(Long id) {
// 先查缓存,缓存不存在查DB并回填
return cacheService.getWithMutex("user:" + id,
() -> userMapper.selectById(id));
}
// 写策略:先更新数据库,再操作缓存
public void updateUser(User user) {
// 方案1:直接删除缓存(简单有效)
userMapper.update(user);
redis.delete("user:" + user.getId());
// 方案2:延迟双删(更强的一致性)
redis.delete("user:" + user.getId());
userMapper.update(user);
ThreadUtil.sleep(100); // 延迟100ms
redis.delete("user:" + user.getId());
}
}
2. 分级策略
-
核心数据(账户余额、库存):Binlog+MQ 强一致性
-
重要数据(用户信息、订单):互斥锁 + 延迟双删
-
普通数据(商品信息、配置):逻辑过期 + 异步更新
-
统计类数据(排行榜、计数):定时任务批量更新
3. 降级策略
java
复制
下载
public Object getDataWithFallback(String key) {
try {
return cacheService.getWithLogicalExpire(key);
} catch (Exception e) {
// 缓存服务异常时,降级到直接查询数据库
log.warn("缓存服务异常,降级到DB查询");
return dbService.query(key);
}
}
总结
是的,这四种方案确实就是MySQL与Redis数据一致性的核心解决方案:
-
互斥锁 - 强一致性,适合核心业务
-
逻辑过期 - 高性能最终一致性,适合高并发读
-
定时任务 - 简单可靠,适合数据变化不频繁场景
-
Binlog+MQ - 准实时强一致性,适合大型复杂系统
选择建议:
-
80%的场景:互斥锁 + 延迟双删就能满足需求
-
高并发读场景:结合逻辑过期
-
大型系统:采用Binlog方案作为基础架构
在实际项目中,通常根据业务特点混合使用多种方案,在一致性、性能、复杂度之间找到最佳平衡点。

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



