oracle数据库缓存机制-oracle的缓存机制
点击上方“Java领袖”,选择“热门公众号”
干货第一时间送达!
介绍
为什么写这篇文章?
首先,缓存以其高并发、高性能的特点,在项目中得到了广泛的应用。 对于读取缓存oracle数据库缓存机制,大家没有疑惑,都是按照下图的流程进行业务操作。
但是在更新缓存方面,更新完数据库之后,是更新缓存还是删除缓存。 或者先删除缓存,再更新数据库。 其实大家之间也有很多争论。 目前还没有综合性的博客分析这些解决方案。 所以博主冒着被大家喷的风险写了这篇文章。
文章结构
本文由以下三个部分组成
1.解释缓存更新策略
2.分析每种策略的缺点
3、针对不足之处提供改进方案
文本
我先解释一下。 理论上,为缓存设置一个过期时间是保证最终一致性的一种解决方案。 在这种方案下,我们可以为缓存中存储的数据设置一个过期时间,所有的写操作都以数据库为准,我们只对缓存操作尽力而为。 也就是说,如果数据库写入成功,缓存更新失败,只要到了过期时间,后续的读请求自然会从数据库中读取新的值,然后回填缓存。 因此,接下来讨论的思路不依赖于为缓存设置过期时间的方案。
在这里,我们讨论三种更新策略:
先更新数据库,再更新缓存
先删除缓存,再更新数据库
先更新数据库,再删除缓存
应该没人问我为什么没有先更新缓存再更新数据库的策略。
1.先更新数据库,再更新缓存
这个计划普遍遭到大家的反对。 为什么? 有两个原因。
原因1(线程安全角度)
同时有更新操作的请求A和请求B,那么就会有
(1)线程A更新数据库
(2)线程B更新数据库
(3)线程B更新缓存
(4)线程A更新缓存
意思是请求A更新缓存应该早于请求B更新缓存,但是由于网络等原因,B更新缓存比A早,造成脏数据,所以不考虑。
原因二(业务场景视角)
有两点:
(1) 如果你有写数据库场景多,读数据场景少的业务需求,采用这种方案会导致数据还没读完就频繁更新缓存,浪费性能。
(2)如果你写入数据库的值不是直接写入缓存,而是经过一系列复杂的计算后才写入缓存。 然后,每次写入数据库后,写入缓存的值都会重新计算,这无疑是一种性能浪费。 显然,删除缓存更合适。
接下来的讨论是争议最大的,先删除缓存,再更新数据库。 或者先更新数据库,再删除缓存。
2.先删除缓存,再更新数据库
该程序导致不一致的原因是。 同时有一个更新操作的请求A,还有一个查询操作的请求B。 那么就会出现下面的情况:
(1)请求A执行写操作,删除缓存
(2)请求B查询,发现缓存不存在
(3)请求B查询数据库获取旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况会导致不一致。 而且,如果不采用缓存设置过期时间的策略,数据永远是脏数据。
那么,如何解决呢?采用延迟双删策略
伪代码如下
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
转换成中文说明是
(1) 先清除缓存
(2)重新写数据库(这两步和之前一样)
(3)休眠1秒,再次清除缓存
这样就可以再次删除1秒内产生的缓存脏数据。
那么,这个1秒是怎么确定的,应该休眠多长时间呢?
针对以上情况,请读者评估自己项目读取数据业务逻辑的耗时。 那么,写数据的休眠时间可以根据读数据耗时的业务逻辑增加几百毫秒。 这样做的目的是保证读请求结束oracle数据库缓存机制,写请求可以删除读请求造成的缓存脏数据。
如果使用mysql的读写分离架构呢?
ok,这样的话,数据不一致的原因如下,还是有两个请求,一个请求A进行更新操作,一个请求B进行查询操作。
(1)请求A执行写操作,删除缓存
(2)请求A向数据库写入数据,
(3)请求B查询缓存,发现缓存没有值
(4)请求B向从库查询。 此时主从同步还没有完成,所以查询的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库成为新值
以上情况就是数据不一致的原因。 还是用双删延迟策略。 但是修改了sleep时间,主从同步的延迟时间增加了几百ms。
采用这种同步淘汰策略,如果吞吐量降低了怎么办?
好的,然后将第二个删除设置为异步。 自己开一个线程,异步删除。 这样写的请求就不需要sleep一段时间就可以返回了。 这样做会增加吞吐量。
第二次删除,删除失败怎么办?
这是一个很好的问题,因为第二次删除失败,出现了下面的情况。 还有两个请求,一个请求A进行更新操作,一个请求B进行查询操作。 为了方便起见,假设它是一个单一的数据库:
(1)请求A执行写操作,删除缓存
(2)请求B查询,发现缓存不存在
(3)请求B查询数据库获取旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
(6) Request A试图删除Request B写入缓存的值,但是失败了。
ok,也就是说。 如果第二次缓存删除失败,缓存和数据库不一致的问题又会出现。
如何解决?
具体解决办法,看博主对(3)更新策略的分析。
3.先更新数据库,再删除缓存
首先,一句话。 老外提出了一种叫做“Cache-Aside pattern”的缓存更新例程。其中指出
另外,知名社交网站facebook也在论文“Scaling Memcache at Facebook”中提出,他们也是采用先更新数据库再删除缓存的策略。
这种情况不存在并发问题吗?
没有。假设会有两个请求,一个请求A进行查询操作,一个请求B进行更新操作,那么就会出现下面的情况
(1)缓存刚刚失败
(2)请求A查询数据库,得到一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将找到的旧值写入缓存
ok,如果出现以上情况,确实会出现脏数据。
然而,这种情况发生的可能性有多大?
上述情况的发生有一个先天条件,即步骤(3)中的写数据库操作比步骤(2)中的读数据库操作耗时少,这样就可以使步骤(4)先于步骤(5)。 不过仔细想想,数据库的读操作比写操作快多了(不然为什么要做读写分离,读写分离的意思是因为读操作速度更快,占用资源更少),所以步骤 (3) 比步骤 (2) 花费的时间更少,这种情况很少见。
假设,有人非要吵架,得了强迫症,非要解决怎么办?
如何解决上面的并发问题呢?
首先,为缓存设置一个有效时间是一个解决方案。 其次,采用策略(2)中给出的异步延迟删除策略,保证在读请求完成后执行删除操作。
不一致还有其他原因吗?
是的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的问题。 删除缓存失败怎么办? 不会有矛盾吗? 比如写数据的请求写入数据库,删除缓存失败,就会出现不一致的情况。 这也是缓存更新策略(2)最后留下的问题。
怎么解决?
提供有保证的重试机制就足够了。 这里有两组解决方案。
选项一:
如下所示
流程如下
(1) 更新数据库数据;
(2) 各种问题删除缓存失败
(3)将要删除的key发送到消息队列
(4)自己消费消息,得到需要删除的key
(5) 继续重试删除操作,直到成功
但是,这种解决方案的缺点是会导致大量入侵业务代码。 于是就有了第二个方案。 方案二,启动一个订阅程序,订阅数据库的binlog,获取需要操作的数据。 在应用程序中,启动另一个程序从订阅程序中获取信息,并删除缓存。
方案二:
流程如下图所示:
(1)更新数据库数据
(2)数据库将操作信息写入binlog日志
(3)订阅程序提取需要的数据和密钥
(4)再创建一个非业务代码获取信息
(5)尝试删除缓存操作,发现删除失败
(6)将信息发送到消息队列
(7) 再次从消息队列中获取数据,再次尝试操作。
备注:上面的binlog订阅程序在mysql中有一个现成的中间件canal,可以完成订阅binlog日志的功能。 至于oracle,博主目前不知道有没有现成的中间件可以使用。 另外,博主对重试机制使用了消息队列的方式。 如果对一致性的要求不是很高,就在程序中另外开启一个线程,每隔一段时间重试即可。 这些大家可以自由灵活的使用,只是提供一个思路。
总结
这篇文章其实是对网上现有一致性方案的总结。 关于先删除缓存再更新数据库的更新策略,也有维护一个内存队列的方案。 博主看了看,觉得实现起来极其复杂,也没有必要,所以文章里没必要给出。 最后希望大家有所收获。
Java负责人