redis:双写一致的原理、解决方案和面试回答(延时双删、分布式锁、异步通知MQ、canal)

目录

前言

1.场景导入

2.什么是双写一致

3.先删除缓存还是先操作数据库?

3.1 先删除缓存再删除数据库(不可行)

3.1.1 过程详解

正常情况

异常情况

3.2 先删除数据库再删除缓存(不可行)

3.2.1 过程详解

正常情况

异常情况

4. 解决方案

4.1  延时双删

4.1.1 为什么要删除两次缓存?

4.1.2 为什么要延迟双删呢?

4.2分布式锁(强一致性)

4.2.1 互斥锁

4.2.2 读写锁

共享锁/读锁

排他锁/独占锁

4.3 异步通知

5.面试回答

5.1 面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

5.2 面试官:那这个排他锁是如何保证读写、读读互斥的呢?

5.3 面试官:你听说过延时双删吗?为什么不用它呢?


前言

一定要结合面试题回答,分为两种情况:1.一致性要求高的 ,2.允许延迟一致的

1.场景导入

如果现在有个数据要更新,是先删除缓存,还是先操作数据库呢?当多个线程同时进行访问数据的操作,又是什么情况呢?

2.什么是双写一致

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

3.先删除缓存还是先操作数据库?

3.1 先删除缓存再删除数据库(不可行)

结论:仍然存在双写不一致

3.1.1 过程详解

正常情况

:假设起始数据   缓存:10   数据库:10

异常情况

3.2 先删除数据库再删除缓存(不可行)

结论:仍然存在双写不一致

3.2.1 过程详解

正常情况

起始数据    缓存:10  数据库 :10

异常情况

4. 解决方案

能保障强一致性:延时双删、分布式锁 


不能保障强一致性,只能保障最终的一致性:异步通知

4.1  延时双删

定义:延时双删就是正常删除缓存、修改数据库后还要延时一会再次删除缓存。

4.1.1 为什么要删除两次缓存?

因为从上面的场景导入,我们发现,无论是先删除缓存还是先修改数据库,都会有数据不一致,即脏数据的风险。

4.1.2 为什么要延迟双删呢?

延时一会是因为一般数据库都是主从分离,读写分离的。延时是为了让主库有时间通知到从库,所有数据库的更新操作全部走完。
延时双删极大程度上避免了脏数据的风险,但因为有延时的存在,延时时间不好控制,所以也不能说百分百避免。

4.2分布式锁(强一致性)

4.2.1 互斥锁

直接加互斥锁能保障数据的强一致性,但是性能较低。此时我们就需要优化一下互斥锁。因为存入缓存的数据,一般都是读多写少。为此我们引入两个单独的锁,分别叫共享锁排他锁。也被合称为读写锁。

4.2.2 读写锁

特点:强一致,性能低(写操作时还是单线程操作,别的线程会被阻塞)

应用场景:业务要求必须强一致时才使用

共享锁/读锁

共享锁,又叫读锁(readLock),加锁之后,其他线程可以共享读操作。

排他锁/独占锁

排他锁,又叫独占锁(writeLock),加锁之后,阻塞其他线程读和写操作。

举例

我们想要拿到共享锁或者排他锁,都需要先拿到读写锁。

1.获取读写锁对象

RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");

2.获取读锁和写锁对象

使用读写锁来分别获取读锁和写锁对象

RLock readLock = readWriteLock.readLock();
RLock writeLock = readWriteLock.writeLock();

3.读操作

步骤:

  1. 获取读锁readLock,允许多个线程同时读取数据。

  2. 从Redis中获取键为"item"+id的数据。

  3. 如果数据存在,直接返回。

  4. 如果数据不存在,查询最新的Item对象数据,并将其存入Redis。

  5. 最后释放读锁

public void getById(Integer id){
  RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
  RLock readLock = readWriteLock.readLock();
  try{
    readLock.lock();
    System.out.println("readLock...");
    Item item = (Item) redisTemplate.opsForValue().get("item"+id);
    if(item != null){
      return item;
    }
    //查询操作(这里是简化了)
    item = new Item(id, "华为手机", "华为手机", 5999.00);
    //写入缓存
    redisTemplate.opsForValue().set("item"+id, item);
    return item;
  }finally{
    readLock.unlock();
  }
}

4.写操作

步骤:

  1. 获取写锁writeLock,确保同一时间只有一个线程可以执行写操作。

  2. 更新数据Item对象属性。

  3. 模拟一个耗时操作(Thread.sleep(10000))。(这里是延时双删中的延时操作)

  4. 删除Redis中键为"item"+id的数据。

  5. 最后释放写锁。

public void updateById(Integer id){
  RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
  RLock writeLock = readWriteLock.writeLock();
  try{
    writeLock.lock();
    System.out.println("writeLock...");
     //更新数据(这里是简化写法)
    Item item = new Item(id, "华为手机", "华为手机", 5299.00);
    try{
    //这里是延时双删中的延时操作
      Thread.sleep(10000);
    }catch(InterruptedException e){
      e.printStackTrace();
    }
    redisTemplate.delete("item"+id);
  }finally{
    writeLock.unlock();
  }
}

代码参考文章:https://blog.csdn.net/zhiaidaidai/article/details/135030539

4.3 异步通知

异步通知的也有两个主流方案:MQ、Canal
在这里插入图片描述


在这里插入图片描述
canal的方案对于业务代码几乎是零侵入的。

5.面试回答

5.1 面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

(注意结合自身项目回答)

我:

我的这个项目有推荐用户的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,且推荐接口的并发量不高,所以我当时采用的读写锁保证的强一致性。

我采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。

5.2 面试官:那这个排他锁是如何保证读写、读读互斥的呢?

我:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法

5.3 面试官:你听说过延时双删吗?为什么不用它呢?

我:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除
缓存中的数据,其中这个延时多尔不太好确定,在延时的过程中可能会出现脏数据,并不能保证强
致性,所以没有采用它。

我的业务:

1.我的项目中定时任务使用了redission分布式锁,并且使用了看门狗机制,,保证了定时任务写入缓存数据的一致性

2.推荐接口(查询接口)存在数据不一致的场景:

并发场景下,例如缓存刚好过期,第一个线程请求推荐接口(查询接口),发现缓存为空,查询数据库,准备写入缓存.....

未写入缓存之前,此时数据库已经有用户注册,用户表写入了很多新用户数据(增加),或者有用户修改信息,删除操作

而此情况下有第二个线程,请求推荐接口,发现缓存未空,查询数据库,并且写入了缓存(此时是数据库相对第一个线程是最新的情况:期间存在数据增删改),而第一个线程最后覆盖了第二个线程写入的新缓存,后写入了旧的缓存

解决办法:

定时任务使用分布式锁,保证了项目部署到分布式环境下只有一个服务器执行,推荐接口(查询接口)使用读写锁,并且分布式锁和读写锁是相同粒度的锁,保证了双写一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值