Redis和MySQL的实时数据同步方案
针对 Redis 和 MySQL 的实时数据同步,需根据业务场景选择不同的技术方案,核心目标是保障数据一致性、降低延迟、提升系统可靠性。以下是几种典型方案及其适用场景:
方案一:基于 MySQL Binlog 的异步同步
原理
- 监听 MySQL 的 Binlog 日志(记录所有数据变更事件)。
- 解析 Binlog(如通过 Canal、Debezium 等工具)。
- 将变更事件推送到 Redis(更新或删除对应缓存)。
架构流程
MySQL → Binlog → 中间件(Canal) → 消息队列(Kafka) → Redis消费者 → 更新Redis
优势
- 对应用透明:无需修改业务代码。
- 高可靠性:Binlog 是 MySQL 原生日志,确保数据变更不丢失。
- 支持复杂变更:可捕获 UPDATE、DELETE、INSERT 等操作。
缺点
- 延迟稍高:异步处理通常有毫秒到秒级延迟。
- 部署复杂度高:需维护中间件和消息队列。
适用场景
- 缓存一致性要求高:如电商商品详情、库存同步。
- 写操作频繁:需实时更新缓存的场景。
实现示例
// Canal客户端监听Binlog(伪代码)
CanalConnector connector = CanalConnectors.newClusterConnector("127.0.0.1:2181", "example", "", "");
connector.connect();
connector.subscribe(".*\\..*");
while (true) {
Message message = connector.getWithoutAck(100);
for (CanalEntry.Entry entry : message.getEntries()) {
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
String tableName = entry.getHeader().getTableName();
String key = "cache:" + tableName + ":" + rowData.getBeforeColumns(0).getValue();
if (rowChange.getEventType() == CanalEntry.EventType.DELETE) {
redis.del(key); // 删除缓存
} else {
redis.set(key, serialize(rowData.getAfterColumnsList())); // 更新缓存
}
}
}
}
}
方案二:应用层双写(强一致性)
原理
在业务代码中同步写入 MySQL 和 Redis,通过事务保证原子性。
架构流程
应用层 → 开启事务 → 写入MySQL → 写入Redis → 提交事务
优势
- 强一致性:Redis 与 MySQL 数据完全同步。
- 低延迟:同步写入,无异步链路。
缺点
- 性能损耗:双写增加请求耗时。
- 事务复杂性:需处理分布式事务(如 Redis 写入失败时的回滚)。
适用场景
- 写操作低频但强一致:如账户余额、支付状态。
- 简单系统:无复杂中间件维护能力的小型项目。
实现示例
// 伪代码:双写(需结合事务管理器)
@Transactional
public void updateUser(User user) {
// 先写MySQL
userMapper.update(user);
// 再写Redis
String key = "user:" + user.getId();
redisTemplate.opsForValue().set(key, user);
// 若Redis写入失败,MySQL事务会回滚
}
方案三:消息队列解耦(最终一致性)
原理
- 应用层写入 MySQL 后,发送消息到消息队列(如 Kafka、RocketMQ)。
- 消费者异步消费消息,更新 Redis。
架构流程
应用层 → 写MySQL → 发消息到MQ → 消费者 → 更新Redis
优势
- 解耦:业务层与缓存更新逻辑分离。
- 削峰填谷:MQ 缓冲流量,避免 Redis 压力过大。
缺点
- 最终一致性:存在秒级延迟。
- 消息堆积风险:需监控消费者处理速度。
适用场景
- 高并发写入:如社交平台动态发布后的缓存预热。
- 允许短暂不一致:如文章阅读量统计。
实现示例
// 伪代码:写入MySQL后发消息
public void createOrder(Order order) {
orderMapper.insert(order); // 写MySQL
kafkaTemplate.send("order-topic", order.getId()); // 发送消息
}
// 消费者更新Redis
@KafkaListener(topics = "order-topic")
public void listen(String orderId) {
Order order = orderMapper.selectById(orderId); // 查最新数据
redisTemplate.opsForValue().set("order:" + orderId, order);
}
方案四:延迟双删(应对缓存穿透)
原理
- 先删除 Redis 缓存。
- 更新 MySQL。
- 延迟一定时间后再次删除 Redis(处理并发脏读)。
架构流程
应用层 → 删除Redis → 写MySQL → 延迟 → 再删Redis
优势
- 简单有效:减少缓存与数据库不一致时间窗口。
缺点
- 无法完全避免不一致:极端并发下仍有脏数据可能。
- 依赖延迟时间设置:需根据业务调整。
适用场景
- 读多写少:如配置信息更新。
- 对一致性要求不苛刻:允许短暂旧数据存在。
实现示例
// 伪代码:延迟双删
public void updateConfig(Config config) {
String key = "config:" + config.getId();
redisTemplate.delete(key); // 第一次删除
configMapper.update(config); // 写MySQL
// 延迟1秒后再删一次(异步执行)
scheduledExecutor.schedule(() -> redisTemplate.delete(key), 1, TimeUnit.SECONDS);
}
方案五:结合数据库触发器(慎用)
原理
- 在 MySQL 中设置触发器(Trigger),监听数据变更。
- 触发器调用外部程序(如 UDF 或 HTTP API)更新 Redis。
缺点
- 性能影响大:触发器会增加数据库负载。
- 维护困难:需编写存储过程或外部接口。
适用场景
- 遗留系统改造:无法修改应用代码时的临时方案。
方案选型对比
方案 | 一致性 | 延迟 | 性能影响 | 复杂度 | 适用场景 |
---|---|---|---|---|---|
Binlog 异步同步 | 最终一致 | 毫秒~秒级 | 低 | 高 | 高频写、要求最终一致 |
应用层双写 | 强一致 | 毫秒级 | 高 | 中 | 低频写、强一致(如支付) |
消息队列解耦 | 最终一致 | 秒级 | 中 | 中 | 高并发写、允许延迟 |
延迟双删 | 最终一致 | 秒级 | 低 | 低 | 读多写少、简单系统 |
数据库触发器 | 强一致 | 毫秒级 | 高 | 高 | 遗留系统改造(不推荐) |
关键注意事项
- 缓存失效策略:
- 主动更新(推荐):通过同步机制更新。
- 被动淘汰:设置合理 TTL,防止长期脏数据。
- 数据回环问题:
- 避免同步操作再次触发写 MySQL(如 Binlog 同步到 Redis 后,不应反向写回 MySQL)。
- 监控与告警:
- 监控 Redis 与 MySQL 的数据差异(如定时对比关键表)。
- 监控同步延迟(如 Kafka 消费 Lag)。
- 降级方案:
- 同步失败时,可降级为直接读 MySQL,并记录日志告警。
总结
- 优先推荐 Binlog + 消息队列方案:适合多数高并发场景,平衡一致性与性能。
- 强一致性场景选择双写+事务:需结合分布式事务框架(如 Seata)。
- 简单场景用延迟双删:快速实现,但需容忍短暂不一致。
- 避免过度设计:根据实际业务需求选择最简单有效的方案。