这几天到处乱转,不知道在哪里看到了缓存的双写,突然想起来这块虽然简单,但是细节还是够多的大家可以关注一下。这篇文章会详细讲一下双写一致性。首先,我们知道现在业务中应用缓存是非常普遍的,甚至可能和数据库一样频繁。如果你的用户量增加了,直接用一个裸MySQL来承担所有的压力,显然是不合理的。这里的缓存目前业界主流的是Redis。之前有讲过Redis相关的文章,这里不再赘述。有兴趣的可以阅读:Redis基本数据结构及使用Redis数据持久化Redis主从同步RedisSentinel高可用Redis集群详解讲解量,感觉写了这么多Redis不罗列。..言归正传。在我们的业务中,一般需要将一些常用的热点数据(或者不经常变化但比较大的数据)放到Redis中进行缓存。下次业务请求查询时,可以直接返回Redis中的数据,减少业务系统与数据库的交互。这样做有两个好处,一个是减轻了数据库的压力,另一个就不用说了,可以有效的降低API对相同数据的RT(ResponseTime)。后者其实还可以,而且对于减轻数据库的压力尤为重要,因为虽然我们的业务服务可以以较低的成本进行横向扩展,但是数据库做不到。这里的不能,并不代表数据库不能扩容。在主从架构下,MySQL可以通过扩展Slave节点的数量来有效的横向扩展读请求。但是由于Master节点不是无状态的,所以扩容比较麻烦。是的,很麻烦,横向扩展也不是不可以。但是在那种架构下,我举个例子,在master-master架构下,会带来很多意想不到的数据同步问题,给整个架构带来新的复杂性。之前写的MySQL主从原理中提到,双主架构更多的是HA而不是负载均衡。因此,相同的数据会同时存在于Redis和MySQL中。如果数据没有变化,那就是完美匹配。但现实很骨感,这个数据有99.9999%的概率肯定会变。为了保持Redis和MySQL中数据的一致性,就诞生了双写问题。CacheAsidePattern最经典的方案是CacheAsidePattern,它定义了一套缓存和数据库的读写方案,保证缓存和数据库的数据一致性。具体方案CacheAsidePattern分为两类Cases,分别是read和write。对于读请求,会先在Redis中查询数据,命中则直接返回数据。而如果不是从缓存中获取,就会去DB中查询,将查询到的数据写回Redis,然后返回response。更新比较简单,但也是争议最大的。当收到写请求时,先更新DB中的数据,成功后删除缓存中的数据。请注意,这是删除,而不是更新。因为在实际生产中,缓存中可能会存储不止一个单一的值,比如true、false或者1、19。为什么会被删除?也可以将整个结构存储在缓存中,其中包含很多字段。那么每更新一个字段,是否需要从缓存中读取数据,解析成对应的结构体,然后更新对应字段的值,再写回缓存?还是直接把原来的缓存存Delete,然后把最新的数据写入缓存?其实乍一看,好像也没什么不妥。我不应该这样更新吗?在这里,我们更多的关注更新的方式,而忽略了更多的必需品。我们更新这个值之后,在接下来的一段时间内会不会频繁访问呢?也许吧,但它可能根本无法访问。既然可能无法访问,为什么还要更新呢?而且,更新缓存带来的开销有时会非常大。但是,这只是单个缓存数据源的情况。如果一个读模型缓存在缓存中,它的数据是从多个表的数据中计算出来的,开销会更大。阅读模型简单的理解就是利用已有的数据对一些数据进行计算统计。这种思路类似于懒加载的方式,只在需要的时候计算。争议在哪里?前面说了更新的顺序是先更新DB中的数据,成功后删除缓存。但是有人认为应该先删除缓存,然后再更新DB中的数据。乍一看,问题可能并不明显。甚至觉得有点道理。因为如果先删除缓存,如果删除操作失败,DB中的数据不会被更新,所以缓存和DB中数据的一致性也能得到保证。而且,如果删除缓存成功了,但是更新DB失败了,下次取数据的时候再把数据写回缓存也是非常合理的。然而,这只是在单线程的情况下。如果是在多线程的情况下,会直接造成致命的数据不一致。上面的流程图详细描述了这种情况。更新请求1刚刚删除了缓存中的数据,查询请求2过来了。查询请求2会发现缓存是空的,所以按照CacheAsidePattern的读请求标准,会从DB中加载最新的数据,写入缓存。此时更新请求1还没有更新DB,所以查询请求2写入缓存的数据还是旧数据。这样查询请求3会在下次更新前读取旧数据。然后,updaterequest1更新最新的数据到DB,缓存和DB中的数据不一致。实际上CacheAsidePattern中的模式在某些情况下还是会造成数据不一致。但是这个概率很低,因为触发这种不一致的条件太苛刻了。首先是缓存必须失效,然后读请求和写请求并发执行,读请求在写请求之后执行。为什么说概率不高呢?首先,在实际生产中,读请求一般要比写请求快很多。另外,读请求到DB请求数据的时间必须早于写请求,写缓存的时间必须晚于写请求。和当初的情况相比,条件已经很严苛了。如果实在不能容忍,可以采用2PC方式保证数据一致性,也可以序列化请求来解决,但代价是牺牲并发量。End其实还有其他几种解决方案,比如ReadThroughtPattern、WriteThroughPattern、WriteAround、WriteBehindCachingPattern等等。但是这些相对于CacheAsidePattern来说还是比较简单的,大家可以自己去了解一下。
