16.深入分布式缓存:从原理到实践 --- 新的旅程

1.更好的引入缓存技术
	缓存不是系统架构的必选项,只有在遇到性能瓶颈的业务场景,才可能需要引入缓存。引入缓存时,要从'大处着眼,小处着手',即首先要从宏观考虑整体的缓存场景,
  缓存层级及缓存(同步/更新)策略,然后从局部考虑选择合适的缓存组件及使用方式(数据结构,分布,部署),并制定缓存系统的SLA,最后在系统运行过程中,要对缓存
  系统进行监控报警,还要根据业务发展,访问规模的变化,不断的对缓存架构进行优化和演进。

	1.缓存引入前的考量
		引入缓存前,首先要分析业务的应用规模,访问量级。对规模不大的小系统或系统发展初期,数据量和访问量不大,引入缓存并不能带来显著的性能提升,反而会
	  增加开发,运维的复杂性。另外对于单条数据尺寸比较大的业务,如图片系统,虽然访问量可能比较大,但更佳的选择可能是分布式文件系统而非缓存。而对于具体
	  有一定规模的常规业务系统,数据规模,访问量较大,且持续快速增加,要保证服务的稳定性,在突发流量下也能快速响应用户需求,就需要把频繁读写的数据从
	  磁盘访问变成内存访问,也就是引入缓存。
	  	其次还要分析系统架构,在系统架构中合适的模块,层次引入缓存,并考虑各个缓存系统之间的更新方式,一致性保障策略等,让缓存系统和其他架构层(如持久层,
	  分布式文件层)有机结合。

	2.缓存组件的选择
		缓存的种类很多,实际使用的时候,需要根据缓存的位置(系统前后端),待存数据类型,访问方式,内心效率等情况来选择最合适的缓存组件。
		一般业务系统中,大部分数据都是简单的kv数据类型,如微博的feed content,feed 列表等。这些简单的信息只需要进行set,get,delete操作,不需要在缓存端
	  做计算操作,最合适用memcached作为缓存组件。
	  	其次对于需要部分获取,事务型变更,缓存端计算的集合类操作,拥有丰富数据结构和访问接口的redis也许会更合适。redis还支持以主从方式进行数据备份,支持
	  数据持久化,可以将内存中的数据保存在磁盘,重启时再次加载使用。因磁盘缓存方式的性能问题,redis数据基本只适合保存在内存中,由此带来的问题是:在某些业务
	  场景,如果待缓存的数据量特别大,而数据的访问量不太大或者冷热区分,也必须将所有的数据都放在内存中,缓存成本(特别是机器成本)会特别高。如果业务遇到这种
	  场景,可以考虑用pika,ssdb等其他缓存组件

	3.缓存架构的设计
		确定缓存组件后,首先需要结合业务场景,设计业务数据对应的缓存结构及缓存容量规划。如微博在用memcached缓存feed content 时,最初采用json,xml格式进行
	  缓存,后来采用更加紧凑的PB(protocol buffer)结构;而对feed content的容量规划设计,则包含了content的平均size,缓存数据量,峰值读写qps,命中率,过期
	  时间,平均穿透加载时间等。
	  	其次要结合缓存组件特点,设计缓存的读写策略,分布策略,过期策略等。如在海量数据,大并发访问场景下使用redis时,要考虑如何在主从之间进行读写分离,要考虑
	  key的hash算法,分布策略,以使数据分散和请求访问更加平均,同时还要考虑如何让冷数据/过期数据更快从内存剔除(如主动删除冷数据,低峰scan清理过期数据),让更多
	  的热数据常驻内存,从而确保缓存有持续的高命中率。
	  	最后要从开发,运维角度,设计缓存的一致性,高可用性方案。如微博在memcached对feed vector进行更新时,如果该vector不存在则不更新,待有请求时再从持久层获取
	  最新数据,如果该vector存在,则通过Main层的cas,HA层/L1层的set来进行更新和一致性保证。又如微博对redis采用dns方式进行主从的访问及运维,如果是master故障,
	  运维系统可以根据策略快速选择新的master,并将其他所有slave指向新的master,并更新dns,确保redis缓存的快速恢复;而对于slave故障,则直接将其摘除slave DNS即可,
	  访问基本不受影响。

	4.缓存系统的监控及演进
		在系统运行过程中,需要对缓存系统进行实时监控报警,在缓存组件出现故障或无法满足需求时,进行修复及快速扩展。监控可以采用集中探测的方式,也可以采用分布式汇报的
	  方式,具体探测方案需要根据监控系统的特点进行确定。
	  	另外,随着业务发展和访问规模的变化,缓存架构也需要不断进行优化及演进。如在微博发展过程中,单个核心数据的访问量从万级增加到十万,百万级别,同时突发事件可能在
	  短时间带来30%以上的访问量,微博为此在memcached上先后引入 Main-HA,L1-Main-Ha多层结构。

