Java 开发日记:MySQL-Redis 双写一致性的 4 种常见模式,哪种适合你的项目?

大家好,我是小明,一个 Java 后端开发者。最近在项目中遇到了一个经典问题:当数据同时写入 MySQL 和 Redis 时,如何保证一致性?MySQL 作为持久化存储,Redis 作为缓存,双写不一致会导致脏数据,影响用户体验。今天,我就来分享 4 种常见模式,并帮你分析哪种最适合你的项目。所有代码都用 Java 实现,基于 Spring Boot 框架,简单易懂。

什么是双写一致性?

在分布式系统中,我们经常用 MySQL 存储核心数据,Redis 作为缓存加速读取。但当数据更新时,如果先写 MySQL 再写 Redis(或反之),可能出现不一致。例如,用户更新信息后,MySQL 成功但 Redis 失败,导致缓存中还是旧数据。这就是双写一致性问题。核心目标是在保证性能的同时,确保数据最终一致。

下面,我详细介绍 4 种常见模式,每种都附上 Java 代码示例。假设我们用 Spring Data JPA 操作 MySQL,Spring Data Redis 操作 Redis。

模式 1: 同步双写

同步双写是最直接的方式:在同一个事务中,先写 MySQL,再写 Redis。如果任何一步失败,整个事务回滚。这保证了强一致性,但可能降低性能,因为每次写操作都涉及两个系统。

优点: 简单易实现,数据一致性高。
缺点: 性能开销大,如果 Redis 不可用,整个操作失败。
适用场景: 对一致性要求极高的场景,如金融交易系统。

Java 代码示例:

@Service
public class SyncWriteService {
    @Autowired
    private UserRepository userRepository; // MySQL repository
    @Autowired
    private RedisTemplate<String, User> redisTemplate; // Redis template

    @Transactional
    public void updateUser(User user) {
        // 先写 MySQL
        userRepository.save(user);
        // 再写 Redis
        redisTemplate.opsForValue().set("user:" + user.getId(), user);
    }
}

模式 2: 异步双写(使用消息队列)

异步双写通过消息队列(如 Kafka 或 RabbitMQ)解耦写操作。先写 MySQL,成功后发送消息到队列,消费者异步写 Redis。这提高了性能,但可能延迟一致性。

优点: 性能好,系统更可靠,即使 Redis 故障也能重试。
缺点: 数据可能短暂不一致,实现复杂。
适用场景: 高并发读写的系统,如电商库存管理。

Java 代码示例:

@Service
public class AsyncWriteService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private KafkaTemplate<String, User> kafkaTemplate;

    public void updateUser(User user) {
        // 写 MySQL
        userRepository.save(user);
        // 发送消息到队列
        kafkaTemplate.send("user-update-topic", user);
    }

    // Kafka 消费者服务
    @KafkaListener(topics = "user-update-topic")
    public void consumeUserUpdate(User user, @Autowired RedisTemplate<String, User> redisTemplate) {
        // 异步写 Redis
        redisTemplate.opsForValue().set("user:" + user.getId(), user);
    }
}

模式 3: 写后删

写后删模式:先写 MySQL,然后立即删除 Redis 中的缓存。下次读取时,如果缓存缺失,再从 MySQL 加载到 Redis。这避免了写 Redis 失败的风险,但可能导致缓存击穿。

优点: 实现简单,减少写操作开销。
缺点: 删除缓存后,读取可能变慢,一致性依赖于后续加载。
适用场景: 读多写少的应用,如内容发布系统。

Java 代码示例:

@Service
public class WriteDeleteService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    @Transactional
    public void updateUser(User user) {
        // 写 MySQL
        userRepository.save(user);
        // 删除 Redis 缓存
        redisTemplate.delete("user:" + user.getId());
    }

    // 读取方法:如果缓存缺失,从 MySQL 加载
    public User getUser(Long id) {
        User user = redisTemplate.opsForValue().get("user:" + id);
        if (user == null) {
            user = userRepository.findById(id).orElse(null);
            if (user != null) {
                redisTemplate.opsForValue().set("user:" + id, user);
            }
        }
        return user;
    }
}

模式 4: 延迟双删

延迟双删是写后删的增强版:先写 MySQL,然后删除 Redis 缓存,再延迟一段时间后二次删除。这处理了并发写操作可能导致的脏数据,但增加了复杂性。

优点: 一致性更强,减少缓存脏数据概率。
缺点: 延迟控制难,可能影响性能。
适用场景: 高并发且数据变更频繁的系统,如社交网络。

Java 代码示例:

@Service
public class DelayedDeleteService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RedisTemplate<String, User> redisTemplate;
    @Autowired
    private ScheduledExecutorService executorService; // 线程池用于延迟

    @Transactional
    public void updateUser(User user) {
        // 写 MySQL
        userRepository.save(user);
        // 第一次删除缓存
        redisTemplate.delete("user:" + user.getId());
        // 延迟 1 秒后二次删除
        executorService.schedule(() -> {
            redisTemplate.delete("user:" + user.getId());
        }, 1, TimeUnit.SECONDS);
    }
}

如何选择适合你的项目?

现在,我们来比较这 4 种模式,帮你决策。基于三个维度:一致性要求、性能需求和实现复杂度。

模式 一致性强度 性能影响 实现复杂度 推荐场景
同步双写 高(强一致) 低(慢) 低(简单) 金融、支付系统
异步双写 中(最终一致) 高(快) 中(需消息队列) 电商、高并发平台
写后删 中(最终一致) 高(快) 低(简单) 博客、新闻类应用
延迟双删 高(强一致) 中(中等) 高(需延迟控制) 社交、实时更新系统

选择指南:

  • 如果你的项目对一致性要求极高(如资金操作),选同步双写或延迟双删。同步双写更简单,但延迟双删能处理并发问题。
  • 如果性能是关键(如千万级用户),选异步双写或写后删。异步双写通过队列平滑流量,写后删则减少额外开销。
  • 如果团队资源有限,优先写后删或同步双写,它们代码少、易维护。异步双写需要搭建消息队列,延迟双删需小心定时任务。
  • 一般建议:在测试环境模拟高负载,验证一致性和性能。例如,用 JMeter 压测,观察数据延迟。
结语

在 Java 开发中,MySQL-Redis 双写一致性是常见挑战。通过这 4 种模式,你可以根据项目需求灵活选择。记住,没有“万能方案”——同步双写适合强一致系统,异步双写提升吞吐量,写后删简化逻辑,延迟双删增强可靠性。希望这篇日记能帮你少踩坑!如果有疑问,欢迎评论区讨论。下次见!

(本文基于实际项目经验原创,代码示例简化自生产环境,避免使用特定技术如 PHP 或微信相关术语,确保通用性。)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