redis什么是缓存与数据库双写不一致?怎么解决?(代码片段)

没对象的指针 没对象的指针     2023-03-29     611

关键词:

什么是缓存与数据库双写不一致?怎么解决?

1. 热点缓存重建

我们以热点缓存 key 重建来一步步引出什么是缓存与数据库双写不一致,及其解决办法。

1.1 什么是热点缓存重建

在实际开发中,开发人员使用 “缓存 + 过期时间” 的策略来实现加速数据读写和内存使用率,这种策略能满足大多数业务场景。但还是会有一些问题:

  1. 当前 key 是一个热点 key(某时间管理大师登顶微博热搜第一),并发量非常大;

  2. 在缓存失效瞬间,重建缓存不能在短时间完成(可能是一个负责业务场景,需要经过复杂的计算、多次IO、多次服务之间调用等等),有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

下面一起看一下热点缓存重建场景下的解决方案

1.2 基于 DCL(double check lock) 双重检测锁解决热点缓存并发重建问题

synchronized(this) 
    productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) 
        if (EMPTY_CACHE.equals(productStr)) 
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            return new Product();
        
        product = JSON.parseObject(productStr, Product.class);
        // 读延期
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
        return product;
    

    product = productDao.get(productId);
    if (product != null) 
        redisUtil.set(productCacheKey, JSON.toJSONString(product),
                genProductCacheTimeout(), TimeUnit.SECONDS);
        productMap.put(productCacheKey, product);
     else 
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    

DCL 存在的问题:

  • synchronized 只在单节点内部有效,多节点会在每个web服务上缓存重建一次
  • this 是单例的,比如同时有 101、102 两个商品需要热点重建,101 先请求,synchronized(this) 会把 102 阻塞,可以 synchronized(每个商品)

解决办法:分布式锁解决热点缓存并发重建问题

1.3 分布式锁解决热点缓存并发重建问题

RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
try 
    productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) 
        if (EMPTY_CACHE.equals(productStr)) 
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            return new Product();
        
        product = JSON.parseObject(productStr, Product.class);
        // 读延期
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
        return product;
    

    product = productDao.get(productId);
    if (product != null) 
        redisUtil.set(productCacheKey, JSON.toJSONString(product),
                genProductCacheTimeout(), TimeUnit.SECONDS);
        productMap.put(productCacheKey, product);
     else 
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    
 finally 
    hotCacheLock.unlock();

问题:缓存与数据库双写不一致

2. 缓存与数据库双写不一致

2.1 Cache Aside Pattern

Cache Aside Pattern 是最经典的 “缓存 + 数据库” 读写的模式。包括 Facebook 的论文《Scaling Memcache at Facebook》也使用了这个策略。

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

  • 命中:应用程序从cache中取数据,取到后返回。

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。


为什么不是写完数据库后更新缓存?而是删除缓存?

可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据

是不是 Cache Aside 这个就不会有并发问题了?

不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

这个问题理论上会出现,不过,实际上出现的概率可能非常低。

因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

所以,这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。

2.2 缓存与数据库双写不一致

2.2.1 数据不一样场景

(1)双写不一致情况

线程1 在写数据库与更新缓存之间卡顿了一下,然后 线程2线程1 卡顿的这个空隙去写了数据库并刷新了缓存,然后 线程2 都已经执行完了,线程1 又把脏数据更新到了缓存,造成了数据库与缓存不一致。

(2)读写并发不一致

线程1 执行读操作,且没有命中缓存,然后就到数据库中取数据;此时来了一个 线程2 执行写操作,写完数据库后,让缓存失效,然后,之前的 线程1 再把老的数据放进去,会造成脏数据。

2.2.2 解决方案

(1)缓存数据加上过期时间

  1. 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

  2. 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

(2)消息队列串行化

(1)主要思路:在后台进程中我们可以创建多个队列,然后根据hash算法将写请求路由到不同的队列中,当来读请求的时候,就加入队列中,当写请求处理完毕后,再去处理读请求。
(2)分析:如果对于同一份数据有多个写请求同时在队列中,那么来一个读请求中加入队列中之后,一般写请求耗时比较久,那么读请求会需要很久才能返回,这样会特别影响性能,但能保证一致性(一般情况下建议不要用)。

(3)加分布式锁

通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。

    RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
    hotCacheLock.lock();
    try 
        productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) 
            if (EMPTY_CACHE.equals(productStr)) 
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product();
            
            product = JSON.parseObject(productStr, Product.class);
            // 读延期
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
            return product;
        

        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        RLock readLock = readWriteLock.readLock();
        readLock.lock();
        try 
            product = productDao.get(productId);
            if (product != null) 
                redisUtil.set(productCacheKey, JSON.toJSONString(product),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
                productMap.put(productCacheKey, product);
             else 
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            
         finally 
            readLock.unlock();
        
     finally 
        hotCacheLock.unlock();
    


    @Transactional
    public Product update(Product product) 
        Product productResult = null;
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock writeLock = readWriteLock.writeLock();
        writeLock.lock();
        try 
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), product);
         finally 
            writeLock.unlock();
        
        return productResult;
    

(4)canal 监听 binlog日志

可以用阿里开源的 canal 通过监听数据库的 binlog日志 及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

https://coolshell.cn/articles/17416.html

高并发场景下缓存+数据库双写不一致问题分析与解决方案设计

