目录
5.1 面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
前言
一定要结合面试题回答,分为两种情况: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.读操作
步骤:
-
获取读锁
readLock
,允许多个线程同时读取数据。 -
从Redis中获取键为
"item"+id
的数据。 -
如果数据存在,直接返回。
-
如果数据不存在,查询最新的
Item
对象数据,并将其存入Redis。 -
最后释放读锁
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.写操作
步骤:
-
获取写锁
writeLock
,确保同一时间只有一个线程可以执行写操作。 -
更新数据
Item
对象属性。 -
模拟一个耗时操作(
Thread.sleep(10000)
)。(这里是延时双删中的延时操作) -
删除Redis中键为
"item"+id
的数据。 -
最后释放写锁。
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.推荐接口(查询接口)存在数据不一致的场景:
并发场景下,例如缓存刚好过期,第一个线程请求推荐接口(查询接口),发现缓存为空,查询数据库,准备写入缓存.....
未写入缓存之前,此时数据库已经有用户注册,用户表写入了很多新用户数据(增加),或者有用户修改信息,删除操作
而此情况下有第二个线程,请求推荐接口,发现缓存未空,查询数据库,并且写入了缓存(此时是数据库相对第一个线程是最新的情况:期间存在数据增删改),而第一个线程最后覆盖了第二个线程写入的新缓存,后写入了旧的缓存
解决办法:
定时任务使用分布式锁,保证了项目部署到分布式环境下只有一个服务器执行,推荐接口(查询接口)使用读写锁,并且分布式锁和读写锁是相同粒度的锁,保证了双写一致。