面试官第二问:redis作为为缓存,mysql的数据如何与redis进行同步呢?
这个问问题实际上考验的是双写一致性
双写一致性:当修改了数据库的数据也要同时更新缓存的数据,数据库的数据和缓存的数据要保持一致
ps:在回答这个问题之前,一定要讲明你的业务背景,你是一致性要求高的业务,还是允许延迟一致的业务。
对于一致性要求高的业务:
在一般的业务中,我们会进行以下操作:
读操作:
写操作:延迟双删
延迟双删:改数据库前后都进行一次删除缓存的操作,改库后的删除为延迟删除
由延迟双删我们可以提出以下三个问题:
一,为什么要延迟双删?
在高并发场景下,单纯依靠一次删除无法彻底解决缓存和数据库不一致的问题,故可以使用延迟双删来解决一些对于一致性要求不极端的场景。
二,先删除缓存还是先删除数据库?
1,先删除缓存再操作数据库
假定开始时,缓存是10,数据库是10
下图是正常逻辑情况
特殊情况呢?
我们知道,线程之间是交替执行的。线程一删除缓存后,线程2开始执行,但是无法查询到缓存,于是就查询数据库,将数据库数据10写入到了缓存当中。此时缓存是10。
回到线程一,线程一在线程二结束后继续执行,将数据库更新,写入值为20。这样,数据库和缓存之间的数据就不一致了。
2,先操作数据库再删除缓存
假定开始时,缓存是0,数据库是10
下图是正常逻辑情况
特殊情况呢?
在这张图里,线程一先操作,查询缓存未命中,开始查询数据v=10尝试写入缓存。此时,线程二加入,将数据库数据更新为v=20,再删除缓存(缓存中没有数据的情况下删不删缓存都无所谓的)。接下来线程一写入缓存,写入开始的v=10。
此时,缓存中值为10,数据库中为20,据库和缓存之间的数据又不一致了。
我们发现,无论是先删除缓存还是先先删除数据库,都会存在数据不一致的现象。
所以我们选择再修改数据库前后都进行一次删除缓存的操作
三,为什么要延迟删除?
我们一般情况下数据库是主从模式,独显分离的。我们需要给数据库一点时间将数据从主节点连接到从节点。但是延迟的时间不好确定,所以也有脏数据的风险。
如何做到完全没有脏数据呢?我们可以考虑双写一致的写法:
双写一致(利用互斥锁实现)通过加锁的方式保证一个线程的任务在执行过程中不会被其他线程交替执行所影响。
显而易见的是,这样的操作性能会很低。所以我们可以做一些优化:
我们建议把读多写少的数据存入缓存,把读少写多的数据存入数据库中。
这里的实现需要涉及俩个锁(读写锁)的应用,共享锁,排他锁
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作(可读不可写)
排他锁,独占writeLock,加锁之后,阻塞其他线程读写操作
当我们读数据的时候,可以加入共享锁,限制写的功能,当我们写数据的时候,可以加入排他锁,阻止其他线程读写。
具体实现代码如下:
读锁:(这里的开锁指的是打开锁的功能,不是“打开锁”)
public Item getByid(Integer id){
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
//读之前加读锁,读锁的作用就是等待lockkey释放写锁后再读
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);
//返回数据
return item;
}finally {
readLock.unlock();
}
}
写锁:
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(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
//删除缓存
redisTemplate.delete("ITEM"+id);
}finally {
writeLock.unlock();
}
}
读写锁的功能具有强一致性但是性能过低,会阻塞其他线程对信息的获取,适合的业务必须是强一致性的业务。
对于允许延迟一致的业务:
我们可以用异步通知来保证数据的最终一致性(利用mq)
我们需要保证mq的可靠性来保证数据的最终一致性。
也可以使用Canal的异步通知:
如果业务可以接受短暂延迟,canal是不错的选择。
总结: