基本理论

CAP原则

  CAP原则指的是,在一个分布式系统中,以下三个原则中最多只能满足两个:

  1、一致性(Consistency);

  2、可用性(Availability);

  3、分区容错性(Partition tolerance)。

BASE理论

原理

  BASE理论是Basically Available(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)三个短语的缩写。BASE理论是对CAP原则中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP原则逐步演化而来。更具体地说,BASE理论是对 CAP 中 AP 方案的一个补充。其基本思路就是:通过业务,牺牲强一致性而获得可用性,允许数据在一段时间内是不一致的,但是最终要达到一致性状态。

  BASE理论的解释如下:

  1、基本可用(Basically Available):分布式系统在出现不可预知故障的时候,允许损失部分可用性,比如响应时间可能会变长,功能可能会降级。

  2、软状态(Soft State):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

  3、最终一致性(Eventually Consistent):最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性

BASE理论和ACID理论

  ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用。ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。

Redis缓存一致性解决方案

基本原则

  Redis缓存是BASE理论的一个典型应用。Redis缓存和数据库相当于存放数据的两个节点,为了保证系统的可用性,需要牺牲一部分的一致性,也就是说,Redis缓存和数据库允许短时间内数据不一致,但是最终要达到一致,这是各种Redis缓存方案所遵循的一个基本原则。

Cache-Aside(旁路缓存)

  所谓旁路缓存,就是读取缓存、读取数据库和更新缓存的操作都在应用系统来完成,是业务系统最常用的缓存策略。

  旁路缓存的方案有四种,分别是:

  1、更新数据库的同时更新缓存,但是先更新缓存,再更新数据库;

  2、更新数据库的同时更新缓存,但是先更新数据库,再更新缓存;

  3、更新数据库的同时删除缓存,先删除缓存,再更新数据库,查询时再更新缓存;

  4、更新数据库的同时删除缓存,先更新数据库,再删除缓存,查询时再更新缓存。

  方案1、2的缺点是:

  1、在多个请求并发更新数据时,由于更新数据库和更新缓存这两步不能构成一个原子操作,因此很容易导致缓存和数据库最终不一致。解决办法有两个,一是给更新操作加分布式锁,保证同一时刻只有一个请求可以更新,但这些会降低写入的性能,二是给缓存设置较短的过期时间,保证最终的一致性;

  2、更新后的缓存很可能会长时间不被访问,如果每次更新数据库时都更新缓存,很可能造成资源浪费,更好的方法是更新数据库时删除缓存,等到查询数据时再更新缓存,这是lazy-loading思想的一种体现。

  方案1、2的优点是:

  1、由于缓存不会被删除,因此缓存的命中率很高。

  方案3、4解决了并发更新导致的数据不一致问题,但因为要删除缓存,所以缓存命中率相对低一点。

  方案3的缺点是:

  1、由于更新数据库的速度比更新缓存慢很多,因此当更新请求和查询请求并发执行时,如果在删除缓存和更新数据库之间,查询请求用数据库中的旧数据更新了缓存,就会导致数据库和缓存最终不一致。

  为了解决方案3的这个问题,可以采用延迟双删的方法,更新数据的伪代码如下:

// 先删除缓存
redis.delete(key);
// 再更新数据库
mysql.update(key);
// 等待查询请求中的更新缓存操作执行完成后,再删除缓存
Thread.sleep(n);
redis.delete(key);

  方案4也可能会出现缓存和数据库的最终不一致,但几率很小,仅限于下面的情况:查询请求执行时缓存刚好过期,在它更新完缓存之前,更新请求完成了更新数据库、删除缓存的操作,我们知道更新数据库比更新缓存慢很多,因此这种情况出现的概率很低,但是为了避免发生极端情况,最好给缓存加上过期时间。

  Cache-Aside的四种缓存方案总结如下表所示:


更新数据库时更新缓存

先删除缓存、再更新数据库,查询时更新缓存

先更新数据库、再删除缓存,查询时更新缓存

优点

1、缓存命中率高

1、使用懒加载的思想,节约了性能

1、使用懒加载的思想,节约了性能;2、发生缓存和数据库不一致的可能性很小。

缺点

1、有较大可能发生缓存和数据库最终不一致;2、缓存更新后很可能不会使用,造成性能浪费。

1、缓存命中率低;2、有较大可能发生缓存和数据库最终不一致。

1、缓存命中率低

如何保证最终一致性

1、对更新操作加分布式锁,保证同一时刻只有一个更新操作,但会降低写入性能;2、给缓存设置较短的过期时间。

1、延迟双删;2、给缓存设置过期时间。

1、给缓存设置过期时间

  基于上面的原因,推荐使用先更新数据库、再删除缓存,查询时更新缓存,给缓存设置过期时间作为兜底的方案。

如何保证缓存删除成功

  在第4种方案中,需要保证缓存能够删除成功,如果缓存删除失败,会导致缓存和数据库最终不一致。

  保证缓存能够删除成功的方法主要有两个:

  1、引入消息队列,将要删除的缓存数据放入消息队列,让消费者去删除缓存。如果删除缓存失败,可以从消息队列中重新获取数据,然后再次删除缓存,这就是重试机制。如果超过一定的重试次数还没有删除成功,就向业务层发送报错信息。如果删除成功,就从消息队列中移除缓存数据。

  2、先更新数据库,再删缓存的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

  问题:为什么不能用Spring事务来保证更新数据库和删除缓存同时成功或同时失败?

Read Through、Write Through、Write Behind

  Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

  Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)。

Write Behind 又叫 Write Back。一些了解Linux操作系统内核的同学对write back应该非常熟悉,这不就是Linux文件系统的Page Cache的算法吗?是的,你看基础这玩意全都是相通的。所以,基础很重要,我已经不是一次说过基础很重要这事了。

  Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

  但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。

  另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

引用自:缓存更新的套路

Redis缓存失效解决方案

缓存穿透

  缓存穿透指的是用户访问的数据在缓存和数据库中都不存在,导致无法建立缓存,请求会一直访问数据库,如果请求数量很大时,就会给数据库造成压力。

  缓存穿透的解决办法有三种:

  1、在程序入口处对请求参数进行验证,过滤掉恶意的请求;

  2、在请求到达缓存之前,使用布隆过滤器过滤掉数据库中不存在的数据,由于布隆过滤器存在一定误判概率,剩下的数据有一小部分是数据库中不存在的数据;

  3、将数据库中不存在的数据存入缓存,值设置为"null",同时设置过期时间,这样当缓存建立起来,这些请求就不会直接访问数据库了。

缓存击穿

  缓存击穿指的是缓存中的某个key突然过期,此时如果有大量的请求查询这个key,会直接访问数据库,给数据库造成压力。

  缓存击穿的解决办法有:

  1、设置热点数据永不过期;

  2、为缓存设置逻辑过期时间,当缓存逻辑过期时,直接返回过期数据,然后新开一个独立线程来重建缓存,重建缓存的操作使用互斥锁进行保护,保证同一时刻只能有一个线程在重建缓存,互斥锁要设置超时时间,防止出现锁长时间被占用的情况。这种方法虽然会导致缓存和数据库短时间的不一致,但并发性能较高。

缓存雪崩

  缓存雪崩和缓存击穿比较类似,它指的是缓存中大量key同时过期,此时如果有大量的请求查询这些key,会直接访问数据库,给数据库造成压力。

  缓存雪崩的解决办法,除了包括缓存击穿的解决办法外,还有一条:

  1、为这些key设置随机过期时间,防止同时过期。