...可用架构中非常重要的一个环节。Redis主要解决了关系型数据库并发量低的问题,有助于缓解关系型数据库在高并发场景下的压力,提高系统的吞吐量(具体Redis是如何提高系统的性能、吞吐量,后面会专门讲)。而我们在Redis的... 查看详情

redis缓存+数据库双写不一致问题分析与解决方案

...解决思路1.常规简单的解决方案 先删除缓存,在更新数据库,如果删除缓存成功,修改数据库失败了,那么数据库中依然是旧数据,如果去读取数据的时候,发现缓存没有,则去读数据库,数据库会把... 查看详情

高并发场景下的缓存+数据库双写不一致问题分析与解决方案设计

...前端的nginx服务都会发送请求给库存服务,去获取相应的数据库存这一块,写数据库的时候,直接更新redis缓存实际上没有这么的简单,这里,其实就涉及到了一个 查看详情

redis缓存+数据库双写不一致问题分析与解决方案

...解决思路1.常规简单的解决方案 先删除缓存,在更新数据库,如果删除缓存成功,修改数据库失败了,那么数据库中依然是旧数据,如果去读取数据的时候,发现缓存没有,则去读数据库,数据库会把... 查看详情

redission读写锁解决db和缓存双写不一致(代码片段)

...然后去更新了缓存。那么此时的缓存其实是个脏数据。有什么办法呢?方案一直接加redission的普通Rlock分布式 查看详情

缓存数据库双写不一致问题处理(代码片段)

我们的数据库操作中,一般会封装同步修改缓存的写法,但是这是一个两步操作,有可能带来缓存数据库数据不一致的问题。使用redisson提供的分布式锁解决参考:基于redis的分布式锁在我们之前加锁的逻辑中࿰... 查看详情

什么是/使用缓存(cache),缓存更新策略数据库缓存不一致解决方案及实现缓存与数据库双写一致(代码片段)

(目录)实现这个方案:商户查询缓存商户查询缓存1.什么是缓存(Cache)?前言:什么是缓存?举个例子:例如:例1:StaticfinalConcurrentHashMap<K,V>map=newConcurrentHashMap<>();例2:staticfinalCache<K,V>USER_CACHE=CacheBuilder.newBuilder().b 查看详情

redis11_缓存和数据库一致性如何保证解决方案提供canel解决数据一致性问题

文章目录①.缓存和数据库双写一致保证②.缓存数据一致性-解决方案③.缓存数据一致性-解决-Canal①.缓存和数据库双写一致保证①.只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问... 查看详情

redis与mysql双写一致性(代码片段)

双写一致性时为了保证Redis缓存与MySQL数据库中的数据一样我们对Redis中没有的数据,MySQL怎么回写呢?我们用双检加锁策略这样只要第一个请求发过来,后面的请求就不会发送到MySQL,直接从Redis中获取缓存数据就可以了。 为... 查看详情

美团二面:redis与mysql双写一致性如何保证?

...s与MySQL双写一致性如何保证?这道题其实就是在问缓存和数据库在双写场景下,一致性是如何保证的?本文将跟大家一起来探讨如何回答这个问题。公众号:捡田螺的小男孩谈谈一致性一致性就是数据保持一致,在分布式系统中... 查看详情

redis缓存双写一致性(代码片段)

...细分缓存一致性多种更新策略挂牌报错,凌晨升级先更新数据库,在更新缓存先删除缓存,在更新数据库先更新数据库,在删除缓存延迟双删策略总结双写一致性Redis与Mysql双写一致性canal主要是用于MySQL数据库增量日志数据的订阅,消... 查看详情

缓存数据库双写不一致问题处理(代码片段)

我们的数据库操作中,一般会封装同步修改缓存的写法,但是这是一个两步操作,有可能带来缓存数据库数据不一致的问题。使用redisson提供的分布式锁解决参考:基于redis的分布式锁在我们之前加锁的逻辑中࿰... 查看详情

缓存数据库双写不一致问题处理(代码片段)

我们的数据库操作中,一般会封装同步修改缓存的写法,但是这是一个两步操作,有可能带来缓存数据库数据不一致的问题。使用redisson提供的分布式锁解决参考:基于redis的分布式锁在我们之前加锁的逻辑中࿰... 查看详情

一个高频面试题:怎么保证缓存与数据库的双写一致性?

...的组件,但是用到了分布式缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?CacheAsidePattern最经典的缓存+数据库读写的模式,就是CacheAsidePattern。读的... 查看详情

redis缓存简介以及缓存的更新策略(代码片段)

...1、缓存模型和思路 2、代码如下五、缓存更新策略 1、数据库缓存不一致解决方案: 2、数据库和缓存不一致采用什么方案3、CacheAsidePattern实现4、先操作数据库还是先操作缓存?六、实现商铺和缓存与数据库双写一致 1... 查看详情

保证缓存与数据库双写时的数据一致性

缓存与数据库双写时的数据一致性问题:只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?  一般来说,就是如果你的系统不是严格要求缓存... 查看详情

如何保证数据库和缓存双写一致性?(代码片段)

大家好,我是苏三,又跟大家见面了。前言数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。我很负责的告诉大家,该问题无论在面试,还... 查看详情

数据库与缓存一致性问题解决方案

...广泛用在缓存场景,一是能提高业务系统的性能,二是为数据库抵挡了高并发的流量请求。把Redis作为缓存组件,需要防止出现以下的一些问题,否则可能会造成生产事故。Redis缓存满了怎么办?缓存穿透、缓存击穿、缓存雪崩... 查看详情