互联网时代,怎么让系统在狂轰乱炸甚至泰山压顶的情况下,都屹立不倒?这是个缓存为王的时代,几乎接触到所有的软件,都离不开它。怎么样去合理的使用缓存,首先要认识并了解它。
1.浏览器缓存
If-Modified-Since、Last-Modified/Etag、Cache-Control/Expires
Html5: manifest
2.CDN缓存
3.Web代理缓存
4.MySql Query Cache
百万级QPS的资源调用 (高并发)
99.99%的可用性 (高可用)
毫秒级的核心请求响应时间 (高性能)
•设计这样的系统,不可避免的要考虑使用分布式缓存,并从可用性、并发性、性能多个方面进行综合考量。
一般来说从两个方面来个是否需要使用缓存:
CPU占用:
如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达. 式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。
数据库IO占用:
如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。
缓存就是数据交换的缓冲区.
命中率 = 缓存中读取次数 / 总读取次数
就近原则
80%的访问量都集中在20%的热数据上(适用二八原则)。将高频访问的数据,放入缓存中,
可以大大提高系统整体的承载能力
根据缓存在软件系统中所处位置的不同,大体可以分为三类:
Ø客户端侧的缓存
页面缓存和浏览器缓存
Ø服务器侧的缓存
服务器本地缓存、分布式缓存、数据库缓存
Ø网络中的缓存
WEB代理缓存和边缘缓存(CDN边缘缓存)
根据缓存的规模和部署方式也可以分为:
Ø单体缓存
Ehcache. Guava Cache
Ø缓存集群
Ø分布式缓存
memcached缓存. Redis缓存
原始数据
结果数据
提前加载Proactive Load
动态加载Reactive Load——或称反应式加载
数据冗余
自动故障转移
高可用性一般又是通过冗余也就是多副本来解决,多副本接着又带来了一致性问题。
所以分布式系统要解决的问题可简单归结为多副本的一致性问题。
怎么解决一致性问题呢?抢答:用事务。何为事务?多个操作序列的原子性。
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
paxos算法
当决定是否缓存一个对象时,关于数据的格式和访问机制,你需要考虑三个主要问题
线程安全——当缓存的内容可以被多个线程访问时,使用某种锁定机制来保证数据不会被两个线程同时操作;
序列化——将一个对象缓存时,需要将它序列化以便保存,所以包缓存的对象必须支持序列化;
存数据、读数据涉及到序列化与反序列化,序列化与反序列化的过程是非常消耗CPU的操作,很多问题就出现在这上面,特别是当我们缓存了比较复杂的数据对象的时候。
规格化缓存数据——缓存数据时,相对于要使用的数据格式而言,要保证数据的格式是优化过的。
FIFO(first in first out)
缓存空间不够/超出最大元素限制的情况下,先进先出淘汰数据。
主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
LFU(less frequently used)
无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。
主要比较元素的hitCount。在保证高频数据有效性场景下,可选择这类策略
LRU(least recently used)
无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。
主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
缓存穿透
缓存击穿
缓存雪崩
缓存穿透
缓存穿透是指查询的数据,在缓存中查不到就会去DB取查询,DB中也不存在。高并发情况下,会明显增大DB压力。
解决方案:
1.对于返回为NULL的依然缓存。
采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,或设置较短的超时时间来解决这个问题。
2.制定一些规则过滤一些不可能存在的数据。
小数据用BitMap,大数据可以用布隆过滤器,比如你的订单ID 明显是在一个范围1-1000,如果不是1-1000之内的数据那其实可以直接给过滤掉。
缓存击穿
对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去DB访问,此时DB访问量会急剧增加 。
解决方案:
1.加分布式锁:加载数据的时候,可以利用分布式锁锁住这个数据的Key。对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样DB不会同时受到很多线程访问同一条数据。
2. 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。
缓存雪崩
缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问DB,DB压力过大导致系统雪崩。
为了避免这个问题,我们采取下面的手段:
1. 增加缓存系统可用性,做好灾备;通过监控关注缓存的健康程度,根据业务量适当的扩容缓存。
2. 采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。
3. 缓存的过期时间可以取个随机值,比如以前是设置10分钟的超时时间,那每个Key都可以随机8-13分钟过期,尽量让不同Key的过期时间不同。
4. 降级和流控
提前预知风险点,做好准备,即使出现问题,也便于更好的流控;故障期间通过降级非核心功能来保证核心功能可用性;通过拒掉部分请求保证有部分请求还能正常响应
静态变量一次获取缓存内存中,减少频繁的I/O读取
静态变量实现类间可共享,进程内可共享,缓存的实时性稍差
为了解决本地缓存数据的实时性问题,目前大量使用的是结合ZooKeeper的自动发现机制,
实时变更本地静态变量缓存。
1. 缓存数据有两级:内存和磁盘
2. 提供了三种缓存清空策略:
FIFO:先进先出
LRU:最近最少使用
LFU:最近不经常使用
EHcahe分布式集群:
1. RMI组播方式
2. p2p方式
3. JMS消息模式
4. Terracotta(是一款由美国Terracotta公司开发的著名开源Java集群平台,它在JVM与Java应用之间实现了一个专门处理集群功能的抽象层,允许用户在不改变现有系统代码的情况下实现单机Java应用向集群话应用的无缝迁移)
缓存回收:
基于容量的回收(size-based eviction)
定时回收(Timed Eviction)
基于引用的回收(Reference-based Eviction)
Redis是基于内存、可持久化的日志型、Key-Value数据库 高性能存储系统
1. Redis Cluster,节点之间通过去中心化的方式,提供了完整的sharding、replication.
A和B为master节点,对外提供写服务。分别负责1/2/3和4/5的slot。A/A1和B/B1/B2之间通过主备复制的方式同步数据
5个节点,两两通过Redis Cluster Bus交互,相互交换如下的信息:
•数据分片(slot)和节点的对应关系;
•集群中每个节点可用状态;
2. 集群元数据维护
redis cluster节点间采取gossip协议进行通信
维护集群的元数据有两种方式:集中式和gossip
集中式:
优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;
不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。
gossip:
优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。
Gossip 协议
1)扩展性
网络可以允许节点的任意增加和减少,新增加的节点的状态最终会与其他节点一致。
2)容错
网络中任何节点的宕机和重启都不会影响 Gossip 消息的传播,Gossip 协议具有天然的分布式系统容错特性。
3)去中心化
Gossip 协议不要求任何中心节点,所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,任意一个节点就可以把消息散播到全网。
4)一致性收敛
Gossip 协议中的消息会以一传十、十传百一样的指数级速度在网络中快速传播,因此系统状态的不一致可以在很快的时间内收敛到一致。消息传播速度达到了 logN。
5)简单
Gossip 协议的过程极其简单,实现起来几乎没有太多复杂性。
3.Redis一致性的达成
当Cluster 结构不发生变化时,各个节点通过gossip 协议在几轮交互之后,便可以得知Cluster的结构信息,达到一致性的状态。但是当集群结构发生变化时(故障转移/分片迁移等),优先得知变更的节点通过Epoch变量,将自己的最新信息扩散到Cluster,并最终达到一致。
更新规则如下:
1. 当某个节点率先知道了变更时,将自身的currentEpoch 自增,并使之成为集群中的最大值。再用自增后的currentEpoch 作为新的Epoch 版本;
2. 当某个节点收到了比自己大的currentEpoch时,更新自己的currentEpoch;
3. 当收到的Redis Cluster Bus 消息中的某个节点的Epoch > 自身的时,将更新自身的内容;
4. 当Redis Cluster Bus 消息中,包含了自己没有的节点时,将其加入到自身的配置中。
4.Redis集群设计
Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。其结构设计:
•节点负责存储数据,记录集群状态,集群节点能自动发现其他节点,检测出节点的状态,并在需要时剔除故障节点,提升新的主节点
•所有节点通过PING-PONG机制彼此互联,使用一个二级制协议(Cluster Bus) 进行通信,优化传输速度和带宽。发现新的节点、发送PING包、特定情况下发送集群消息,集群连接能够发布与订阅消息。
•集群的节点不可用后,在经过集群半数以上Master节点与故障节点通信超过cluster-node-timeout时间后,认为该节点故障,从而集群根据自动故障机制,将从节点提升为主节点。这时集群恢复可用
Redis cluster 数据分片
数据分片(Sharding)引入哈希槽(hash slot)来实现:一个 Redis Cluster包含16384(0~16383)个哈希槽.
所有键都会被映射到这些slot中,使用公式slotId=CRC16(key)/16384。
集群中的每个主节点(Master)都负责处理16384个哈希槽中的一部分,当集群处于稳定状态时,每个哈希槽都只由一个主节点进行处理,每个主节点可以有一个到N个从节点(Slave),当主节点出现宕机或网络断线等不可用时,从节点能自动提升为主节点进行处理。
Redis cluster 数据分片
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383
新增一个主节点:redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。
5.Redis主从模式
redis cluster 为了保证数据的高可用性,加入了主从模式.
采用异步复制;
一个主redis可以含有多个从redis;
主从复制对于主redis服务器来说是非阻塞的;
主从复制对于从redis服务器来说也是非阻塞的;这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;
6.Redis数据持久化
Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案
Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据
采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。
7.Redis客户端分片
通过业务代码自己实现路由
优势:可以自己控制分片算法、性能比代理的好
劣势:维护成本高、扩容/缩容 等运维操作都需要自己研发
第一种:传统的数据分布方法,将key的hash值对机器数取模
第二种:一致性hash
第三种:tair负载均衡算法,构造一张对照表
缓存-一致性hash
整体分了三部分缓存:
应用Nginx本地缓存、分布式缓存、Tomcat堆缓存
每一层缓存都用来解决相关的问题,如应用Nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少访问回源率、Tomcat堆缓存用于防止相关缓存失效/崩溃之后的冲击。
1)首先接入Nginx将请求负载均衡到应用Nginx,
2)接着应用Nginx读取本地缓存(本地缓存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盘/内存)、Local Redis实现)
3)如果Nginx本地缓存没命中,则会读取相应的分布式缓存。
4)如果分布式缓存也没有命中,则会回源到Tomcat集群。
5)在Tomcat应用中,首先读取本地堆缓存,如果有则直接返回。
6)作为可选部分,如果步骤4没有命中可以再尝试一次读主Redis集群操作。目的是防止当从有问题时的流量冲击。
7)如果所有缓存都没有命中只能查询DB。
欢迎关注公众号:“架构一线”,定期分享一些实战心得,互联网前沿技术等.