灵魂拷问
前几篇文章咱们介绍了缓存的优点以及数据一致性的问题,在一个面临高并发系统中,缓存几乎成了每一个架构师应对高流量的首冲解决方案,可是,一个好的缓存系统,除了和数据库一致性问题以外,还存在着其余问题,给总体的系统设计引入了额外的复杂性。而这些复杂性问题的解决方案也直接了影响系统的稳定性,最多见的好比缓存的命中率问题,在一个高并发系统中,核心功能的缓存命中率通常要保持在90%以上甚至更高,若是低于这个命中率,整个系统可能就面临着随时被峰值流量击垮的可能,这个时候咱们就须要优化缓存的使用方式了。mysql
据说你还不会缓存?redis
若是按照传统的缓存和DB的流程,一个请求到来的时候,首先会查询缓存中是否存在,若是缓存中不存在则去查询对应的数据库。假如系统每秒的请求量为10000,而缓存的命中率为60%,则每秒穿透到数据库的请求数为4000,对于关系型数据库mysql来讲,每秒4000的请求量对于分了一主三从的Mysql数据库架构来讲也已经足够大了,再加上主从的同步延迟等诸多因素,这个时候你的mysql已经行走在down机边缘了。sql
缓存的最终目的,是在保证请求低延迟的状况下,尽最大努力提升系统的吞吐量
那缓存系统可能会影响系统崩溃的缘由有那些呢?数据库
缓存穿透是指:当一个请求到来的时候,在缓存中没有查找到对应的数据(缓存未命中),业务系统不得不从数据库(这里其实能够笼统的成为后端系统)中加载数据
发生缓存穿透的缘由根据场景分为两种:后端
当数据在缓存和数据库都不存在的时候,若是按照通常的缓存设计,每次请求都会到数据库查询一次,而后返回不存在,这种场景下,缓存系统几乎没有起任何做用。在正常的业务系统中,发生这种状况的几率比较小,就算偶尔发生,也不会对数据库形成根本上的压力。设计模式
最可怕的是出现一些异常状况,好比系统中有死循环的查询或者被黑客攻击的时候,尤为是后者,他会故意伪造大量的请求来读取不存在的数据而形成数据库的down机,最典型的场景为:若是系统的用户id是连续递增的int型,黑客很容易伪造用户id来模拟大量的请求。缓存
这种场景通常属于业务的正常需求,由于缓存系统的容量通常是有限制的,好比咱们最经常使用的Redis作为缓存,就受到服务器内存大小的限制,因此全部的业务数据不可能都放入缓存系统中,根据互联网数据的二八规则,咱们能够优先把访问最频繁的热点数据放入缓存系统,这样就能利用缓存的优点来抗住主要的流量来源,而剩余的非热点数据,就算是有穿透数据库的可能性,也不会对数据库形成致命压力。服务器
换句话说,每一个系统发生缓存穿透是不可避免的,而咱们须要作的是尽可能避免大量的请求发生穿透,那怎么解决缓存穿透问题呢?解决缓存的穿透问题本质上是要解决怎么样拦截请求的问题,通常状况下会有如下几种方案:数据结构
当请求的数据在数据库中不存在的时候,缓存系统能够把对应的key写入一个空值,这样当下次一样的请求就不会直接穿透数据库,而直接返回缓存中的空值了。这种方案是最简单粗暴的,可是要注意几点:
//获取用户信息 public static UserInfo GetUserInfo(int userId) { //从缓存读取用户信息 var userInfo = GetUserInfoFromCache(userId); if (userInfo == null) { //回写空值到缓存,并设置缓存过时时间为10分钟 CacheSystem.Set(userId, null,10); } return userInfo; }
布隆过滤器:将全部可能存在的数据哈希到一个足够大的 bitmap 中,一个必定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
布隆过滤器有几个很大的优点
具体能够参见以前的文章或者百度脑补一下布隆过滤器:
因为布隆过滤器基于hash算法,因此在时间复杂度上是O(1),在应对高并发的场景下很是合适,不过使用布隆过滤器要求系统在产生数据的时候须要在布隆过滤器同时也写入数据,并且布隆过滤器也不支持删除数据,由于多个数据可能会重用同一个位置。
缓存雪崩是指缓存中数据大批量同时过时,形成查询数据库数据量巨大,引发数据库压力过大致使系统崩溃。
与缓存穿透现象不一样,缓存穿透是指缓存中不存在数据而形成会对数据库形成大量查询,而缓存雪崩是由于缓存中存在数据,可是同时大量过时形成。可是本质上是同样的,都是对数据库形成了大量的请求。
不管是穿透仍是雪崩都面临着一样的数据会有多个线程同时请求,同时查询数据库,同时回写缓存的一致性问题。举例来讲,当多个线程同时请求用户id为1的用户,这个时候缓存正好失效,那这多个线程同时会查询数据库,而后同时会回写缓存,最可怕的是,这个回写的过程当中,另一个线程更新了数据库,就形成了数据不一致,这个问题在以前的文章中着重讲过,你们必定要注意。
一样的数据会被多个线程产生多个请求是产生雪崩的一个缘由,针对这种状况的解决方案是把多个线程的请求顺序化,使其只有一个线程会产生对数据库的查询操做,好比最多见的锁机制(分布式锁机制),如今最多见的分布式锁是用redis来实现,可是redis实现分布式锁也有必定的坑,能够参见以前的文章(若是使用的是Actor模型的话会在无锁的模式下更优雅的实现请求顺序化)
多个缓存key同时失效的场景是产生雪崩的主要缘由,针对这样的场景通常能够利用如下几种方案来解决
给缓存的每一个key设置不一样的过时时间是最简单的防止缓存雪崩的手段,总体思路是给每一个缓存的key在系统设置的过时时间之上加一个随机值,或者干脆是直接随机一个值,有效的平衡key批量过时时间段,消掉单位之间内过时key数量的峰值。
public static int SetUserInfo(int userId) { //读取用户信息 var userInfo = GetUserInfoFromDB(userId); if (userInfo != null) { //回写到缓存,并设置缓存过时时间为随机时间 var cacheExpire = new Random().Next(1, 100); CacheSystem.Set(userId, userInfo, cacheExpire); return cacheExpire; } return 0; }
这种场景下,能够把缓存设置为永不过时,缓存的更新不是由业务线程来更新,而是由专门的线程去负责。当缓存的key有更新时候,业务方向mq发送一个消息,更新缓存的线程会监听这个mq来实时响应以便更新缓存中对应的数据。不过这种方式要考虑到缓存淘汰的场景,当一个缓存的key被淘汰以后,其实也能够向mq发送一个消息,以达到更新线程从新回写key的操做。
和数据库同样,缓存系统的设计一样须要考虑高可用和扩展性。虽然缓存系统自己的性能已经比较高了,可是对于一些特殊的高并发的热点数据,仍是会遇到单机的瓶颈。举个栗子:假如某个明星出轨了,这个信息数据会缓存在某个缓存服务器的节点上,大量的请求会到达这个服务器节点,当到达必定程度的时候一样会发生down机的状况。相似于数据库的主从架构,缓存系统也能够复制多分缓存副本到其余服务器上,这样就能够将应用的请求分散到多个缓存服务器上,缓解因为热点数据出现的单点问题。
和数据库主从同样,缓存的多个副本也面临着数据的一致性问题,同步延迟问题,还有主从服务器相同key的过时时间问题。
至于缓存系统的扩展性一样的道理,也能够利用“分片”的原则,利用一致性哈希算法将不一样的请求路由到不一样的缓存服务器节点,来达到水平扩展的要求,这一点和应用的水平扩展道理同样。
经过以上能够看出,不管是应用服务器的高可用架构仍是数据库的高可用架构,仍是缓存的高可用其实道理都是相似的,当咱们掌握了其中一种就很容易的扩展到任何场景中。若是这篇文章对你有多帮助,请分享给身边的朋友,最后欢迎你们留言写下大家在平常开发中用到的其余关于缓存高可用,可扩展性,以及防止穿透和雪崩的方案,让咱们一块儿进步!!
更多精彩文章