2.缓存分类总结
	1.按宿主层次区分
		1.本地缓存/进程内缓存
			应用服务器本地的缓存模式,不如在一个JVM内。本地缓存又称为进程内缓存,进程内缓存直接访问进程所属内存,无需做进程间通信,速度是各种缓存方式中最高的。本地
		  缓存又可以分为堆内缓存和堆外缓存,堆内缓存对GC的影响比较大,堆外缓存又会增加额外的序列化和反序列化开销。

		2.进程间缓存
			如果进程内的缓存比较大,那么重启后,缓存就需要重新加载,导致系统启动过慢,特别在需要紧急重启的情况下,影响比较大。可以通过在本机单独启动一个进程专门放
		  缓存,通过domain socket通信。

		3.远程缓存
			远程缓存指的是需要跨服务器访问的缓存。一般缓存数据存放于单独的缓存服务器上。典型的如memcached,redis。

		4.二级缓存
			就是本地缓存+远程缓存的结合。互联网系统中,对于大量易变的数据,一般是散列到分布式存储的远程缓存,减少不必要的db层访问,来获得大幅的性能提升;而对于不易改变
		  但访问量巨大的数据,则可以进一步放置到本地缓存,来获得远比远程缓存高3~4个数量级的访问性能。

	2.按存储介质区分
		1.内存缓存
			内存缓存就是把数据驻留在内存中,支持高速并发访问,读写高效和简单,能满足大部分需求场景,但宕机或者服务器重启会导致数据丢失。
		2.持久化缓存
			持久化缓存则会把数据写入到磁盘,读写性能比较低,但容量更大,成本更低。随着ssd,Funsion-io 硬盘技术的日趋完善,以及基于这些硬盘技术的fatcache,flashcache
		  等开源软件的不断涌现,可以预计不久将来,持久化缓存将会得到更广泛的应用。

	3.按架构层次区分
		页面缓存,浏览器缓存,web服务器缓存,反向代理缓存,应用级缓存。

