MENU

Redis的双写一致性问题

Redis的双写一致性问题

image-20230527152245451

你只要用缓存,就可能会涉及到redis缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,

谈谈自己对双写一致性的理解!

双写一致性的了解

如果redis中有数据----需要和数据库中的值相同

如果redis中无数据----需要数据库中的值要是最新值,且准备回写redis

缓存按照操作分,细分2种----只读缓存||读写缓存

同步直写策略---写数据库后也同步写redis缓存,缓存和数据库中的数据一致;对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步致谢策略。

异步缓写策略---正常的业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库,物流系统;异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重视重写。

那么你如何解决一致性问题?

双检加锁策略

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

    public String get(String key){
        String value = redis.get(key);//查询缓存
        if (value != null){
            //缓存存在直接返回
            return value;
        }else {
            //缓存不存在则对方法加锁
            //假设请求量很大,缓存过期
            synchronized (TestFailure.class){
                value = redis.get(key);// 再查一边redis
                if (value != null){
                    //查到数据直接返回
                    return value;
                }else {
                    //二次查询缓存也不存在 直接查DB
                    value= dao.get(key);
                    //数据缓存
                    redis.setnx(key,value,time);
                    //返回
                    return value;
                }
            }
        }
    }
//实际操作----如下:
public User findUserById2(Integer id){
        User user = null;
        String key = CHACHE_USER_KEY + id;
        // 1 先从redis里面查询,如果有直接返回结果,如果没有再去查询Mysql
        user = (User) redisTemplate.opsForValue().get(key);
        if (user == null){
            // 2 大厂用,对应高QPS的优化,进来就加锁,保证一个请求操作,让外面的redis等待一下,避免击穿Mysql
            synchronized(UserService.class){
                // 3 二次查 redis还是null,可以去查mysql了(mysql默认有数据)
              user = (User) redisTemplate.opsForValue().get(key);
              if (user == null){
                  // 4 查询mysql拿数据
                  user = userMapper.selectByPrimaryKey(id); //mysql有数据默认
                  if (user == null){
                      return null;
                  }else {
                      // 5 mysql 里面有数据的,需要回写redis,完成数据一致性的同步工作。
                      redisTemplate.opsForValue().setIfAbsent(key,user,7L, TimeUnit.DAYS);
                  }
              }
            }
        }
        return user;
    }

双写一致性,先动缓存redis还是数据库mysql?

延时双删做过吗?会有哪些问题?

public void deleteOrderDate(Order order){
   try(Jedis jedis = RedisUtils.getJedis()){
       //1.线程A先成功删除redis的缓存
jedis.del(order.getId()+"");
//2.线程A再更新mysql
orderDao。update(order);
//暂停2秒钟,其它业务逻辑导致耗时延时,2是随便乱写的,只是为了讲解技术方便 A还没完成
       try { TimeUnit.sECoNDs.sleep( timeout: 2); 
 } catch (InterruptedException e) { e.printstackTrace(); 
}
jedis.del(order.getId()+"");
}catch (Exception e){ 
e.printstackTrace();
}
}
加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。
因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。

问题1:这个删除的休眠时间是多少?

这个时间怎么确定呢?

第一种方法:
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
第二种方法:
新启动一个后台监控程序,比如后面要讲解的WatchDog监控程序,会加时

问题2:这种同步淘汰策略,吞吐量降低怎么办?

public void deleteOrderDate(Order order){
   try(Jedis jedis = RedisUtils.getJedis()){
       //1.线程A先成功删除redis的缓存
  jedis.del(order.getId()+"");
  //2.线程A再更新mysql
  orderDao。update(order);
  //暂停2秒钟
       try { TimeUnit.sECoNDs.sleep( timeout: 2); 
      } catch (InterruptedException e) { e.printstackTrace(); 
    }
    completableFuture.suppLyAsync(() -> {
       //3将第二次删除作为异步的。自己起一个线程,异步删除。
       //这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
       return jedis.del( key: order.getId() + "");
       }).whencomplete((t,u) -> {
           system.out.println( "------t : "+t);
           system.out.println( "------t: "+u);
       }).exceptionally(e ->{
           system.out.println( "------e: "+e.getMessage());
           return 44L;
       }).get();
}catch (Exception e){ 
  e.printstackTrace();
}
}

