2025-12-07 Reids如何保持跟MySQL的一致性

Redis 和 MySQL 保持数据一致性是一个常见的设计挑战,需要根据业务场景选择合适的方案。以下是几种主要策略:

一、常见方案对比

方案 原理 优点 缺点 适用场景
Cache Aside 应用层管理 简单可靠,减少缓存穿透 短暂不一致,逻辑复杂 读多写少
Write Through 写缓存时同步写DB 强一致性 性能较低,实现复杂 一致性要求高
延迟双删 先删缓存,更新DB,再删缓存 减少不一致时间 仍有不一致窗口 一般业务
基于Binlog 监听MySQL变更同步到Redis 解耦,实时性好 架构复杂,延迟 高一致性要求

二、详细实现方案

1. Cache Aside(旁路缓存)模式

最常用的模式,由应用层控制:

java
// 读流程
public Object getData(String key) {
    // 1. 先读缓存
    Object data = redis.get(key);
    if (data != null) {
        return data;
    }
    
    // 2. 缓存未命中,读数据库
    data = mysql.selectById(key);
    if (data != null) {
        // 3. 写入缓存,设置过期时间
        redis.setex(key, 300, data);
    }
    return data;
}

// 写流程
public void updateData(String key, Object newData) {
    // 1. 更新数据库
    mysql.update(newData);
    // 2. 删除缓存
    redis.delete(key);
}

2. 延迟双删策略

java
public void updateDataWithDoubleDelete(String key, Object newData) {
    // 1. 第一次删除缓存
    redis.delete(key);
    
    // 2. 更新数据库
    mysql.update(newData);
    
    // 3. 延迟再次删除(处理其他线程读旧数据写缓存的情况)
    executor.schedule(() -> {
        redis.delete(key);
    }, 500, TimeUnit.MILLISECONDS);
}

3. 基于MySQL Binlog的同步

使用Canal或Debezium监听binlog:

yaml

Canal配置示例

canal:
destination: example
mysql-addr: 127.0.0.1:3306
username: canal
password: canal
java
// 监听binlog变化
@EventListener
public void onDataChange(CanalEvent event) {
for (RowData rowData : event.getRowDataList()) {
if (rowData.getEventType() == EventType.UPDATE) {
// 更新Redis缓存
String key = buildCacheKey(rowData);
Object value = buildCacheValue(rowData);
redis.set(key, value);
} else if (rowData.getEventType() == EventType.DELETE) {
// 删除Redis缓存
String key = buildCacheKey(rowData);
redis.delete(key);
}
}
}

  1. 使用消息队列保证最终一致性
    java
    // 写服务
    @Transactional
    public void updateOrder(Order order) {
    // 1. 更新数据库
    orderDao.update(order);

    // 2. 发送缓存更新消息
    Message msg = new Message("cache-update",
    CacheEvent.build(order.getId(), "DELETE"));
    messageQueue.send(msg);
    }

// 消费者服务
@MessageListener(topic = "cache-update")
public void handleCacheUpdate(CacheEvent event) {
if (event.getType() == "DELETE") {
redis.delete(event.getKey());
} else if (event.getType() == "UPDATE") {
// 异步重新加载数据
reloadCache(event.getKey());
}
}

三、关键问题与解决方案

1. 缓存穿透

java
// 布隆过滤器或缓存空值
public Object getDataWithProtection(String key) {
    // 布隆过滤器判断是否存在
    if (!bloomFilter.mightContain(key)) {
        return null;
    }
    
    Object data = redis.get(key);
    if (data != null) {
        // 特殊标记表示空值
        if (data.equals("__NULL__")) {
            return null;
        }
        return data;
    }
    
    data = mysql.selectById(key);
    if (data == null) {
        // 缓存空值,短时间过期
        redis.setex(key, 60, "__NULL__");
    } else {
        redis.setex(key, 300, data);
    }
    return data;
}
  1. 缓存击穿(热点key失效)
java
// 使用互斥锁或永不过期策略
public Object getDataWithLock(String key) {
    Object data = redis.get(key);
    if (data == null) {
        String lockKey = "lock:" + key;
        // 获取分布式锁
        if (redis.setnx(lockKey, "1", 10)) {
            try {
                data = mysql.selectById(key);
                if (data != null) {
                    redis.setex(key, 300, data);
                }
            } finally {
                redis.delete(lockKey);
            }
        } else {
            // 等待其他线程加载
            Thread.sleep(50);
            return redis.get(key);
        }
    }
    return data;
}

四、最佳实践建议

  1. 分级策略
    强一致性要求:金融账户、库存扣减 → 直接读数据库

弱一致性要求:用户信息、商品信息 → 缓存+过期机制

最终一致性要求:点赞数、浏览量 → 异步更新

  1. 缓存设计
    java
    // 设置合理的过期时间
    redis.setex(key,
    // 基础时间 + 随机时间,避免同时失效
    baseTTL + ThreadLocalRandom.current().nextInt(0, 300),
    value);

// 使用版本号或时间戳
String cacheKey = String.format("data:%s:v%d", id, version);

  1. 监控与降级
    java
    // 监控缓存命中率
    @Slf4j
    @Component
    public class CacheMonitor {
    private AtomicLong hitCount = new AtomicLong();
    private AtomicLong missCount = new AtomicLong();

    public void recordHit() {
    hitCount.incrementAndGet();
    }

    public double getHitRate() {
    long total = hitCount.get() + missCount.get();
    return total > 0 ? (double) hitCount.get() / total : 0;
    }
    }
    五、推荐方案选择
    大多数场景:Cache Aside + 延迟双删

高一致性要求:Write Through 或 基于Binlog同步

读多写少:Cache Aside + 合理过期时间

写多读少:考虑不用缓存或很短过期时间

六、工具推荐
Canal:阿里开源的MySQL binlog增量订阅&消费组件

RedisTimeSeries:用于时序数据,确保一致性

Redisson:提供各种分布式锁和数据结构

根据你的具体业务场景(数据更新频率、一致性要求、读写比例),选择最合适的方案组合。建议先从简单的Cache Aside开始,随着业务复杂度增加再逐步引入更高级的方案。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容