1:缓存技术和框架的重要性mysql
互联网的一些高并发,高性能的项目和系统中,缓存技术是起着功不可没的做用。缓存不只仅是key-value的简单存取,它在具体的业务场景中,仍是很复杂的,须要很强的架构设计能力。我曾经就遇到过由于缓存架构设计不到位,致使了系统崩溃的案例。redis
2:缓存的技术方案分类算法
1)是作实时性比较高的那块数据,好比说库存,销量之类的这种数据,咱们采起的实时的缓存+数据库双写的技术方案,双写一致性保障的方案。sql
2)是作实时性要求不高的数据,好比说商品的基本信息,等等,咱们采起的是三级缓存架构的技术方案,就是说由一个专门的数据生产的服务,去获取整个商品详情页须要的各类数据,通过处理后,将数据放入各级缓存中。数据库
3:高并发以及高可用的复杂系统中的缓存架构都有哪些东西缓存
1)在大型的缓存架构中,redis是最最基础的一层。高并发,缓存架构中除了redis,还有其余的组成部分,可是redis相当重要。安全
若是你的数据量不大(10G之内),单master就能够。redis持久化+备份方案+容灾方案+replication(主从+读写分离)+sentinal(哨兵集群,3个节点,高可用性)网络
若是你的数据量很大(1T+),采用redis cluster。多master分布式存储数据,水平扩容,自动进行master -> slave的主备切换。架构
2)最经典的缓存+数据库读写的模式,cache aside pattern。读的时候,先读缓存,缓存没有的话,那么就读数据库。更新缓存分如下两种方式:并发
数据发生变化时,先更新缓存,而后再更新数据库。这种适用于缓存的值相对简单,和数据库的值一一对应,这样更新比较快。
数据发生变化时,先删除缓存,而后再更新数据库,读数据的时候再设置缓存。这种适用于缓存的值比较复杂的场景。好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值的。这样更新缓存的代价是很高的。若是你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存代价很高。并且这个缓存的值若是不是被频繁访问,就得不偿失了。
大部分状况下,建议适用删除更新的方式。其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。
举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次; 可是这个缓存在1分钟内就被读取了1次,有大量的冷数据。28黄金法则,20%的数据,占用了80%的访问量。实际上,若是你只是删除缓存的话,那么1分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。每次数据过来,就只是删除缓存,而后修改数据库,若是这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被从新计算的。
3)数据库与缓存双写不一致问题的解决方案
问题:并发请求的时候,数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。另外一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。
方案:数据库与缓存更新与读取操做进行异步串行化。(引入队列)
更新数据的时候,将相应操做发送到一个jvm内部的队列中。读取数据的时候,若是发现数据不在缓存中,那么将从新读取数据的操做也发送到同一个jvm内部的队列中。队列消费者串行拿到对应的操做,而后一条一条的执行。这样的话,一个数据变动的操做,先执行删除缓存,而后再去更新数据库,可是还没完成更新。此时若是一个读请求过来,读到了空的缓存,那么能够先将缓存更新的请求发送到队列中,此时会在队列中积压,而后同步等待缓存更新完成。
这里有两个能够优化的点:
一个队列中,其实多个读缓存,更新缓存的请求串在一块儿是没意义的,并且若是读同一缓存的大量请求到来时,会依次进入队列等待,这样会致使队列最后一个的请求响应时间超时。所以能够作过滤,若是发现队列中已经有一个读缓存,更新缓存的请求了,那么就不用再放个新请求操做进去了,直接等待前面的更新操做请求完成便可。若是请求还在等待时间范围内,不断轮询发现能够取到值了,那么就直接返回; 若是请求等待的时间超过必定时长,那么这一次直接从数据库中读取当前的旧值。
若是请求量特别大的时候,能够用多个队列,每一个队列对应一个线程。每一个请求来时能够根据请求的标识id进行hash路由进入到不一样的队列。
最后,必定要作根据实际业务系统的运行状况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操做,可能会致使最后一个更新操做对应的读请求,会hang多少时间,若是读请求在200ms返回,若是你计算事后,哪怕是最繁忙的时候,积压10个更新操做,最多等待200ms,那还能够的。若是一个内存队列可能积压的更新操做特别多,那么你就要加机器,让每一个机器上部署的服务实例处理更少的数据,那么每一个内存队列中积压的更新操做就会越少。其实根据以前的项目经验,通常来讲数据的写频率是很低的,所以实际上正常来讲,在队列中积压的更新操做应该是不多的。
举个例子:一秒就100个写操做。单台机器,20个内存队列,每一个内存队列,可能就积压5个写操做,每一个写操做性能测试后,通常在20ms左右就完成,那么针对每一个内存队列中的数据的读请求,也就最多hang一下子,200ms之内确定能返回了。若是把写QPS扩大10倍,可是通过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每一个机器20个队列,200个队列。大部分的状况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的,少许状况下,可能遇到读跟数据更新冲突的状况,如上所述,那么此时更新操做若是先入队列,以后可能会瞬间来了对这个数据大量的读请求,可是由于作了去重的优化,因此也就一个更新缓存的操做跟在它后面。
4)大型缓存全量更新问题的解决方案
问题:缓存数据很大时,可能致使redis的吞吐量就会急剧降低,网络耗费的资源大。若是不维度化,就致使多个维度的数据混合在一个缓存value中。并且不一样维度的数据,可能更新的频率都大不同。拿商品详情页来讲,若是如今只是将1000个商品的分类批量调整了一下,可是若是商品分类的数据和商品自己的数据混杂在一块儿。那么可能致使须要将包括商品在内的大缓存value取出来,进行更新,再写回去,就会很坑爹,耗费大量的资源,redis压力也很大
方案:缓存维度化。举个例子:商品详情页分三个维度:商品维度,商品分类维度,商品店铺维度。将每一个维度的数据都存一份,好比说商品维度的数据存一份,商品分类的数据存一份,商品店铺的数据存一份。那么在不一样的维度数据更新的时候,只要去更新对应的维度就能够了。大大减轻了redis的压力。
5)经过多级缓存,达到高并发极致,同时给缓存架构最后的安全保护层。具体能够参照上一篇文章【亿级流量的商品详情页架构分析】。
6)分布式并发缓存重建的冲突问题的解决方案
问题:假如数据在全部的缓存中都不存在了(LRU算法弄掉了),就须要从新查询数据写入缓存。对于分布式的重建缓存,在不一样的机器上,不一样的服务实例中,去作上面的事情,就会出现多个机器分布式重建去读取相同的数据,而后写入缓存中。
方案:分布式锁:若是你有多个机器在访问同一个共享资源,那么这个时候,若是你须要加个锁,让多个分布式的机器在访问共享资源的时候串行起来。分布式锁固然有不少种不一样的实现方案,redis分布式锁,zookeeper分布式锁。
zookeeper分布式锁的解决并发冲突的方案
(1)变动缓存重建以及空缓存请求重建,更新redis以前,都须要先获取对应商品id的分布式锁
(2)拿到分布式锁以后,须要根据时间版本去比较一下,若是本身的版本新于redis中的版本,那么就更新,不然就不更新
(3)若是拿不到分布式锁,那么就等待,不断轮询等待,直到本身获取到分布式的锁
7)缓存冷启动的问题的解决方案
问题:新系统第一次上线,此时在缓存里多是没有数据的。或者redis缓存全盘崩溃了,数据也丢了。致使全部请求打到了mysql。致使mysql直接挂掉。
方案:缓存预热。
提早给redis中灌入部分数据,再提供服务
确定不可能将全部数据都写入redis,由于数据量太大了,第一耗费的时间太长了,第二根本redis容纳不下全部的数据,须要根据当天的具体访问状况,实时统计出访问频率较高的热数据,而后将访问频率较高的热数据写入redis中,确定是热数据也比较多,咱们也得多个服务并行读取数据去写,并行的分布式的缓存预热。
8)恐怖的缓存雪崩问题的解决方案
问题:缓存服务大量的资源所有耗费在访问redis和源服务无果,最后本身被拖死,没法提供服务。
方案:相对来讲,考虑的比较完善的一套方案,分为事前,事中,过后三个层次去思考怎么来应对缓存雪崩的场景。
事前:高可用架构。主从架构,操做主节点,读写,数据同步到从节点,一旦主节点挂掉,从节点跟上。
事中:多级缓存。redis cluster已经完全崩溃了,缓存服务实例的ehcache的缓存还能起到做用。
过后:redis数据能够恢复,作了备份,redis数据备份和恢复,redis从新启动起来。
9)缓存穿透问题的解决方案
问题:缓存中没有这样的数据,数据库中也没有这样的数据。因为缓存是不命中时被动写的,而且出于容错考虑,若是从存储层查不到数据则不写入缓存,这将致使这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击咱们的应用,这就是漏洞。
方案:有不少种方法能够有效地解决缓存穿透问题,最多见的则是采用布隆过滤器,将全部可能存在的数据哈希到一个足够大的bitmap中,一个必定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(咱们采用的就是这种),若是一个查询返回的数据为空(不论是数 据不存在,仍是系统故障),咱们仍然把这个空结果进行缓存,但它的过时时间会很短,最长不超过五分钟。