一、引子html
通常而言,如今互联网应用(网站或App)的总体流程,以下图所示,用户请求从界面(浏览器或App界面)到网络转发、应用服务再到存储(数据库或文件系统),而后返回到界面呈现内容。前端
随着互联网的普及,内容信息愈来愈复杂,用户数和访问量愈来愈大,咱们的应用须要支撑更多的并发量,同时应用服务器和数据库服务器所作的计算也愈来愈多。可是每每咱们的应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何可以有效利用有限的资源来提供尽量大的吞吐量?一个有效的办法就是引入缓存,打破标准流程,每一个环节中请求能够从缓存中直接获取目标数据并返回,从而减小计算量,有效提高响应速度,让有限的资源服务更多的用户。java
二、概述node
2.一、缓存特征mysql
命中率程序员
命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个很是重要的问题,它是衡量缓存有效性的重要指标。命中率越高,代表缓存的使用率越高。web
最大元素(或最大空间)redis
缓存中能够存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不一样的场景合理的设置最大元素值每每能够必定程度上提升缓存的命中率,从而更有效的时候缓存。算法
清空策略sql
如上描述,缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提高命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提高命中率。常见的通常策略有:
FIFO(first in first out)
先进先出策略,最早进入缓存的数据在缓存空间不够的状况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的建立时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
LFU(less frequently used)
最少使用策略,不管是否过时,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
LRU(least recently used)
最近最少使用策略,不管是否过时,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此以外,还有一些简单策略好比:
根据过时时间判断,清理过时时间最长的元素;根据过时时间判断,清理最近要过时的元素;随机清理;根据关键字(或元素内容)长短清理等。
2.二、缓存介质
虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,能够分红内存、硬盘文件、数据库。
1内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,可是内存的缺点是没有持久化到物理磁盘,一旦应用异常break down而从新启动,数据很难或者没法复原。
2硬盘:通常来讲,不少缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的状况下,能够被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
3数据库:前面有提到,增长缓存的策略的目的之一就是为了减小数据库的I/O压力。如今使用数据库作缓存介质是否是又回到了老问题上了?其实,数据库也有不少种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于咱们经常使用的关系型数据库等。
2.三、缓存分类和应用场景
缓存有各种特征,并且有不一样介质的区别,那么实际工程中咱们怎么去对缓存分类呢?在目前的应用服务框架中,比较常见的,是根据缓存与应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存):
本地缓存:指的是在应用中的缓存组件,其最大的优势是应用和cache是在同一个进程内部,请求缓存很是快速,没有过多的网络开销等,在单应用不须要集群支持或者集群状况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序没法直接的共享缓存,各应用或集群的各节点都须要维护本身的单独缓存,对内存是一种浪费。
分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优势是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
目前各类类型的缓存都活跃在成千上万的应用服务中,尚未一种缓存方案能够解决一切的业务场景或数据类型,咱们须要根据自身的特殊场景和背景,选择最适合的缓存方案。缓存的使用是程序员、架构师的必备技能,好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。
三、浏览器缓存
3.一、简介:
HTTP报文就是浏览器和服务器间通讯时发送及响应的数据块。
浏览器向服务器请求数据,发送请求(request)报文;服务器向浏览器返回数据,返回响应(response)报文。
报文信息主要分为两部分
1.包含属性的首部(header)--------------------------附加信息(cookie,缓存信息等)与缓存相关的规则信息,均包含在header中
2.包含数据的主体部分(body)-----------------------HTTP请求真正想要传输的部分。
浏览器缓存也被称为客户端缓存,分为强缓存和协商缓存。
3.二、强缓存
强缓存是利用Expires和Cache-Control这两个http response header来实现的,它们都用来表示资源在客户端缓存的有效期。
Expries的值是一个GMT格式的绝对时间,表明着资源失效的时间,缺点就是当浏览器和服务器的时间相差较大时,会致使缓存混乱。另外Expires 是HTTP 1.0的东西,如今默认浏览器均默认使用HTTP 1.1,因此它的做用基本忽略。另外一个问题是,到期时间是由服务端生成的,可是客户端时间可能跟服务端时间有偏差,这就会致使缓存命中的偏差。
Cache-Control的max-age是相对时间,能够和Expries同时启用,同时启用的时候Cache-Control的优先级高。
Cache-Control 有重要的规则,常见的取值有private、public、no-cache、max-age,no-store,默认为private。
private: 客户端能够缓存
public: 客户端和代理服务器均可缓存(前端的同窗,能够认为public和private是同样的)
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 须要使用对比缓存来验证缓存数据(后面介绍)
no-store: 全部内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发来讲,缓存越多越好,so...基本上和它说886)
javaweb里面,咱们能够使用相似下面的代码设置强缓存:
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); //Expires:过期期限值
response.setHeader("Cache-Control", "public"); //Cache-Control来控制页面的缓存与否,public:浏览器和缓存服务器均可以缓存页面信息;
response.setHeader("Pragma", "Pragma"); //Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存
还能够经过相似下面的java代码设置不启用强缓存:
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//浏览器和缓存服务器都不该该缓存页面信息。
3.三、协商缓存
当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,若是协商缓存命中,请求响应返回的http状态为304而且会显示一个Not Modified的字符串。
协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的。
【Last-Modified,If-Modified-Since】的控制缓存的原理是:
1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间:
2)浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值:
3)服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,若是没有变化则返回304 Not Modified,可是不会返回资源内容;若是有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,由于既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header:
4)浏览器收到304的响应后,就会从缓存中加载资源。
5)若是协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在从新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值。
【Last-Modified,If-Modified-Since】都是根据服务器时间返回的header,通常来讲,在没有调整服务器时间和篡改客户端缓存的状况下,这两个header配合起来管理协商缓存是很是可靠的,可是有时候也会服务器上资源其实有变化,可是最后修改时间却没有变化的状况,而这种问题又很不容易被定位出来,而当这种状况出现的时候,就会影响协商缓存的可靠性。因此就有了另一对header来管理协商缓存,这对header就是【ETag、If-None-Match】。
【ETag、If-None-Match】,它们的缓存管理的方式是:
1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个惟一标识,这个惟一标识是一个字符串,只要资源有变化这个串就不一样,跟最后修改时间没有关系,因此能很好的补充Last-Modified的问题:
2)浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值:
3)服务器再次收到资源请求时,根据浏览器传过来If-None-Match和而后再根据资源生成一个新的ETag,若是这两个值相同就说明资源没有变化,不然就是有变化;若是没有变化则返回304 Not Modified,可是不会返回资源内容;若是有变化,就正常返回资源内容。与Last-Modified不同的是,当服务器返回304 Not Modified的响应时,因为ETag从新生成过,response header中还会把这个ETag返回,即便这个ETag跟以前的没有变化:
4)浏览器收到304的响应后,就会从缓存中加载资源。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】通常都是同时启用,这是为了处理Last-Modified不可靠的状况。有一种场景须要注意:
分布式系统里多台机器间文件的Last-Modified必须保持一致,以避免负载均衡到不一样机器致使比对失败;
分布式系统尽可能关闭掉ETag(每台机器生成的ETag都会不同)
1)当ctrl+f5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
2)当f5刷新网页时,跳过强缓存,可是会检查协商缓存;
四、数据库缓存
4.一、应用场景
针对数据库的增、删、查、改,数据库缓存技术应用场景绝大部分针对的是“查”的场景。好比,一篇常常访问的帖子/文章/新闻、热门商品的描述信息、好友评论/留言等。由于在常见的应用中,数据库层次的压力有80%的是查询,20%的才是数据的变动操做。因此绝大部分的应用场景的仍是“查”缓存。固然,“增、删、改”的场景也是有的。好比,一篇文章访问的次数,不可能每访问一次,咱们就去数据库里面加一次吧?这种时候,咱们通常“增”场景的缓存就必不可少。不然,一篇文章被访问了十万次,代码层次不会还去作十万次的数据库操做吧。
常见的数据库,好比oracle、mysql等,数据都是存放在磁盘中。虽然在数据库层也作了对应的缓存,但这种数据库层次的缓存通常针对的是查询内容,并且粒度也过小,通常只有表中数据没有变动的时候,数据库对应的cache才发挥了做用。但这并不能减小业务系统对数据库产生的增、删、查、改的庞大IO压力。因此数据库缓存技术在此诞生,实现热点数据的高速缓存,提升应用的响应速度,极大缓解后端数据库的压力。
要说用于数据库缓存场景的开源技术,那必然是memcache和redis这两个中间件:
由于都是专一于内存缓存领域,memcache和redis向来都有争议。好比性能,究竟是memcache性能好,仍是redis性能更好等。一样都是内存缓存技术,它们都有本身的技术特性。没有更好的技术,只有更合适的技术。我的总结一下,有持久化需求或者对数据结构和处理有高级要求的应用,选择redis。其余简单的key/value存储,选择memcache。因此根据自身业务特性,数据库缓存来选择适合本身的技术。
4.二、memcache
MemCache的工做流程以下:先检查客户端的请求数据是否在memcached中,若有,直接把请求数据返回,再也不对数据库进行任何操做;若是请求的数据不在memcached中,就去查数据库,把从数据库中获取的数据返回给客户端,同时把数据缓存一份到memcached中(memcached客户端不负责,须要程序明确实现);每次更新数据库的同时更新memcached中的数据,保证一致性;当分配给memcached内存空间用完以后,会使用LRU(Least Recently Used,最近最少使用)策略加上到期失效策略,失效数据首先被替换,而后再替换掉最近未使用的数据。
memcache是一套高性能的开源的分布式的高速内存对象缓存系统。由服务端和客户端组成,以守护程序(监听)方式运行于一个或多个服务器中,随时会接收客户端的链接和操做。memcache主要把数据对象缓存到内存中,它可以用来存储各类格式的数据,包括图像、视频、文件以及数据库检索的结果等,经过在内存里维护一个统一的巨大的hash表。简单的说就是将数据调用到内存中,而后从内存中读取,从而大大提升读取速度。memcache基于一个存储键/值对的hashmap进行存储对象到内存中。memcache是用C写的,可是客户端能够用任何语言来编写,并经过memcached协议与守护进程通讯。
特性:
在 Memcached中能够保存的item数据量是没有限制的,只要内存足够 。
Memcached单进程在32位系统中最大使用内存为2G,若在64位系统则没有限制,这是因为32位系统限制单进程最多可以使用2G内存,要使用更多内存,能够分多个端口开启多个Memcached进程 。
最大30天的数据过时时间,设置为永久的也会在这个时间过时,常量REALTIME_MAXDELTA
单个item最大数据是1MB,超过1MB数据不予存储,常量POWER_BLOCK 1048576进行控制。
另解:
memcached是应用较广的开源分布式缓存产品之一,它自己其实不提供分布式解决方案。在服务端,memcached集群环境实际就是一个个memcached服务器的堆积,环境搭建较为简单;cache的分布式主要是在客户端实现,经过客户端的路由处理来达到分布式解决方案的目的。客户端作路由的原理很是简单,应用服务器在每次存取某key的value时,经过某种算法把key映射到某台memcached服务器nodeA上,所以这个key全部操做都在nodeA上。
memcached客户端采用一致性hash算法做为路由策略,相对于通常hash(如简单取模)的算法,一致性hash算法除了计算key的hash值外,还会计算每一个server对应的hash值,而后将这些hash值映射到一个有限的值域上(好比0~2^32)。经过寻找hash值大于hash(key)的最小server做为存储该key数据的目标server。若是找不到,则直接把具备最小hash值的server做为目标server。同时,必定程度上,解决了扩容问题,增长或删除单个节点,对于整个集群来讲,不会有大的影响。最近版本,增长了虚拟节点的设计,进一步提高了可用性。
对于key-value信息,最好不要超过1m的大小;同时信息长度最好相对是比较均衡稳定的,这样可以保障最大限度的使用内存;同时,memcached采用的LRU清理策略,合理设置过时时间,提升命中率。
无特殊场景下,key-value能知足需求的前提下,使用memcached分布式集群是较好的选择,搭建与操做使用都比较简单;分布式集群在单点故障时,只影响小部分数据异常,目前还能够经过Magent缓存代理模式,作单点备份,提高高可用;整个缓存都是基于内存的,所以响应时间是很快,不须要额外的序列化、反序列化的程序,但同时因为基于内存,数据没有持久化,集群故障重启数据没法恢复。高版本的memcached已经支持CAS模式的原子操做,能够低成本的解决并发控制问题。
4.三、Redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value、远程内存数据库(非关系型数据库),和Memcached相似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。在此基础上,redis支持各类不一样方式的排序。与memcached同样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操做写入追加的记录文件,而且在此基础上实现了master-slave(主从)同步。
4.3.一、持久化
Redis支持主从同步。数据能够从主服务器向任意数量的从服务器上同步,从服务器能够是关联其余从服务器的主服务器。这使得Redis可执行单层树复制。存盘能够有意无心的对数据进行写操做。
Redis支持两种持久化方式:
(1):snapshotting(快照)也是默认方式。(把数据作一个备份,将数据存储到文件)
(2)Append-only file(缩写aof)的方式。
快照是默认的持久化方式,这种方式是将内存中数据以快照的方式写到二进制文件中,默认的文件名称为dump。rdb。能够经过配置设置自动作快照持久化的方式。咱们能够配置redis在n秒内若是超过m个key键修改就自动作快照。
aof方式:因为快照方式是在必定间隔时间作一次的,因此若是redis意外down掉的话,就会丢失最后一次快照后的全部修改。aof比快照方式有更好的持久化性,是因为在使用aof时,redis会将每个收到的写命令都经过write函数追加到文件中,当redis重启时会经过从新执行文件中保存的写命令来在内存中重建整个数据库的内容。
Redis一共支持四种持久化方式,主要使用的两种:
定时快照方式(snapshot):该持久化方式实际是在Redis内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否知足配置的持久化触发的条件,若是知足则经过操做系统fork调用来建立出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就能够经过子进程来遍历整个内存来进行存储操做,而主进程则仍然能够提供服务,当有写入时由操做系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。它的缺点是快照只是表明一段时间内的内存映像,因此系统重启会丢失上次快照与重启之间全部的数据。
基于语句追加文件的方式(aof):aof方式实际相似MySQl的基于语句的binlog方式,即每条会使Redis内存数据发生改变的命令都会追加到一个log文件中,也就是说这个log文件就是Redis的持久化数据。aof的方式的主要缺点是追加log文件可能致使体积过大,当系统重启恢复数据时若是是aof的方式则加载数据会很是慢,几十G的数据可能须要几小时才能加载完,固然这个耗时并非由于磁盘文件读取速度慢,而是因为读取的全部命令都要在内存中执行一遍。另外因为每条命令都要写log,因此使用aof的方式,Redis的读写性能也会有所降低。
4.3.二、Redis同步
总体过程概述以下:
一、 初始化
配置好主从后,不管slave是初次仍是从新链接到master, slave都会发送PSYNC命令到master。若是是从新链接,且知足增量同步的条件,那么redis会将内存缓存队列中的命令发给slave, 完成增量同步(Partial resynchronization)。不然进行全量同步。
二、正常同步开始
任何对master的写操做都会以redis命令的方式,经过网络发送给slave。
全量同步(full resynchronization)
Redis全量复制通常发生在Slave初始化阶段,这时Slave须要将Master上的全部数据都复制一份。具体步骤以下:
1)从服务器链接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的全部写命令;
3)主服务器BGSAVE执行完后,向全部从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃全部旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步:
Redis增量复制是指Slave初始化后开始正常工做时主服务器发生的写操做同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
几个重要概念:
内存缓存队列(in-memory backlog):用于记录链接断开时master收到的写操做
复制偏移量(replication offset):master, slave都有一个偏移,记录当前同步记录的位置
master服务器id(master run ID):master惟一标识。
增量同步的条件:现网络链接断开后,slave将尝试重连master。当知足下列条件时,重连后会进行增量同步:
一、slave记录的master服务器id和当前要链接的master服务器id相同
二、slave的复制偏移量比master的偏移量靠前。好比slave是1000, master是1100
三、slave的复制偏移量所指定的数据仍然保存在主服务器的内存缓存队列中
4.3.三、
Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务全部的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各类命令的算法时间复杂度,这些信息意味着:
Redis是线程安全的(由于只有一个线程),其全部操做都是原子的,不会因并发产生数据异常;
Redis的速度很是快(由于使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1));
使用高耗时的Redis命令是很危险的,会占用惟一的一个线程的大量处理时间,致使全部的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)
关于Key的一些注意事项:
不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不只会消耗更多的内存,还会致使查找的效率下降
Key短到缺失了可读性也是很差的,例如”u1000flw”比起”user:1000:followers”来讲,节省了寥寥的存储空间,却引起了可读性和可维护性上的麻烦
最好使用统一的规范来设计Key,好比”object-type:id:attr”,以这一规范设计出的Key多是”user:1000”或”comment:1234:reply-to”
Redis容许的最大Key长度是512MB(对Value的长度限制也是512MB)
Redis的基本数据类型只有String,但Redis能够把String做为整型或浮点型数字来使用,主要体如今INCR、DECR类的命令上:
INCR:将key对应的value值自增1,并返回自增后的值。只对能够转换为整型的String数据起做用。时间复杂度O(1)
INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对能够转换为整型的String数据起做用。时间复杂度O(1)
DECR/DECRBY:同INCR/INCRBY,自增改成自减。
INCR/DECR系列命令要求操做的value类型为String,并能够转换为64位带符号的整型数字,不然会返回错误。
也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 - 1]范围内。
前文提到过,Redis采用单线程模型,自然是线程安全的,这使得INCR/DECR命令能够很是便利的实现高并发场景下的精确控制。
Hash即哈希表,Redis的Hash和传统的哈希表同样,是一种field-value型的数据结构,能够理解成将HashMap搬入Redis。
Hash很是适合用于表现对象类型的数据,用Hash中的field对应对象的field便可。
Hash的优势包括:
能够实现二元查找,如”查找ID为1000的用户的年龄”;
比起将整个对象序列化后做为String存储的方法,Hash可以有效地减小网络传输的消耗;
当使用Hash维护一个集合时,提供了比List效率高得多的随机访问命令。
针对Redis的性能优化,主要从下面几个层面入手:
最初的也是最重要的,确保没有让Redis执行耗时长的命令;
使用pipelining将连续执行的命令组合执行;
操做系统的Transparent huge pages功能必须关闭:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
若是在虚拟机中运行Redis,可能自然就有虚拟机环境带来的固有延迟。能够经过。/redis-cli —intrinsic-latency 100命令查看固有延迟。同时若是对Redis的性能有较高要求的话,应尽量在物理机上直接部署Redis。
检查数据持久化策略;
考虑引入读写分离机制;
长耗时命令;
Redis绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,在文本和官方文档中均对每一个命令的时间复杂度有说明。
一般来讲,O(1)的命令是安全的,O(N)命令在使用时须要注意,若是N的数量级不可预知,则应避免使用。例如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,一般来讲这些命令执行的很快,但若是这个Hash中的field数量极多,耗时就会成倍增加。
又如使用SUNION对两个Set执行Union操做,或使用SORT对List/Set执行排序操做等时,都应该严加注意。
避免在使用这些O(N)命令时发生问题主要有几个办法:
不要把List当作列表使用,仅当作队列来使用;
经过机制严格控制Hash、Set、Sorted Set的大小;
可能的话,将排序、并集、交集等操做放在客户端执行;
绝对禁止使用KEYS命令;
避免一次性遍历集合类型的全部成员,而应使用SCAN类的命令进行分批的,游标式的遍历。
Redis提供了SCAN命令,能够对Redis中存储的全部key进行游标式的遍历,避免使用KEYS命令带来的性能问题。同时还有SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历。SCAN类命令的使用请参考官方文档:https://redis。io/commands/scan
Redis提供了Slow Log功能,能够自动记录耗时较长的命令。相关的配置参数有两个:
slowlog-log-slower-than xxxms #执行时间慢于xxx毫秒的命令计入Slow Logslowlog-max-len xxx #Slow Log的长度,即最大纪录多少条Slow Log;
使用SLOWLOG GET [number]命令,能够输出最近进入Slow Log的number条命令。
使用SLOWLOG RESET命令,能够重置Slow Log;
Redis延迟
一、网络引起的延迟
尽量使用长链接或链接池,避免频繁建立销毁链接;
客户端进行的批量数据操做,应使用Pipeline特性在一次交互中完成。
二、数据持久化引起的延迟
Redis的数据持久化工做自己就会带来延迟,须要根据数据的安全级别和性能要求制定合理的持久化策略:
AOF + fsync always的设置虽然可以绝对确保数据安全,但每一个操做都会触发一次fsync,会对Redis的性能有比较明显的影响
AOF + fsync every second是比较好的折中方案,每秒fsync一次
AOF + fsync never会提供AOF持久化方案下的最优性能
使用RDB持久化一般会提供比使用AOF更高的性能,但须要注意RDB的策略配置
每一次RDB快照和AOF Rewrite都须要Redis主进程进行fork操做。fork操做自己可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的状况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟;
Redis在fork子进程时须要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共须要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2。27Ghz的物理机上,这一fork操做耗时216ms。
能够经过INFO命令返回的latest_fork_usec字段查看上一次fork操做的耗时(微秒)
Swap引起的延迟
当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,致使Redis出现不正常的延迟。Swap一般在物理内存不足或一些进程在进行大量I/O操做时发生,应尽量避免上述两种状况的出现。
/proc//smaps文件中会保存进程的swap记录,经过查看这个文件,可以判断Redis的延迟是否由Swap产生。若是这个文件中记录了较大的Swap size,则说明延迟颇有多是Swap形成的。
三、数据淘汰引起的延迟
当同一秒内有大量key过时时,也会引起Redis的延迟。在使用时应尽可能将key的失效时间错开。
引入读写分离机制
Redis的主从复制能力能够实现一主多从的多节点架构,在这一架构下,主节点接收全部写请求,并将数据同步给多个从节点。
在这一基础上,咱们可让从节点提供对实时性要求不高的读请求服务,以减少主节点的压力。
尤为是针对一些使用了长耗时命令的统计类任务,彻底能够指定在一个或多个从节点上执行,避免这些长耗时命令影响其余请求的响应。
主从复制与集群分片
主从复制:
Redis支持一主多从的主从复制架构。一个Master实例负责处理全部的写请求,Master将写操做同步至全部Slave。
借助Redis的主从复制,能够实现读写分离和高可用:
实时性要求不是特别高的读请求,能够在Slave上完成,提高效率。特别是一些周期性执行的统计任务,这些任务可能须要执行一些长耗时的Redis命令,能够专门规划出1个或几个Slave用于服务这些统计任务
借助Redis Sentinel能够实现高可用,当Master crash后,Redis Sentinel可以自动将一个Slave晋升为Master,继续提供服务
启用主从复制很是简单,只须要配置多个Redis实例,在做为Slave的Redis实例中配置:
slaveof 192。168。1。1 6379 #指定Master的IP和端口
当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据经过Redis Protocol同步给Slave。以后主从之间的数据便一直以Redis Protocol进行同步;
使用Sentinel作自动failover
Redis的主从复制功能自己只是作数据同步,并不提供监控和自动failover能力,要经过主从复制功能来实现Redis的高可用,还须要引入一个组件:Redis Sentinel
Redis Sentinel是Redis官方开发的监控组件,能够监控Redis实例的状态,经过Master节点自动发现Slave节点,并在监测到Master节点失效时选举出一个新的Master,并向全部Redis实例推送新的主从配置。
Redis Sentinel须要至少部署3个实例才能造成选举关系。
关键配置:
sentinel monitor mymaster 127。0。0。1 6379 2 #Master实例的IP、端口,以及选举须要的同意票数sentinel down-after-milliseconds mymaster 60000 #多长时间没有响应视为Master失效sentinel failover-timeout mymaster 180000 #两次failover尝试间的间隔时长sentinel parallel-syncs mymaster 1 #若是有多个Slave,能够经过此配置指定同时重新Master进行数据同步的Slave数,避免全部Slave同时进行数据同步致使查询服务也不可用
另外须要注意的是,Redis Sentinel实现的自动failover不是在同一个IP和端口上完成的,也就是说自动failover产生的新Master提供服务的IP和端口与以前的Master是不同的,因此要实现HA,还要求客户端必须支持Sentinel,可以与Sentinel交互得到新Master的信息才行。
集群分片:
为什么要作集群分片:
Redis中存储的数据量大,一台主机的物理内存已经没法容纳
Redis的写请求并发量大,一个Redis实例以没法承载
当上述两个问题出现时,就必需要对Redis进行分片了。
Redis的分片方案有不少种,例如不少Redis的客户端都自行实现了分片功能,也有向Twemproxy这样的以代理方式实现的Redis分片方案。然而首选的方案还应该是Redis官方在3。0版本中推出的Redis Cluster分片方案。
本文不会对Redis Cluster的具体安装和部署细节进行介绍,重点介绍Redis Cluster带来的好处与弊端。
Redis Cluster的能力
可以自动将数据分散在多个节点上
当访问的key不在当前分片上时,可以自动将请求转发至正确的分片
当集群中部分节点失效时仍能提供服务
其中第三点是基于主从复制来实现的,Redis Cluster的每一个数据分片都采用了主从复制的结构,原理和前文所述的主从复制彻底一致,惟一的区别是省去了Redis Sentinel这一额外的组件,由Redis Cluster负责进行一个分片内部的节点监控和自动failover。
Redis Cluster分片原理
Redis Cluster中共有16384个hash slot,Redis会计算每一个key的CRC16,将结果与16384取模,来决定该key存储在哪个hash slot中,同时须要指定Redis Cluster中每一个数据分片负责的Slot数。Slot的分配在任什么时候间点均可以进行从新分配。
客户端在对key进行读写操做时,能够链接Cluster中的任意一个分片,若是操做的key不在此分片负责的Slot范围内,Redis Cluster会自动将请求重定向到正确的分片上。
hash tags
在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式明明的key,将会确保进入同一个Slot中。例如:{uiv}user:1000和{uiv}user:1001拥有一样的hash tag {uiv},会保存在同一个Slot中。
使用Redis Cluster时,pipelining、事务和LUA 功能涉及的key必须在同一个数据分片上,不然将会返回错误。如要在Redis Cluster中使用上述功能,就必须经过hash tags来确保一个pipeline或一个事务中操做的全部key都位于同一个Slot中。
有一些客户端(如Redisson)实现了集群化的pipelining操做,能够自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不一样的分片上执行。可是Redis不支持跨分片的事务,事务和LUA 仍是必须遵循全部key在一个分片上的规则要求。
主从复制 vs 集群分片
在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?
从各个方面看,Redis Cluster都是优于主从复制的方案
Redis Cluster可以解决单节点上数据量过大的问题
Redis Cluster可以解决单节点访问压力过大的问题
Redis Cluster包含了主从复制的能力
那是否是表明Redis Cluster永远是优于主从复制的选择呢?
并非。
软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,必定也会带来相应的弊端。采用Redis Cluster的弊端包括:
1 维护难度增长。在使用Redis Cluster时,须要维护的Redis实例数倍增,须要监控的主机数量也相应增长,数据备份/持久化的复杂度也会增长。同时在进行分片的增减操做时,还须要进行reshard操做,远比主从模式下增长一个Slave的复杂度要高。
2客户端资源消耗增长。当客户端使用链接池时,须要为每个数据分片维护一个链接池,客户端同时须要保持的链接数成倍增多,加大了客户端自己和操做系统资源的消耗。
3性能优化难度增长。你可能须要在多个分片上查看Slow Log和Swap日志才能定位性能问题。
4事务和LUA 的使用成本增长。在Redis Cluster中使用事务和LUA 特性有严格的限制条件,事务和中操做的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。若是应用的场景中大量涉及事务和的使用,如何在保证这两个功能的正常运做前提下把数据平均分到多个数据分片中就会成为难点。
因此说,在主从复制和集群分片两个方案中作出选择时,应该从应用软件的功能特性、数据和访问量级、将来发展规划等方面综合考虑,只在确实有必要引入数据分片时再使用Redis Cluster。
下面是一些建议:
须要在Redis中存储的数据有多大?将来2年内可能发展为多大?这些数据是否都须要长期保存?是否能够使用LRU算法进行非热点数据的淘汰?综合考虑前面几个因素,评估出Redis须要使用的物理内存。
用于部署Redis的主机物理内存有多大?有多少能够分配给Redis使用?对比(1)中的内存需求评估,是否足够用?
Redis面临的并发写压力会有多大?在不使用pipelining时,Redis的写性能能够超过10万次/秒(更多的benchmark能够参考 https://redis。io/topics/benchmarks )
在使用Redis时,是否会使用到pipelining和事务功能?使用的场景多很少?
综合上面几点考虑,若是单台主机的可用物理内存彻底足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,能够省去不少没必要要的麻烦。同时,若是应用中大量使用pipelining和事务,也建议尽量选择主从复制架构,能够减小设计和开发时的复杂度。
Redis Java客户端的选择
Redis的Java客户端不少,官方推荐的有三种:Jedis、Redisson和lettuce。
在这里对Jedis和Redisson进行对比介绍
Jedis:
轻量,简洁,便于集成和改造
支持链接池
支持pipelining、事务、LUA ing、Redis Sentinel、Redis Cluster
不支持读写分离,须要本身实现
文档差(真的不好,几乎没有……)
Redisson:
基于Netty实现,采用非阻塞IO,性能高
支持异步请求
支持链接池
支持pipelining、LUA ing、Redis Sentinel、Redis Cluster
不支持事务,官方建议以LUA ing代替事务
支持在Redis Cluster架构下使用pipelining
支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下均可以使用
内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
能够与Spring Session集成,实现基于Redis的会话共享
文档较丰富,有中文文档
对于Jedis和Redisson的选择,一样应遵循前述的原理,尽管Jedis比起Redisson有各类各样的不足,但也应该在须要使用Redisson的高级特性时再选用Redisson,避免形成没必要要的程序复杂度提高。
五、缓存在高并发场景下的常见问题
5.一、缓存一致性问题
当数据时效性要求很高时,须要保证缓存中的数据与数据库中的保持一致,并且须要保证缓存节点和副本中的数据也保持一致,不能出现差别现象。这就比较依赖缓存的过时和更新策略。通常会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存。
在不少应用场景中,当一个数据发生变动的时候,不少人在考虑怎么样确保缓存数据和数据库中数据保存一致性,确保从缓存读取的数据是最新的。甚至,有人在对应数据变动的时候,先更新数据库,而后再去更新缓存。我以为这个考虑不太现实,一方面这会致使代码层次逻辑变得复杂,另一方面也真想不明白还要缓存干什么了。在绝大多数的应用中,缓存中的数据和数据库中的数据是不一致的。即,咱们牺牲了实时性换回了访问速度。好比,一篇常常访问的帖子,可能这篇帖子已经在数据库层次进行了变动。而咱们每次访问的时候,读取的都是缓存中的数据(帖子)。既然是缓存,那么必然是对实时性能够有必定的容忍度的数据,容忍度的时间能够是5分钟,也能够是5小时,取决于业务场景的要求。相反,必定要求是实时性的数据库,就不该该从缓存里读取,好比库存,再好比价格。
5.二、缓存并发问题
缓存过时后将尝试从后端数据库获取数据,这是一个看似合理的流程。可是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库形成极大的冲击,甚至致使 “雪崩”现象。此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会致使一致性的问题。那如何避免相似问题呢?咱们会想到相似“锁”的机制,在缓存更新或者过时的状况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其余的请求只须要牺牲必定的等待时间,便可直接从缓存中继续获取数据。
5.三、缓存穿透问题
缓存穿透在有些地方也称为“击穿”。不少朋友对缓存穿透的理解是:因为缓存故障或者缓存过时致使大量请求穿透到后端数据库服务器,从而对数据库形成巨大冲击。
这实际上是一种误解。真正的缓存穿透应该是这样的:
在高并发场景下,若是某一个key被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而致使了大量请求达到数据库,而当该key对应的数据自己就是空的状况下,这就致使数据库中并发的去执行了不少没必要要的查询操做,从而致使巨大冲击和压力。
能够经过下面的几种经常使用方式来避免缓存传统问题:
一、缓存空对象
对查询结果为空的对象也进行缓存,若是是集合,能够缓存一个空的集合(非null),若是是缓存单个对象,能够经过字段标识来区分。这样避免请求穿透到后端数据库。同时,也须要保证缓存数据的时效性。这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。
二、单独过滤处理
对全部可能对应数据为空的key进行统一的存放,并在请求前作拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,可是更新不频繁的数据。
5.四、缓存颠簸问题
缓存的颠簸问题,有些地方可能被成为“缓存抖动”,能够看作是一种比“雪崩”更轻微的故障,可是也会在一段时间内对系统形成冲击和性能影响。通常是因为缓存节点故障致使。业内推荐的作法是经过一致性Hash算法来解决。
5.五、缓存的雪崩现象
缓存雪崩就是指因为缓存的缘由,致使大量请求到达后端数据库,从而致使数据库崩溃,整个系统崩溃,发生灾难。致使这种现象的缘由有不少种,上面提到的“缓存并发”,“缓存穿透”,“缓存颠簸”等问题,其实均可能会致使缓存雪崩现象发生。这些问题也可能会被恶意攻击者所利用。还有一种状况,例如某个时间点内,系统预加载的缓存周期性集中失效了,也可能会致使雪崩。为了不这种周期性失效,能够经过设置不一样的过时时间,来错开缓存过时,从而避免缓存集中失效。
从应用架构角度,咱们能够经过限流、降级、熔断等手段来下降影响,也能够经过多级缓存来避免这种灾难。此外,从整个研发体系流程的角度,应该增强压力测试,尽可能模拟真实场景,尽早的暴露问题从而防范。
缓存雪崩是因为原有缓存失效(过时),新缓存未到期间。全部请求都去查询数据库,而对数据库CPU和内存形成巨大压力,严重的会形成数据库宕机。从而造成一系列连锁反应,形成整个系统崩溃。
一、碰到这种状况,通常并发量不是特别多的时候,使用最多的解决方案是加锁排队。
二、加锁排队只是为了减轻数据库的压力,并无提升系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。一样会致使用户等待超时,这是个治标不治本的方法。还有一个解决办法解决方案是:给每个缓存数据增长相应的缓存标记,记录缓存的是否失效,若是缓存标记失效,则更新数据缓存。
缓存标记:记录缓存数据是否过时,若是过时会触发通知另外的线程在后台去更新实际key的缓存。
缓存数据:它的过时时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过时后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。这样作后,就能够必定程度上提升系统吞吐量。
缓存穿透是指用户查询数据,在数据库没有,天然在缓存中也不会有。这样就致使用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,而后返回空。这样请求就绕过缓存直接查数据库,这也是常常提的缓存命中率问题。
解决的办法就是:若是查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
把空结果也给缓存起来,这样下次一样的请求就能够直接返回空了,便可以免当查询的值为空时引发的缓存穿透。同时也能够单独设置个缓存区域存储空值,对要查询的key进行预先校验,而后再放行给后面的正常缓存处理逻辑。
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去加载相关的数据。
解决思路:
1,直接写个缓存刷新页面,上线时手工操做下。
2,数据量不大,能够在WEB系统启动的时候加载。
3,定时刷新缓存。
5.五、缓存更新
缓存淘汰的策略有两种:
(1) 定时去清理过时的缓存。
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过时,过时的话就去底层系统获得新数据并更新缓存。
二者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,具体用哪一种方案,能够根据本身的应用场景来权衡。
一、预估失效时间 二、 版本号(必须单调递增,时间戳是最好的选择)三、 提供手动清理缓存的接口。
六、提升缓存命中率
6.一、缓存命中率的介绍
命中:能够直接经过缓存获取到须要的数据。
不命中:没法直接经过缓存获取到想要的数据,须要再次查询数据库或者执行其它的操做。缘由多是因为缓存中根本不存在,或者缓存已通过期。
一般来说,缓存的命中率越高则表示使用缓存的收益越高,应用的性能越好(响应时间越短、吞吐量越高),抗并发的能力越强。因而可知,在高并发的互联网系统中,缓存的命中率是相当重要的指标。
6.二、影响缓存命中率的几个因素
1业务场景和业务需求
缓存适合“读多写少”的业务场景,反之,使用缓存的意义其实并不大,命中率会很低。
业务需求决定了对时效性的要求,直接影响到缓存的过时时间和更新策略。时效性要求越低,就越适合缓存。在相同key和相同请求数的状况下,缓存时间越长,命中率会越高。
互联网应用的大多数业务场景下都是很适合使用缓存的。
2缓存的设计(粒度和策略)
一般状况下,缓存的粒度越小,命中率会越高。举个实际的例子说明:
当缓存单个对象的时候(例如:单个用户信息),只有当该对象对应的数据发生变化时,咱们才须要更新缓存或者让移除缓存。而当缓存一个集合的时候(例如:全部用户数据),其中任何一个对象对应的数据发生变化时,都须要更新或移除缓存。
还有另外一种状况,假设其余地方也须要获取该对象对应的数据时(好比其余地方也须要获取单个用户信息),若是缓存的是单个对象,则能够直接命中缓存,反之,则没法直接命中。这样更加灵活,缓存命中率会更高。
此外,缓存的更新/过时策略也直接影响到缓存的命中率。当数据发生变化时,直接更新缓存的值会比移除缓存(或者让缓存过时)的命中率更高,固然,系统复杂度也会更高。
3缓存容量和基础设施
缓存的容量有限,则容易引发缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了LRU算法)。同时,缓存的技术选型也是相当重要的,好比采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则毕竟容易扩展。因此须要作好系统容量规划,并考虑是否可扩展。此外,不一样的缓存框架或中间件,其效率和稳定性也是存在差别的。
4其余因素
当缓存节点发生故障时,须要避免缓存失效并最大程度下降影响,这种特殊状况也是架构师须要考虑的。业内比较典型的作法就是经过一致性Hash算法,或者经过节点冗余的方式。
有些朋友可能会有这样的理解误区:既然业务需求对数据时效性要求很高,而缓存时间又会影响到缓存命中率,那么系统就别使用缓存了。其实这忽略了一个重要因素--并发。一般来说,在相同缓存时间和key的状况下,并发越高,缓存的收益会越高,即使缓存时间很短。
6.三、提升缓存命中率的方法
从架构师的角度,须要应用尽量的经过缓存直接获取数据,并避免缓存失效。这也是比较考验架构师能力的,须要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并作权衡。尽量的聚焦在高频访问且时效性要求不高的热点业务上,经过缓存预加载(预热)、增长存储容量、调整缓存粒度、更新缓存等手段来提升命中率。
对于时效性很高(或缓存空间有限),内容跨度很大(或访问很随机),而且访问量不高的应用来讲缓存命中率可能长期很低,可能预热后的缓存还没来得被访问就已通过期了。
一致性Hash算法
对于分布式缓存,不一样机器上存储不一样对象的数据。为了实现这些缓存机器的负载均衡,能够采起一致性hash算法来实现。
一致性hash算法经过一个叫做一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,而且起点与终点链接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1]。
假设如今咱们有4个对象,分别为o1,o2,o3,o4,使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1):
hash(o1) = m1
hash(o2) = m2
hash(o3) = m3
hash(o4) = m4
把m1,m2,m3,m4这4个值放置到hash环上,获得以下图:
使用一样的hash函数,咱们将机器也放置到hash环上。假设咱们有三台缓存机器,分别为 c1,c2,c3,使用hash函数计算这3台机器的hash值:
hash(c1) = t1
hash(c2) = t2
hash(c3) = t3
把t1,t2,t3 这3个值放置到hash环上,获得以下图:
为对象选择机器
将对象和机器都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,便是这个对象所属的机器。
例如,对于对象o2,顺序针找到最近的机器是c1,故机器c1会缓存对象o2。而机器c2则缓存o3,o4,机器c3则缓存对象o1。
新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并无由于机器c4的加入而减小负载压力。若是4台机器的性能是同样的,那么这种结果并非咱们想要的。
为此,咱们引入虚拟节点来解决负载不均衡的问题。
将每台物理机器虚拟为一组虚拟机器,将虚拟机器放置到hash环上,若是须要肯定对象的机器,先肯定对象的虚拟机器,再由虚拟机器肯定物理机器。
引用: