当你的缓存不一致该如何解决?

592 阅读4分钟

引言

首先我们在系统中为了应对大量的请求,我们采用了缓存来解决系统的响应,但是一项新的技术往往伴有新的问题,比如我们引入缓存如何保证系统的一致性,如果没有保证一致性往往会导致用户看到错误的信息,影响用户体验,这种情况尤其是在分布式场景下更加常见。

所以本篇着重讨论如何更加高效的设计缓存,从而保证缓存与数据源的一致性。

问题分析

  正常情况下当我们需要去同步缓存的时候,一般情况下只需要去更新缓存再更新数据库或者先更新数据库数据库并且删除缓存,等待数据下一次读取的时候自动更新数据。


那么在上述的操作中因为操作不是原子性的,不论是先更新缓存还是更新数据库都会出现脏数据。

如果你追求强一致性,那么就需要在变动数据的时候上锁,但是这样又有违当初使用缓存去提升系统效率的初衷。

解决方案

所以一个好的设计方案可以让你的系统设计事半功倍,下面就引出一些我们常见的设计方案

1. 延迟双删

其实是在先删缓存之后,在多一个步骤。延迟双删顾名思义就是两次删除,第二次删除的时候设置一个延时时间,下面我举一个栗子:

场景描述

假设我们有一个电商系统,其中包含用户订单信息。当用户修改订单时,系统需要更新数据库中的订单信息。为了确保缓存中的订单信息与数据库保持一致,我们采用延时双删策略。

实现步骤

  1. 删除订单缓存数据
  2. 更新数据库
  3. 设置延时时间进行第二次删除(这里设置的延时时间需要等待全部操作完成之后在进行)
public class OrderService {
    private Cache cache; // 缓存系统
    private Database database; // 数据库访问对象

    /**
     * 更新订单信息
     * @param order 订单对象
     */
    public void updateOrder(Order order) {
        String cacheKey = "order:" + order.getId();

        // 第一次删除缓存
        cache.delete(cacheKey);

        // 更新数据库中的订单信息
        database.updateOrder(order);

        // 延时一段时间,例如100毫秒,等待所有读请求完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 第二次删除缓存
        cache.delete(cacheKey);
    }
}

这种方案是解决数据库一致性一种比较实用的技巧,可以保证数据的最终一致性。但是需要注意以下几个问题

  • 延时的时间需要正确的把握,延时太短无法覆盖所有的读请求,太长则有可能导致占用系统资源

一般我们设置的延迟时间应该:稍大于请求查询数据与会写缓存的时间

  • 其次就是在高并发的场景下还是有可能出现因为请求发送顺序不同的问题导致的数据不一致。

2. 消息队列结合数据库binlog

这种方式就类似于数据库的主从操作,监听日志的变化,对缓存进行操作,便于对代码的解耦。

场景描述

假设我们有一个在线教育平台,该平台提供各种在线课程。平台上的课程信息(如课程描述、讲师信息、价格等)经常需要更新,以反映最新的课程安排和促销活动。

实现步骤

  1. 当操作数据库的时候业务代码只负责去操作数据库即可
    public class CourseService {
    public void updateCourse(Course course) {
        // 更新数据库中的课程信息
        database.update(course);
    }

    public void deleteCourseById(String courseId) {
        // 从数据库中删除课程
        database.delete(courseId);
    }
}
  1. 平台需要专门开启一个服务去订阅数据库的binlog,然后针对这个服务区监听数据库的变更事件,并且发送到消息队列
    public class BinlogSubscriber {
    public void start() {
        BinlogClient client = new BinlogClient(databaseConfig);
        client.subscribe("course_table", (event) -> {
            if (event.getType() == EventType.DELETE || event.getType() == EventType.UPDATE) {
                // 将事件信息发送到消息队列
                MessageQueue.send("course-cache-invalidation-queue", event.getData());
            }
        });
    }
}
  1. 开启一个消息变更服务,对收到的消息做相应的处理
   public class CacheInvalidationConsumer {
    public void start() {
        MessageQueue.subscribe("course-cache-invalidation-queue", (message) -> {
            // 根据消息内容生成缓存的key,并删除缓存
            String cacheKey = generateCacheKeyFromMessage(message);
            cache.delete(cacheKey);
        });
    }

    private String generateCacheKeyFromMessage(String message) {
        // 实现省略,根据消息内容生成缓存的key
        return "course_info:" + message.courseId;
    }
} 

这样就保证了数据和缓存的一致性,但是这种方式系统复杂性比较高