Redis 和 MySQL 数据一致性是分布式系统中的一个常见挑战。保证数据一致性通常涉及几种策略,我会详细解释这些策略并提供相应的代码示例。
- 先更新数据库,再更新缓存
这种方法先更新 MySQL,然后更新或删除 Redis 缓存。
@Transactional
public void updateUser(User user) {
// 1. 更新MySQL
userMapper.updateUser(user);
// 2. 更新Redis缓存
// 方式1:更新缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user);
// 方式2:删除缓存(推荐)
redisTemplate.delete("user:" + user.getId());
}
优点:
- 简单直接
- 保证数据库有最新数据
缺点:
- 如果更新缓存失败,会导致数据不一致
- 先删除缓存,再更新数据库
这种方法先删除 Redis 缓存,然后更新 MySQL。
@Transactional
public void updateUser(User user) {
// 1. 删除Redis缓存
redisTemplate.delete("user:" + user.getId());
// 2. 更新MySQL
userMapper.updateUser(user);
}
优点:
- 避免缓存更新失败导致的不一致
缺点:
- 在高并发情况下可能出现数据不一致
- 延迟双删策略
这种方法在更新数据库前后都删除缓存,并在第二次删除时增加短暂延迟。
@Transactional
public void updateUser(User user) {
// 1. 删除Redis缓存
redisTemplate.delete("user:" + user.getId());
// 2. 更新MySQL
userMapper.updateUser(user);
// 3. 延迟一段时间后再次删除缓存
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延迟500毫秒
redisTemplate.delete("user:" + user.getId());
} catch (InterruptedException e) {
// 处理异常
}
});
}
优点:
- 能够处理高并发场景下的数据一致性问题
缺点:
- 实现较为复杂
- 增加了系统延迟
- 使用消息队列
使用消息队列来保证数据一致性,先更新数据库,然后发送消息到队列,由消费者来更新缓存。
@Transactional
public void updateUser(User user) {
// 1. 更新MySQL
userMapper.updateUser(user);
// 2. 发送消息到消息队列
kafkaTemplate.send("user-update-topic", JSON.toJSONString(user));
}
// 在消费者服务中
@KafkaListener(topics = "user-update-topic")
public void consumeUserUpdate(String message) {
User user = JSON.parseObject(message, User.class);
// 更新Redis缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
优点:
- 解耦了数据库操作和缓存操作
- 可以处理高并发场景
缺点:
- 增加了系统复杂度
- 可能引入短暂的数据不一致
- 使用 Canal 进行 MySQL binlog 同步
使用 Canal 监听 MySQL 的 binlog,然后更新 Redis 缓存。
@Component
public class CanalClient {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostConstruct
public void init() {
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example", "", "");
try {
connector.connect();
connector.subscribe(".*\\..*");
while (true) {
Message message = connector.getWithoutAck(100);
long batchId = message.getId();
List<CanalEntry.Entry> entries = message.getEntries();
if (batchId != -1 && entries.size() > 0) {
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 处理更新操作,更新Redis缓存
updateRedisCache(rowData);
}
}
}
}
}
connector.ack(batchId);
}
} finally {
connector.disconnect();
}
}
private void updateRedisCache(CanalEntry.RowData rowData) {
// 根据rowData更新Redis缓存
// 这里需要根据具体的数据结构来实现
}
}
优点:
- 实时性高
- 对应用层代码无侵入
缺点:
- 配置和维护相对复杂
- 依赖 MySQL binlog 配置
总结:
- 选择哪种方案取决于具体的业务需求、系统架构和性能要求。
- 对于读多写少的场景,可以考虑使用"先更新数据库,再删除缓存"的策略。
- 对于高并发场景,可以考虑使用延迟双删或消息队列的方案。
- 对于实时性要求高的场景,可以考虑使用 Canal 进行 binlog 同步。
- 无论选择哪种方案,都需要考虑异常处理和重试机制,以提高系统的可靠性。
在实际应用中,可