有这么一种情况,微服务查询redis无mysql有,为保证数据双写一致性回写redis你需要注意什么?双检加锁策略你了解过吗?如何尽量避免缓存击穿?

redis和mysql双写100%会出纰漏,做不到强一致性,你如何保证最终一致性?

目的就是达到数据的一致性!

给缓存设置过期时间,定期清理缓存并回写,是保证最终的一致性的解决方案。 ---现在的主流方式!(不是100%正确)

​ 我们可以对存入的缓存的数据设置过期的时间,所有的写操作以数据库为准,对缓存操作只是尽最大的努力即可。也就是说如果数据写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据写入库为准。

解决的方法

数据库和缓存一致性的4种更新策略

先更新数据库,在更新缓存---X 停机的时候就只有一个线程就可以!!!!

会出现两个问题

问题1

1、先更新mysql的某个商品的库存,当前商品的库存为100,更新后为99个
2、先更新mysql修改为99,然后更新reids
3、此时如果出现异常,更新redis失败了,导致mysql里面的库存是99,而redis里面还是100。
4、综上所述,会让数据库里面的和缓存redis里面的数据不一致,读到redis的脏数据

问题2

【先更新数据库,在更新缓存】A、B两个线程发起调用。
正常理想逻辑----

A update mysql 100
A update redis 100
B update mysql 99
B update redis 99

========================================================
【异常逻辑】 多线程环境下,A、B两个线程有快有慢,有前有后有并行。

A update mysql 100
B update mysql 99
B update redis 99
A update redis 100

========================================================
最终导致,mysql和redis数据不一致

先更新缓存,在更新数据库---X

不太推荐!!! ------业务上一般把mysql作为底层数据库,保证最后的解释,作为兜底的。

问题2

【先更新数据库,在更新缓存】A、B两个线程发起调用。
正常理想逻辑----

A update redis 100
A update mysql 100
B update redis 99
B update mysql 99

========================================================
【异常逻辑】 多线程环境下,A、B两个线程有快有慢,有前有后有并行。

A update redis 100
B update redis 99
B update mysql 99
A update mysql 100

========================================================
最终导致,mysql和redis数据不一致

先删除缓存,再更新数据库---X

异常问题:

1、A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时),B突然出现要来读取缓存数据。

public void deleteOrderDate(Order order){
   try(Jedis jedis = RedisUtils.getJedis()){
       //1.线程A先成功删除redis的缓存
jedis.del(order.getId()+"");
//2.线程B再更新mysql
orderDao。update(order);
//暂停20秒钟,其它业务逻辑导致耗时延时,20是随便乱写的,只是为了讲解技术方便 A还没完成
       try { TimeUnit.sECoNDs.sleep( timeout: 20); 
} catch (InterruptedException e) { e.printstackTrace(); 
}
}catch (Exception e){ 
e.printstackTrace();
}
}

2、此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:

  1. B从mysql获得了旧值
    B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
  2. B会把获得的旧值写回redis
    获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。
public order selectorderData(order order){
try (jedis jedis = Redisutils.get3edis()){
   //1先去redis里面查找,找到返回数据找不到去mysqL查找
string result = jedis.get(order.geId() + "");
if (result != nul1) {
       return (order) SON . parse( result);
}else{
       order = orderDao.getorderById(order.getId());
  //2线程B会将从mysqL查到的旧数据写回到redis
       jedis.set( order.getId()+"",order.tostring());
  return order;
   }catch (Exception e){
           e.printstackTrace();
   }
   return nul1;
}

3、A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,
​ 两个并发操作,一个是更新操作,另一个是查询操作,
​ A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
​ 于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
4、总结
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql...还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入nysql数据库
image-20230610003033162

解决方法:

采用延时双删策略

先更新数据库,在删除缓存---!

image-20230610010019947

解决方案

image-20230610011238963
image-20230610011254347
image-20230610011900517
image-20230610012015379

Leave a Comment