3.缓存知识结构更多Tips
	1.缓存使用模式
		缓存是解决性能问题,在具体的代码编写中,最好使用函数封装,把缓存和数据库的操作提炼为模块,避免出现散弹式代码。缓存与数据库的操作关系根据同步,异步以及操作顺序可以分为
	  下面几类:
	  	1.Cache-Aside
	  		Cache-Aside 就是业务代码中管理维护缓存。某些缓存中间件没有关联缓存和存储之间的逻辑,则只能由业务代码来完成了。读场景,先从缓存中获取数据,如果没有命中,则回源到
	  	  存储系统并将元数据放入缓存供下次使用。写场景,先将数据写入到存储系统,写入成功后同步将数据写入缓存。或者写入成功后将缓存数据过期,下次读取时再加载缓存。此模式下的优势
	  	  在于利用数据库比较成熟的高可用机制,数据库写成功,则进行缓存数据更新;如果缓存数据写失败,可以发起重试。

	  	2.Cache-As-SoR
	  		SoR是记录系统,就是实际尺寸原始数据的系统。Cache-As-SoR 顾名思义就是把 cache 当做 SoR,业务代码只对cache操作。而对SoR的访问在cache组件内部。传统来说,具体分为
	  	  Read-Through,Write-Through,Write-Behind三种实现。笔者进一步归纳了Refresh-Ahead模式。

	  	3.Read-Through
	  		在Read-Through模式下,当我们业务代码获取数据时,如果有返回,则先访问缓存;如果没有,则从数据库加载,然后放入缓存。Guava Cache即支持这种模式。

	  	4.Refresh-Ahead
	  		业务代码访问数据时,仅调用cache的get操作。通过设定数据的过期时间在数据过期时,自动从数据库重新加载数据的模式。此模式相较于 Read-Through 模式的好处是性能高,
	  	  坏处是可能获取到非数据库的最新数据。在此对数据精度有一定容忍性的场景合适使用。

	  	5.Write-Through
	  		Write-Through 被称为穿透写模式,业务代码首先调用cache写,实际由cache更新缓存数据和存储数据。

	  	6.Write-Behind
	  		在Write-Behind 模式下,业务只更新到缓存数据,什么时候更新到数据库,由缓存到数据库的同步策略决定、

	2.缓存协议
		缓存常见有redis和memcached协议。redis协议为RESP(redis serialization protocol)。memcached协议主要分为2种:文本(classic ASCII)和二进制(binary)协议,
	  一般客户端均支持文本协议和二进制协议,默认是文本协议。如果选择二进制协议,采用中间件,还需要考虑中间件是否能够支持二进制协议。
	  	1.Redis协议
	  		redis通过CRLF(\r\n)进行拆包。主要包含两部分:类型和data,类型为标识data的数据格式。redis主要协议实现有pipeline,事务,pub/sub,cluster等。
	  		1.pipeline : 将多个请求合并成一个请求进行发送,减少网络请求。在请求数据包较少并且请求次数较多的情况下,pipeline可能有效提升性能;如果请求包长度大于MTU
	  		一般为1500byte,由于拆包,并不能有效提升性能。

	  		2.事务 : 和pipeline的相同点是可以进行网络请求合并。不同点是事务可以保证操作的原子性;事务是执行了exec才会把所有的请求结果一起返回。

	  		3.pub/sub : 实际上是hold住长连接,redis端会主动将消息推送到监听的客户端。

	  		4.cluster : 服务端通过gossip协议实现分布式。集群关系由服务端维护,通过moved或者ack协议进行重定向来通知客户端访问到正确的数据节点。

	  	2.Memcached协议
	  		memcached的协议有文本协议和binary协议。其中,文本协议和redis比较类似,也是通过CRLF(\r\n)来进行拆包。由于memcached存储结构简单,只有很少的命令。
	  		binary协议简单高效,支持更多特性,扩展性更强。比如Opaque,Memcached收到该字段后,再返回到客户端。这样客户端就有能力识别返回的数据是由哪个请求发送的(实现连接
	  	  的多路复用)。

	3.缓存连接池
		连接池是将连接进行复用,提升访问性能。缓存对性能要求较高,连接池的合理更加重要。连接池的实现主要依赖于集群方式和底层IO机制。集群方式如:单点,sharding和cluster。
	  底层IO比如BIO,NIO。常见客户端:
	  	1.Jedis客户端
	  		1.单点连接池
	  			连接池里面放置的是空闲连接,如果被使用掉,连接池就会少一个,连接完后返回连接池。如果没有可用的连接,便会新建连接。

	  		2.sharding连接池
	  			比如有2个redis服务进程(redis1,redis2),对key按照sharding策略选择访问哪一个redis。相较于单点连接池,sharding连接池里面的连接为redis1何redis2两个
	  		  连接。每次申请使用一个连接,实际上是拿到两个不同的连接,然后通过sharding具体选择访问哪一个redis。该方案的缺点是造成连接浪费,比如实际访问redis1,但是实际上
	  		  也占用redis2的连接。

	  		3.cluster连接池
	  			客户端启动的时候,会从某一个redis服务上面,获取到后端cluster集群上面所有的redis服务列表(比如redis1,redis2),并且对每一个redis服务建立独立的连接池。
	  		  如果访问后端redis服务,会先通过crc16计算访问的key确定slot,再通过slot选择对应的连接池(比如redis1的pool),再从对应的连接池里面获取连接,访问后端服务。
	  	
	  	2.Spymemcached客户端
	  		memcached客户端,IO为NIO的实现,在异步系统中可以极大提升系统的性能。客户端实现了连接的多路复用。一个连接可以多个请求同时复用,可以通过极少数的连接支持较高的
	  	  访问。对于mysql的连接,一个请求占用的连接是不能被其他请求使用的,一般需要建立大量的连接。
	  	  	spymemcached多路复用的机制是:key通过sharding策略选择对应的连接,每个连接有一个fifo队列,会将当前的请求封装成Task放置到FIFO队列上面,异步NIO线程进行
	  	  异步的发送与连接。多路复用机制强制依赖于在同一个连接上的请求必须顺序发送,顺序响应。

	4.几个关注点
		使用缓存组件时,需要关注集群组件方式,缓存统计;还需要考虑缓存开发语言对缓存的影响,如对Java开发的缓存需要考虑GC的影响;最后还要特别关注缓存命中率,理解缓存
	  命中率的因素,以及如何提高缓存命中率。
	  	1.集群组件方式
	  		集群方式主要有客户端sharding,proxy和服务集群三种方式:
	  		1.客户端sharding : key在客户端通过一致性hash进行sharding,该种方案服务端运维简单,但是需要客户端实现动态的扩缩容等机制。
	  		2.proxy : proxy实现对后端缓存服务的集群机制。Proxy同时也是一个集群。有2种方案管理proxy集群,通过lvs或者客户端实现。如果流量比较大,lvs也需要考虑进行
	  		集群管理。proxy方案运维复杂,扩展性较强,可以在proxy上面实现限流等扩展功能。
	  		3.服务端集群 : 主要是redis的cluster机制,基于Gossip协议实现。

	  	2.统计
	  		在实际应用中,需要不断改进缓存的功能和性能,需要对缓存进行关键数据的监控。对于进程外缓存,除监控进程内缓存的各种指标外,一般还需要监控cpu,进程内存使用情况,
	  	  连接数等。

	  	3.GC影响
	  		二级缓存存在热key或者大key等难以解决的问题,可以通过本地缓存来有效的解决。对于java的本地缓存,一般有堆内缓存(on-heap)和堆外缓存(off-heap)2种方案。堆内
	  	  缓存的空间由JVM分配及回收,由于缓存的数据量一般比较大,堆内缓存对GC的影响比较大。堆外缓存是直接在page cache中申请内存,生命周期不由JVM管理。

	  	  堆外缓存的优点:
	  	  	1.支持更大的内存
	  	  	2.减少gc的性能开销
	  	  	3.减轻FGC的压力及频率

	  	4.序列化
	  		访问缓存,需要进行序列化和反序列化。redis或memcached存储的都是字节类型的数据。如果需要存储数据到缓存中,需要先在本机进行序列化,转化为字节,通过网络传输,
	  	  缓存以字节的形式进行存储。如果需要读取数据,从缓存中读取的是字节形式的数据,需要进行反序列化,将字节转化为对象。
	  	  	访问本地缓存,对于jvm语言,有堆内和堆外缓存可以进行选择。由于堆内直接以对象的形式存储,不需要考虑序列化,而堆外是以字节类型存储,就需要进行序列化和反序列化。
	  	  堆外存储对gc的影响比较小,但是序列化和反序列化的开销却不能忽略。
	  	  	序列化性能主要考虑如下:
	  	  		1.序列化时间
	  	  			对于层次比较深的对象结构或字段比较多的对象,不同的序列化机制,序列化时间的开销也有比较大的差异
	  	  		2.序列化之后包的大小
	  	  			序列化后的包的大小越小,网络传输越快,同时后端缓存服务在一定的存储空间内,存储的对象也越多。序列化后,在数据量特别大的情况下一般会选择开启是否压缩,
	  	  		  开启压缩的目的是减少传输的包的大小。
	  	  		3.序列化消耗的cpu
	  	  			序列化后的数据,在获取的时候会进行反序列化,特别是批量获取数据的操作,反序列化带来的cpu消耗特别大。序列化一般需要解析对象的结构,而解析对象的结构,
	  	  		  会带来比较大的cpu消耗,所以一般序列化(比如fastJonn)均会缓存对象解析的对象结构,减少cpu的消耗。

	  	5.缓存命中率
	  		1.命中 : 直接获取想要的数据
	  		2.不命中 : 无法直接获取,需要再次查询数据库或者执行其他操作。

	  		1.如何监控缓存的命中率
	  			在memcached中,运行state命令可以查看服务的状态信息,其中 cmd_get 表示总的get次数,get_hits 表示get总的命中次数,命中率=get_hits/cmd_get
	  		  当然,我们也可以通过第三方开源工具,比如:zabbix,MemAdmin等。
	  		  	同理,在redis中可以运行info命令查看redis状态信息,其中 keyspace_hits为总的命中次数,keyspace_misses为总的miss次数,
	  		  命中率=keyspace_hits/(keyspace_hits+keyspace_missed)。开源工具 Redis-Stat,zabbix。

	  		2.影响缓存命中率的几个因素
	  			1.业务场景和业务需求
	  				缓存适合'读多写少'的业务场景,反之,使用缓存的意义不是很大,命中率会很低。业务需求决定了对时效性的要求,直接影响到缓存的过期时间和更新策略。
	  			  时效性要越低,就越适合缓存。在相同key和相同的请求次数情况下,缓存时间越长,命中率会越高。
	  			2.缓存的设计
	  				通常情况下,缓存的粒度越小,命中率会越高。
	  			3.缓存容量和基础设施
	  				缓存的容量有限,容易引起缓存失效和被淘汰。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存
	  			  则容易扩展。所以需要做好缓存容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。
	  			4.其他因素
	  				当缓存节点发生故障的时候,需要避免缓存失效并最大程序降低影响,这种情况也是需要考虑的。通常是通过一致性hash算法,或者节点冗余。

	  			可能会有这样的误区:既然业务需求堆数据时效性要求很高,缓存的时间比较短,容易失效,缓存命中率无法有效保证,那么系统就别使用缓存了。其实忽略了一个很
	  		  重要的因素---并发。通常来说,在相同缓存时间和key的情况下,并发越高,缓存的收益会越高,即便缓存时间很短。

	  		3.提高缓存命中率的方法
	  			应用尽可能的通过缓存直接获取数据,并避免缓存失效。这需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问
	  		  且时效性要求不高的热点业务上,通过缓存预加热,增加存储容量,调整缓存粒度,更新缓存等手段来提高缓存命中率。

	5.缓存管理
		在使用缓存的过程中,如果缓存数据没有命中就会存在缓存穿透,如果缓存穿透率比较高,我们需要分析穿透的原因并解决,选择不同的缓存策略,缓存淘汰算法,避免攻击性穿透,
	  并发更新穿透等情况,同时让缓存数据的失效过程尽可能的平滑。

		1.缓存穿透
			我们在项目中使用缓存通常是先检查缓存是否存在,如果存在直接返回,不存在回源查询。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都回源。
		  这样缓存就失去了意义,流量大的时候,回源系统的压力就会很大。如果有人利用不存在的key一直攻击我们的应用,那就是个漏洞。有一个比较巧妙的方法,可以将这个不存在的key
		  预先设置一个值,比如key和'&&'。在返回这个&&值的时候,我们的应用就认为这是个不存在的key,应用就可以决定是否需要回源。
		  	缓存穿透的第二个场景:网站并发访问高,一个缓存如果失败,可能出现多个进程同时查db,同时设置缓存的情况,如果并发确实很大,这也可能造成回源系统压力大。方案是通过
		  对缓存查询加锁,如果key不存在,就加锁,然后回源,将结果进行缓存,然后解锁;其他进程如果发现有所就等待,然后等解锁后返回数据或者回源查询。这种情况和刚刚说的预先
		  设置值的问题有些类似,只不过利用锁的方式,会造成部分请求等待。
		  	进一步方案:双key,主key生成一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。
		  这种方案的缺点是会产生双份数据,而且需要同时控制附属key和key之间的关系,操作上有一定的复杂度。

		  	mutex解决方案:
		  	1.热点key过期,则增加key_mutex
		  	2.从数据库中 load key 的数据放入缓存
		  	3.添加成功,则删除 key_mutex
		  	4.返回key的值给上层应用
		  	可以有几个思考的:这段逻辑放在cache client,还是cache server?若放到cache client,则可能有上百台机器在访问,增加key_mutex的逻辑无法跨集群加锁,如果
		  在cache server就简单了,但是需要考虑如何把代码plugin in 到 cache server .
		  	第二个思考:如果增加key_mutex,是否要sleep和retry,sleep多少比较好。都需要实践。

		2.缓存失效
			引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能会设置1,5分钟,并发很高可能会出现在某一时间同时生成很多的缓存,并且过期时间
		  一样,这个时候就可能引发过期时间到后,缓存同时失效,请求全部转发到db。
		  	其中一个解决办法是将 缓存失效时间分散开,比如我们可以在原有的失效时间基础上加上一个随机值,比如1~5分钟,这样每一个缓存过期时间的重复率就会降低,就很难引发
		  集体失效的事件。
		  	上述是缓存使用过程中经常遇到的并发穿透,并发失效问题。一般情况下,采用引入空值,锁和随机缓存过期时间的机制。

		3.淘汰算法
			1.LRU
			2.LFU
			3.FIFO

			LRU(least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是'如果数据最近被访问过,那么将来被访问的概率也很高'。算法实现
		  如下:	
		  	1.新数据插入到链表头部
		  	2.每当缓存命中,则将数据移到链表头部
		  	3.当链表满的时候,将链表尾部的数据丢弃
		  	这样的实现固然简单,但也有缺点:当存在热点数据的时候,lru的效率很好,但偶发性的,周期性的批量操作会导致lru命中率急剧下降,缓存污染情况比较严重。
		  	
		  	另外一个比较科学的是LUR-K。LUR-K中的k代表最近使用的次数,因此lru可以认为是lru-1。lru-k的主要目的是为了解决lru算法'缓存污染'的问题,其核心思想是'最近使用
		  过1次'的标准扩展为'最近使用过k次'。相比lur,lur-k需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到k次的时候,才将数据放入缓存。当
		  需要淘汰数据时,lru-k会淘汰第k次访问时时间距当前时间最大的数据。具体算法:
		  	1.数据第一次访问的时候,加入到历史访问列表
		  	2.如果数据在访问历史列表里后没有达到k次访问,则按照一定的规则(FIFO,LRU)淘汰
		  	3.当访问历史队列中的数据访问次数达到k次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序
		  	4.缓存数据队列被再次访问,重新排序
		  	5.需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰'倒数第k次访问离现在最久'的数据
		  	lru-k具有lru的优点,同时能够避免lru的缺点,实际应用中lru-2是综合各种因素后最优的选择,lru-3或者更大的k值命中率会更高,但适应性差,需要大量的数据访问才能
		  将历史访问记录清楚掉。lur-k虽然降低了'缓存污染'带来的问题而且命中率比lru更高,但是也有一定的代价,由于lru-k还需要记录那些被访问过的,但还没有放入缓存的对象,
		  因此内存消耗比较高;当数据量大的时候,内存消耗会比较客观。lur-k需要基于时间排序(可以需要淘汰时再排序,也可以即时排序),cpu消耗比lru要高。

	6.缓存可用性
		1.主备方案
			缓存的主要目标是提升性能,理论上缓存数据的丢失或者缓存不可用,不会影响数据正确性,数据库里面还有数据。但是由于缓存的存在,抵挡住了大部分访问,如果缓存服务器挂
		  了,则存在大部分的请求穿透到数据库,这对数据库访问是灾难性的。

		2.集群方案
			一个redis cluster由多个cluster节点组成。不同节点组服务的数据无交集,即每一个节点组对应数据的一个分片。节点组内部分分为主备两类节点,对应前述的master和slave
		  节点,两者数据准实时一致,通过异步化的主备复制机制保障。一个节点组有且仅有一个master节点,同时有0到多个slave节点。只有master节点对用户提供写服务,读服务可以由
		  master或者slave提供。在master节点异常时,还可以从slave节点中选择一个节点晋升为新的master节点。

	7.数据一致性
		这里的一致性包括缓存数据与数据库数据的一致性,包括多级缓存数据之间的一致性。大部分场景下,追求最终一致性。

		1.最终一致性
			大部分情况下对于缓存与数据库的一致性没有绝对强一致要求,那么在写缓存失败的情况下,可以通过补偿动作进行,达到最终一致性。
			Facebook如何做的:通过更新数据库之后,把需要删除的key给到McSqueal,然后异步删除缓存数据的模式。这样下一次get请求时,如果没有数据,则从数据库里查询同时更新
		  到Memcached集群。
		  	对于时间敏感的数据可以设置很短的过期时间,这样一旦超过失效时间,就可以从数据库重新加载。

		  	保持最终一致性的方法有很多,如,京东采用了通过canal更新缓存原子性的方法,几个关注点:
		  	1.更新数据时使用更新时间戳或者版本对比,如果使用redis可以利用其单线程机制进行原子化更新;
		  	2.使用如canal订阅数据库binlog,此处把mysql看成发布者,binlog是发布的内容,canal看成消费者,canal订阅binlog然后更新到redis。
		  	3.将更新请求按照相应的规则分散到多个队列,然后每个队列进行单线程更新,更新时拉去最新的数据保存;更新之前获取相关的锁再进行更新。

		2.强一致性
			可以使用InnoDB memcached 插件结合mysql来解决缓存数据与数据库一致性问题。

	8.热点数据处理
		设计缓存时,使用sharding或cluster模式,来将不同的key,sharding到不同的机器上面,避免所有的请求访问到同一台机器。但对于同一个key的访问都是在同一个缓存服务,如果
	  出现热key,很容易出现性能瓶颈。一般的解决方案是:主从,热与和本地缓存。

	  	1.数据预热
	  		提供把数据读入缓存的做法就是数据预热处理。需要注意一些问题:
	  			1.是否有监控机制确保预热数据都写成功了
	  			2.数据预热配备回滚方案,遇到紧急问题的时候便于操作。对于新建的cache server集群,也可以通过数据预热模式来做一番手脚。先从冷集群中获取key,如果获取不到,再从
	  		  热集群中获取。同时把获取的key put到冷集群。
	  		  	3.预热数据量的考量,要做好容量评估。在容量允许的范围内预热全量,否则预热访问量最高的
	  		  	4.预热过程中需要注意是否因为批量数据库操作或慢sql等引发数据库性能问题

	  	2.非预期热点策略
	  		对于非预期热点问题,一般建立实时热点发现系统来发现热点。
	  		无论是京东通过nginx+lua,来做应用内缓存,或者是别的local cache 方案;亦无论分布式缓存tair还是redis,对于实时热点发现系统的实现策略大同小异。同时实时统计访问
	  	  分布式热点的key,把对应的key推到本地缓存中,满足离用户最近的原则。当然,使用本地缓存,业务上要容忍本地缓存和分布式缓存的非完全一致。比如秒杀场景,查看详情浏览的
	  	  库存数量,而最终是以成功下单的为准。

	  	3.多级缓存模式
	  		类似于秒杀场景,一旦某个热点触发了一台机器的阈值,那么这台机器cache的数据都将无效,进而间接导致cache被击穿,请求落地应用层数据库出现崩溃现象。这类问题需要与
	  	  具体cache产品结合才能有比较好的解决方案,一个通用的思路是cache在client端做本地cache,当发现热点数据时直接cache在client里,而不要请求到cache的server。
	  	  	以京东的解决方案为例,对于分布式缓存,需要在nginx+lua应用中进行应用缓存来减少redis集群的访问冲击。即首先查询应用本地缓存,如果命中则直接返回缓存,如果没有
	  	  命中则接着查询redis集群,回源到tomcat,然后将数据缓存到应用本地。
	  	  	应用nginx的负载机制采用:正常情况下采用一致性哈希,如果某个请求类型的访问突破了一定的阈值,则自动降级为轮询机制。另外对于一些秒杀之类的热点我们是可以提前知道
	  	  的,可以把相关的数据推送到应用nginx并将负载均衡机制降级为轮询。

	  	4.数据复制模式
	  		在facebook有一招,就是通过多个key_index(key:xxx#N)来解决数据热点读问题。解决方案是所有热点key发布到所有的web服务器;每个服务器的key有对应别名,可以通过
	  	  client端的算法路由到某台机器;做删除动作的时候,删除所有的别名key。可以简单总结为一个通用的group内一致模型。把缓存集群划分为若干分组(group),在同组内,所有
	  	  的缓存服务器,都发布热点key的数据。
	  	  	对于大量读操作而言,通过client端路由策略,随意返回一台机器即可;而写操作,有一种解决是通过定时任务来写入,facebook差异的是删除所有别名key的策略。如果保障
	  	  这一个批量操作都成功?
	  	  	1.容忍部分失败导致的数据版本问题
	  	  	2.只要写操作,则通过定时任务刷新缓存;如果涉及3台服务器,则都操作成功代表该任务表的这条记录成功完成使命,否则重试。

	9.注意事项Tips
		在缓存的使用中,存在一些误用缓存的例子。比如把缓存当做存储来使用;在db负载很低的情况下,为了使用缓存而使用缓存。

		1.慎把缓存当存储
			在关键链路中,如果无法容忍缓存不可用带来的致命危急,那么还是应该把缓存仅作为提升性能的手段,如果缓存不可用,可以访问数据库兜底。
		2.缓存就近原则
			缓存所有的策略是优先访问离自己最近的数据。
			1.CPU缓存 
				离cpu越近,访问速度最快。
			2.客户端缓存
				客户端缓存又称本地缓存,本地缓存能比远程缓存获得较高的性能,本地缓存使用过程中如果数据不仅仅是读,还有写,那么要解决写数据同步给集群其他节点的本地缓存
			  问题。
			3.CDN缓存
				用户端优化的常见手段便是cdn,静态资源一般通过cdn缓存提升访问性能。其基本思路是避免互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输更快,
			  更稳定。cdn系统能够实时的根据网络流量和各个节点的连接,负载情况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
		3.并发控制手段
			保证并发控制的一些常用手段有:乐观锁,Latch,mutex,CAS等;多版本并发控制MVCC通常是保证一致性的重要手段。Latch 是处理数据库内部机制的一种策略。

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值