分布式缓存是很多分布式应用中必不可少的组件,但是在使用分布式缓存的时候,可能会涉及到缓存和数据库的双存储双写。只要你是双写,就会存在数据的一致性问题,那么如何解决一致性问题呢?CacheAsidePattern最经典的缓存+数据库读写模式就是CacheAsidePattern。读取时,先读取缓存,如果没有缓存,则读取数据库,然后取出数据放入缓存,同时返回响应。更新时,先更新数据库,再删除缓存。为什么删除缓存而不是更新缓存?原因很简单。很多时候,在复杂的缓存场景中,缓存的不仅仅是直接从数据库中取值。比如某个表的某个字段可能更新了,那么对应的缓存就需要查询另外两个表的数据并进行计算,计算出缓存的最新值。此外,更新缓存的成本有时非常高。是不是说每次修改数据库,都要更新对应的缓存呢?在某些场景下可能是这样,但是对于更复杂的缓存数据计算场景就不是这样了。如果频繁修改缓存中涉及的多个表,缓存也会频繁更新。但问题是,这个缓存会不会被频繁访问?比如一个缓存涉及的表的字段在1分钟内被修改了20次或者100次,那么缓存就更新了20次或者100次;但是缓存在1分钟内只被读取一次,存在大量的冷数据。实际上,如果只是删除缓存,那么在1分钟内,缓存只会重新计算一次,开销大大降低,并且只在使用缓存的时候才计算缓存。其实删除缓存而不是更新缓存是一种惰性计算的思路。不要每次都重新做复杂的计算,不管会不会用到,而是让它在需要用到的时候重新计算。像mybatis,hibernate,都有懒加载的思想。查询一个部门时,该部门有一个员工列表。不用说,每查询一个部门,里面的1000个员工的数据也会同时被查出来。80%的情况下,要查询这个部门,只需要访问这个部门的信息即可。先查部门,同时访问里面的员工,然后只有要访问里面的员工的时候才去数据库查询1000个员工。最基本的缓存不一致问题及解决方法:先修改数据库,再删除缓存。如果删除缓存失败,会导致数据库中的新数据和缓存中的旧数据,数据不一致。解决办法:先删除缓存,再修改数据库。如果修改数据库失败,数据库中有旧数据,缓存为空,数据不会不一致。因为读取的时候没有缓存,读取的是数据库中的旧数据,然后更新到缓存中。比较复杂的数据不一致问题分析数据发生变化,先删除缓存,再修改数据库,此时还没有修改。一个请求来了,读取缓存,发现缓存为空,查询数据库,找到修改前的旧数据,放入缓存中。随后的数据更改过程完成数据库修改。完了,数据库和缓存中的数据不一样了。..为什么缓存在亿级流量、高并发的场景下会出现这个问题呢?只有在同时读取和写入数据时才会出现此问题。其实如果你的并发量很低,尤其是读并发量很低,每天的访问量只有10000,那么在极少数情况下,会出现刚刚描述的不一致的场景。但是问题是,如果每天上亿流量,每秒几万个并发读,只要每秒有一个数据更新请求,就有可能出现上述的数据库+缓存不一致的情况。解决方案如下:更新数据时,根据数据的唯一标识,将操作路由发送到一个jvm内部队列。读取数据时,如果发现数据不在缓存中,则重新读取数据+更新缓存的操作会根据唯一标识路由后发送到同一个jvm内部队列。一个队列对应一个工作线程,每个工作线程依次获取对应的操作,然后一个一个执行。在这种情况下,对于一个数据更改操作,先删除缓存,然后再更新数据库,但是更新还没有完成。这时候如果有读请求过来,读取一个空的缓存,可以先把缓存更新请求发送到队列中,此时会积压在队列中,然后同步等待缓存更新完成。这里有个优化点。在一个队列中,把多个updatecache请求串起来是没有意义的,所以可以做过滤。如果队列中已经有一个缓存更新请求,那么就不需要再放入一个更新请求。操作已经进入,只需要等待之前的更新操作请求完成即可。该队列对应的工作线程完成上一个操作对数据库的修改后,会执行下一个操作,即缓存更新操作。这时候会从数据库中读取最新的值,然后写入到缓存中。如果请求还在等待时间范围内,并且可以通过不断轮询得到值,则直接返回;如果请求等待超过一定时间,那么这次直接从数据库中读取当前的旧值。在高并发场景下,解决方案要注意以下问题:1.读请求长时间阻塞。因为读请求是非常轻微的异步,所以一定要注意读超时的问题。每个读请求必须在超时时间内。返回范围内。这种方案最大的风险是数据可能会频繁更新,导致大量更新操作积压在队列中,然后读请求会出现大量超时,最后导致大量请求将直接进入数据库。请务必运行一些模拟现实生活的测试,以查看数据更新的频率。另外,由于一个队列中多个数据项的更新操作可能会积压,需要根据自己的业务情况进行测试。你可能需要部署多个服务,每个服务会共享一些数据更新操作。如果100个item的库存修改操作实际挤在一个内存队列中,每个库存修改操作需要10ms完成,那么最后一个item的读请求可能要等10*100=1000ms=1s才能拿到数据,这个时候导致读取请求长期阻塞。一定要根据实际业务系统的运行情况,做一些压力测试,模拟线上环境,看看内存队列在最忙的时候可能会挤多少更新操作,这可能会导致最后一次更新操作对应多长时间读取请求会挂起吗?如果读请求在200ms以内返回,如果算下来即使在最忙的时候也会积压10个update操作,最多200ms的等待是可以的。如果一个内存队列中有很多更新操作可能积压,那么就需要增加机器,让部署在每台机器上的服务实例处理的数据更少,这样每个内存队列中更新操作的积压就会更少.其实根据之前的项目经验,一般来说,数据写入的频率是很低的,所以其实正常情况下,队列中积压的update操作应该很少。一般来说,像这种读高并发读缓存架构的项目写请求很少,能有几百个每秒的QPS就不错了。其实粗略算一下,如果每秒有500个写操作,如果分成5个时间片,每200ms有100个写操作,放在20个内存队列中,每个内存队列可能积压5个写操作。每次写操作经过性能测试,一般在20ms左右完成,所以对每个内存队列的数据的读请求最多会挂一会,200ms内肯定会返回。经过刚才的简单计算,我们知道单机支持的写QPS在几百是没有问题的。如果写QPS扩大10倍,那就扩大机器,扩大机器10倍,每台机器20个队列。2、读请求并发度过高。这里有必要做一个压力测试,保证当出现上述情况时,仍然存在大量读请求挂在服务上,延迟几十毫秒的风险。服务能不能处理好,需要多少台机器处理最大极端情况的峰值。但是因为不是同时更新所有的数据,所以缓存不会同时失效,所以每次都可能有少量数据的缓存失效,然后那些数据对应的读请求就来了,并发量不要特别大。3.多服务实例部署的请求路由如果服务部署了多个实例,必须保证执行数据更新操作和缓存更新操作的请求通过Nginx服务器路由到同一个服务实例。例如,对同一产品的所有读写请求都路由到同一台机器。可以根据某个请求参数自己在服务之间做hash路由,也可以使用Nginx的hash路由功能等等。4.热门产品路由问题导致请求偏斜。如果某个产品的读写请求特别高,全部发送到同一台机器的同一个队列,可能会导致某台机器压力过大。也就是说,因为只有商品数据更新时才会清空缓存,然后读写才会并发。因此,这取决于业务系统。如果更新频率不是太高,这个问题的影响不是特别大。但确实有些机器上的负载可能会更高。
