MST

星途 面试题库

面试题:Redis缓存与数据库事务协同处理中数据一致性问题

在使用Redis缓存和数据库事务协同处理业务时,如何保证数据的一致性?请举例说明可能出现数据不一致的场景,并阐述相应的解决方案。
36.2万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

可能出现数据不一致的场景

  1. 先更新数据库,后更新缓存失败
    • 场景:应用程序先成功更新数据库中的数据,但在更新Redis缓存时,由于网络故障、缓存服务异常等原因导致更新失败。此时,数据库中的数据是新的,而缓存中的数据还是旧的,后续从缓存读取数据就会得到旧数据,造成数据不一致。
    • 示例:以电商商品库存为例,用户下单后,订单服务先更新数据库中商品的库存数量,但在更新Redis缓存中商品库存时失败。其他用户查询商品库存时,从缓存中获取到的是旧的库存数量。
  2. 先删除缓存,后更新数据库失败
    • 场景:应用程序先删除Redis缓存中的数据,准备更新数据库。然而,在更新数据库时,由于数据库故障、事务回滚等原因导致更新失败。此时,缓存中数据已被删除,后续请求会重新从数据库加载数据并写入缓存,但由于数据库更新未成功,加载到缓存中的还是旧数据,导致数据不一致。
    • 示例:在博客系统中,博主修改文章内容,系统先删除Redis缓存中文章的缓存数据,在更新数据库文章内容时,数据库出现磁盘空间不足等故障,更新操作失败。其他用户访问文章时,从数据库加载并重新写入缓存的是旧的文章内容。
  3. 并发场景下,缓存更新不及时
    • 场景:多个请求同时对同一数据进行操作,假设请求A先读取缓存数据,此时请求B删除了缓存并开始更新数据库。请求B更新数据库完成后,请求A基于旧的缓存数据进行业务处理并更新数据库。这样请求A更新的数据库数据可能覆盖了请求B更新的结果,导致数据库和缓存数据不一致。
    • 示例:在社交平台的点赞功能中,用户A和用户B几乎同时点赞一篇帖子。用户A先读取缓存中帖子的点赞数,此时用户B删除缓存并更新数据库点赞数。用户B更新完成后,用户A基于旧的点赞数进行计算并更新数据库,导致最终数据库和缓存中的点赞数可能出现不一致。

解决方案

  1. 更新数据库和更新缓存的重试机制
    • 方案:当更新缓存失败时,应用程序可以设置重试策略。例如,使用定时重试,在一定时间间隔(如1秒、2秒、4秒等)后重试更新缓存操作,最多重试N次。如果重试多次后仍失败,可以记录日志并通知运维人员处理。
    • 示例代码(以Java和Jedis为例)
import redis.clients.jedis.Jedis;

public class CacheUpdater {
    private static final int MAX_RETRIES = 3;
    private static final int RETRY_INTERVAL = 1000; // 1秒

    public static void updateCache(Jedis jedis, String key, String value) {
        int retryCount = 0;
        while (retryCount < MAX_RETRIES) {
            try {
                jedis.set(key, value);
                return;
            } catch (Exception e) {
                retryCount++;
                try {
                    Thread.sleep(RETRY_INTERVAL);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        // 记录日志
        System.err.println("Failed to update cache after " + MAX_RETRIES + " retries.");
    }
}
  1. 先更新数据库,再异步更新缓存
    • 方案:应用程序先更新数据库,更新成功后,将更新缓存的操作放入消息队列(如Kafka、RabbitMQ等)。由专门的消费者从消息队列中获取更新缓存的任务并执行。这样即使更新缓存失败,也不会影响数据库的操作,并且可以通过消息队列的重试机制保证缓存最终被更新。
    • 示例:在订单系统中,订单创建成功后,将更新商品库存缓存的消息发送到Kafka队列。库存服务作为消费者,从Kafka队列中获取消息并更新Redis缓存中的商品库存。
  2. 使用分布式锁
    • 方案:在对数据进行读写操作前,先获取分布式锁(如使用Redis的SETNX命令实现分布式锁)。只有获取到锁的请求才能进行数据的读取、更新数据库和更新缓存等操作,其他请求等待。这样可以避免并发场景下数据不一致的问题。
    • 示例代码(以Java和Jedis为例)
import redis.clients.jedis.Jedis;

public class DistributedLock {
    private Jedis jedis;
    private String lockKey;
    private String lockValue;
    private static final int EXPIRE_TIME = 10000; // 10秒

    public DistributedLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = System.currentTimeMillis() + EXPIRE_TIME + "";
    }

    public boolean acquireLock() {
        String result = jedis.set(lockKey, lockValue, "NX", "EX", EXPIRE_TIME / 1000);
        return "OK".equals(result);
    }

    public void releaseLock() {
        jedis.del(lockKey);
    }
}

在业务代码中使用:

public void processData() {
    Jedis jedis = new Jedis("localhost", 6379);
    DistributedLock lock = new DistributedLock(jedis, "data_lock");
    if (lock.acquireLock()) {
        try {
            // 读取数据、更新数据库、更新缓存等操作
        } finally {
            lock.releaseLock();
        }
    } else {
        // 等待或重试获取锁
    }
    jedis.close();
}
  1. 使用缓存双写一致性方案
    • 方案:在更新数据库成功后,先删除缓存,然后设置一个短时间的过期时间(如1 - 5秒)。这样即使后续读取缓存时发现缓存已过期,会重新从数据库加载数据并写入缓存,保证数据的最终一致性。同时,在更新数据库后,异步发起对缓存的更新操作,尽量减少缓存过期期间数据不一致的时间窗口。
    • 示例:在用户信息修改场景中,用户修改个人资料后,系统先更新数据库,接着删除Redis缓存中用户信息的缓存数据,并设置该缓存键的过期时间为3秒。同时,通过异步任务(如使用Spring的@Async注解)在后台重新查询数据库并更新缓存。