java缓存与数据库如何保证高一致性,缓存与数据库双写一致问题
java基础教程栏目保证缓存与数据库的双写的一致性
如何解决写爬虫IP受阻的问题?立即使用。
请抬起你的头,我的公主,否则皇冠会掉下来。
分布式缓存是许多分布式应用中必不可少的组件。但是使用分布式缓存时,可能会涉及到缓存和数据库的双重存储和双重写入。只要是双写,就会有数据一致性问题。那么如何解决一致性问题呢?
Cache Aside Pattern
读写缓存数据库最经典的模式是缓存旁路模式。
读取时,先读取缓存。如果没有缓存,读取数据库,然后取出数据放入缓存,返回响应。
更新时,先更新数据库,再删除缓存。
为什么是删除缓存,而不是更新缓存?
原因很简单。很多情况下,在复杂点的缓存场景中,缓存不仅仅是直接从数据库中取出的值。
比如某个表的一个字段可能被更新,然后它对应的缓存需要查询另外两个表的数据,计算缓存的最新值。
此外,更新缓存的成本有时非常高。这是否意味着每次修改数据库时,都必须更新相应缓存的副本?也许在某些场景下是这样,但对于更复杂的缓存数据计算的场景就不是这样了。如果您频繁修改缓存中涉及的多个表,缓存将会频繁更新。但问题是,这个缓存会被频繁访问吗?
举个栗子,缓存中涉及的一个表的一个字段在1分钟内被修改20次或者100次,那么缓存就被更新20次或者100次;但是这个缓存1分钟才读一次,而且有很多冷数据。实际上,如果只是删除缓存,那么在1分钟内,缓存只会重新计算一次,开销会大大降低。只有当缓存被使用时,缓存才会被计数。
其实删除缓存,而不是更新缓存,是一种偷懒的计算思路。不要每次都做复杂的计算,不管会不会用,而是让它在需要用的时候重新计算。像mybatis和hibernate这样的人都有懒加载的想法。当您查询一个部门时,该部门会带来一个员工列表。不用说,每查询一个部门,里面1000个员工的数据也查出来。80%的时候,如果你查这个部门,你只需要访问这个部门的信息。先查部门,同时走访里面的员工。那么这个时候,只有你想访问里面的员工,才会去数据库查询1000个员工。
最初级的缓存不一致问题及解决方案
问题:在删除缓存之前修改数据库。如果删除缓存失败,会导致数据库新数据和缓存旧数据,数据不一致。
解决方案:首先删除缓存,然后修改数据库。如果数据库修改失败,那么数据库是旧的,缓存是空的,所以数据不会不一致。因为读取时没有缓存,所以读取数据库中的旧数据,然后更新到缓存中。
比较复杂的数据不一致问题分析
数据变了。首先,删除缓存,然后修改数据库。此时,它还没有被修改。一个请求来了,读取缓存,发现缓存是空的,查询数据库,找到修改前的旧数据,放入缓存。然后数据修改程序完成对数据库的修改。
就是这样。数据库和缓存中的数据是不同的。
为什么在上亿流量、高并发的场景下,缓存会出现这个问题?
这种问题只有在同时读写一段数据时才会出现。事实上,如果你的并发量很低,尤其是读取并发量很低,每天10000次访问,那么在极少数情况下,就会出现刚才描述的不一致的场景。但问题是,如果每天的流量是几亿,并发读取是每秒几万,那么只要有一个每秒数据更新的请求,就可能出现上述的数据库缓存不一致的情况。
解决方案如下:
当更新数据时,根据数据的唯一标识,操作被路由并发送到jvm的内部队列。在读取数据时,如果发现数据不在缓存中,就会重新读取数据更新缓存,根据唯一标识符路由后发送到同一个jvm内部队列。
一个队列对应一个工作线程,每个工作线程依次获取相应的操作,然后逐一执行。在这种情况下,一个数据更改操作,首先删除缓存,然后更新数据库,但更新尚未完成。此时,如果有读取请求到来,读取空缓存,可以先将缓存更新的请求发送到队列中,该请求会积压在队列中,然后等待缓存更新同步完成。
这里有一个优化点。在一个队列中,实际上,将多个缓存更新请求串在一起是没有意义的,所以可以进行过滤。如果发现队列中已经有一个缓存更新请求,不需要再放一个更新请求进去,只需要等待前面的更新操作请求完成即可。
在对应于该队列的工作线程完成了前一个操作的数据库的修改之后,它将执行下一个操作,即缓存更新操作。此时,将从数据库中读取最新值,然后写入缓存。
如果请求还在等待时间范围内,并且可以通过连续轮询获得值,则直接返回;如果请求等待的时间超过一定时间,那么这次将直接从数据库中读取当前的旧值。
在高并发的场景中,解决方案应该注意:
1、读请求长时阻塞
因为读取请求非常轻微的异步,所以必须注意读取超时的问题。每个读取请求必须在超时期限内返回。
这种解决方案最大的风险是数据可能会频繁更新,导致队列中的更新操作积压,然后大量的读请求会超时,最后大量的请求会直接到数据库。一定要通过一些真实的测试,看看数据更新的频率。
另外,由于一个队列中可能有多个数据项的更新操作积压,所以需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分担一些数据更新操作。如果将100个商品的库存修改操作挤在一个内存队列中,每一个库存修改操作都需要10ms才能完成,那么最后一个商品的读取请求可能要等待10 * 100=1000ms=1s才能获得数据,这样就会导致读取请求长期阻塞。
一定要根据实际的业务系统运行做一些压力测试,模拟在线环境,看看最忙的时候内存队列里可能会挤多少个更新操作,可能会导致最后一个更新操作对应的读请求,会挂起多久。如果读请求200ms返回,如果你计算一下,即使在最忙的时候,你有10个更新操作的积压,最多等200ms,那是可以的。
如果一个内存队列可能有特别大的更新操作积压,那么您必须添加机器,以便部署在每台机器上的服务实例可以处理更少的数据,然后每个内存队列中的更新操作积压就会更少。
其实根据之前的项目经验,一般来说数据写入的频率是很低的,所以实际上队列中积压的更新操作应该是很少的。一般来说,高并发和缓存架构的项目写请求很少,每秒QPS能达到几百就不错了。
实际粗略测算一下
如果每秒有500个写操作,如果分成5个时间片,那么每200ms就会有100个写操作,放在20个内存队列中,每个内存队列可能有5个写操作的积压。每次写操作的性能测试后,一般在20ms左右完成,所以对每个内存队列的数据的读请求最多只会挂起一会儿,200 ms内肯定会返回。
经过刚才简单的计算,我们知道,写几百个单机支持的QPS是没有问题的。如果写QPS扩展10倍,那么扩展机器,扩展机器10倍,每台机器有20个队列。
2、读请求并发量过高
这里必须做压力测试,保证当上述情况发生时,还有一个风险,就是突然大量的读请求会挂在服务上,延迟几十毫秒。看看服务能不能处理,需要多少台机器来处理最大极限情况的峰值。
但是并不是所有数据都同时更新,缓存也不会同时失效,所以每次都是少数数据的缓存失效,然后那些数据对应的读取请求来了,并发应该不会特别大。
3、多服务实例部署的请求路由
可能部署了此服务的多个实例,因此必须确保数据更新和缓存更新请求通过Nginx服务器路由到同一个服务实例。
例如,对同一商品的读写请求都被路由到同一台机器。可以自己按照某个请求参数做服务之间的哈希路由,也可以使用Nginx之类的哈希路由功能。
4、热点商品的路由问题,导致请求的倾斜
万一某个商品的读写请求极高,并且全部命中同一台机器的同一队列,可能会造成某台机器压力过大。也就是说,因为只有商品数据更新时才会清空缓存,进而造成读写并发,所以实际上应该是按照业务系统来看的。如果更新频率不太高,这个问题的影响不是特别大,但是有可能部分机器的负载会更高。那是java实现的细节,保证缓存和数据库双写的一致性。请多关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。