关系型数据库对于高并发的处理能力并不强;缓存是在内存中处理,并不需要磁盘IO,所以有高并发和高性能的特性,在项目中广泛使用。
一般缓存应用设计流程
查询数据时:先查询缓存,是否有数据,如果有就直接返回;如果没有数据,就从数据库查询;数据库查询到数据就写入缓存,然后返回;否则直接返回;
由于数据库的操作和缓存的操作不可能在一个事务中,那势必会出现数据库写入失败,缓存不能更新,缓存写入失败等问题
这就会存在缓存与数据库数据不一致情况的情况
从理论上讲,给缓存设置过期时间,是保证最终一致性的解决方案。
方案一:先更新数据库,再更新缓存
a.(线程安全角度出发)
同时有请求A和请求B进行更新操作,会出现:线程A更新了数据库,线程B更新了数据库,线程B更新了缓存,线程A更新了缓存;
请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑
b.(业务场景角度)
如果写库较多,而读库较少的场景,采用此方案就会导致,数据还没读到,缓存就被频繁更新,浪费性能;
如果你写入数据库的值,并不是直接写入缓存,还需要经过一系列复杂的计算再写入,那么无疑浪费性能;显然,删除缓存更合适。
方案二:先删除缓存,再更新数据库
同时,请求A进行更新操作,请求B进行查询操作,会出现以下情形:
请求A进行写操作,删除缓存;请求B发现缓存不存在,查询数据库获得旧值,将旧值写入缓存;请求A将新值写入缓存;
这样就会导致缓存与数据库数据不一致
如何解决?延时双删。先删除缓存,在写库,延时1秒后再删除缓存;
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
问题:对于读写分离架构,会出现什么情况?
场景:请求A进行写操作,请求B进行读操作;
请求A进行写操作删除缓存;请求B发现缓存不在;查询数据库,这个时候主从还未同步,查询到的是旧值;请求B将旧值写入到缓存;数据库完成主从同步,从库变为新值。
还是使用双删延时策略,只是在原延时时间上加几百毫秒;
这种方式可能会造成吞吐量降低,那么就将第二次删除作为异步处理,这样写请求就不用沉睡一段时间后才响应。
流程如下所示
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能