-
秒懂,Redis缓存穿透、缓存击穿、缓存雪崩概念以及应对策
如上图所示,我们在应用程序和Mysql数据库中建立一个中间层,即Redis缓存。通过Redis缓存可以有效减少查询数据库的时间消耗,这极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求极高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存击穿和缓存雪崩。本文将简单介绍缓存穿透、缓存雪崩和缓存击穿这三者之间的区别以及这三类问题的解决方法。一、缓存穿透缓存穿透是指用户访问了不存在的数据,导致缓存无法命中,大量的请求都要穿透到数据库进行查询,从而使得数据库压力过大,甚至挂掉。比如:数据库使用了id为正整数作为键,但是黑客使用负整数向服务器发起请求,这时所有的请求都没有在缓存中命中,从而导致大量请求数据库,如果超过了数据库的承载能力,会导致数据库服务器宏机。一般解决缓存穿透的方法有:(1)缓存空对象这是一个简单粗暴的方法,方法是如果一个查询返回的结果是空的,仍然把这个空结果进行缓存,这样的话缓存层就存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个比较短的过期时间,让其自动过期。(2)使用布隆过滤器拦截这是一种常见而且有效的策略,它将所有可能存在的数据哈希到一个足够大的bitmap中,当查询一个不存在的key时会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。Redis实现了布隆过滤器,我们可以直接使用来达到过滤的目的。具体的原理和使用方法可以参考我的另一篇博文布隆过滤器(BloomFilter)的原理和实现二、缓存击穿(缓存并发)缓存击穿,是指当某个key在过期的瞬间,有大量的请求并发访问过期的键,这类数据一般是热点数据,由于缓存过期了,会同时访问数据库来查询数据,并写回缓存,从而导致数据库瞬间压力过大。缓存击穿的解决方法也有两个:(1)设置热点数据永不过期当遇到这种情况的时候,数据库很难扛下来这么大的并发。最简单的方法就是将热点数据缓存永不过期就好了。(2)使用互斥锁业界比较常用的方法,使用锁控制访问后盾服务的并发量。分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其线程没有获取到分布式锁等待就好了。这种方式将高并发的压力转移到了分布式锁,因此对系统的分布式锁是否合格考验很大。关于分布式锁内容可以参考我的另一篇博文分布式锁的三种实现方案本地锁:与分布式锁的作用类似,我们通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程去数据库中查询,如果一个服务有多个节点,则还会有多个数据库查询操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发的问题。基于Redis实现的分布式锁伪代码:三、缓存雪崩缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询同一时刻都落到了数据库上,造成了数据库压力过大。缓存雪崩与缓存击穿的区别在于这里针对很多key同时失效,前者则是针对某一个热点key失效解决方案:(1)不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀(2)在缓失效后,通过分布式锁或者分布式队列的方式控制数据库写缓存的线程数。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(和缓存击穿中使用互斥锁类似)(3)如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。随机过期时间伪代码:解释:在同一分类中的key,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。这种情况就应该考虑使用主备缓存策略了。最后再介绍几个有关缓存的概念缓存预热缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!解决思路:1、直接写个缓存刷新页面,上线时手工操作下;2、数据量不大,可以在项目启动的时候自动进行加载;3、定时刷新缓存;缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:(1)定时去清理过期的缓存;(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。总结本文介绍了缓存应用中比较典型的是三个问题:缓存穿透,缓存击穿和缓存雪崩的概念和基本的解决方法。最后还介绍了缓存预热、缓存更新以及缓存降级等概念。在缓存的应用中还有一个比较典型的问题就是如何保证数据的一致性问题,也欢迎大家参考我的另一篇博文Redis缓存和MySQL数据一致性方案详解参考[1]https://www.pieruo.com/13549.html[2]https://www.cnblogs.com/midoujava/p/11277096.html
LoveIT 2021-01-18Redis -
管理分布式会话的四种方式以及基于Redis的分布式会话实现方案
应用服务器的高可用架构设计最为理想的是服务无状态,但实际上业务总会有状态的,以session记录用户信息的例子来讲,未登入时,服务器没有记入用户信息的session访问网站都是以游客方式访问的,账号密码登入网站后服务器必须要记录你的用户信息记住你是登入后的状态,以该状态分配给你更多的权限。那么管理session有哪些方法呢?一、四种分布式Session管理方案1、Session复制session复制是早期企业应用系统使用比较多的一种服务器集群Session管理机制。应用服务器开启Web容器的的Session复制功能,在集群中的几台服务器之间同步Session对象,是的每台服务器上都保存所有用户的Session信息,这样任何一台机器宕机都不会导致Session数据的丢失,而服务器使用Session时候,也只需要在本机获取即可。如图1所示。这种方案简单,且从本机读取session也相当快捷,但有非常明显的缺陷:只能使用在集群规模比较小的情况下(企业应用系统,使用人数少,相对比较常见这种模式),当集群规模比较大的时候,集群服务器之间需要大量的通信进行Session的复制,占用服务器和网络的大量资源,系统负担较大。而且由于用户的session信息在每台服务器上都有备份,在大量用户访问下,可能会出现服务器内存都还不够session使用的情况。2.session会话保持(黏滞会话)会话保持是利用负载均衡的原地址Hash算法实现,负载均衡服务器总是将来源于同一IP的请求分发到同一台服务器上,,也可以根据cookie信息将同一个用户的请求每次都分发到同一台服务器上,不过这时的负载均衡服务器必须工作在HTTP协议层上。这种会话保持也叫黏滞会话(StickySessions)在Nginx中配置的会话保持:这种方案虽然保证了每个用户都能准确的拿到自己的session,而且大量用户访问也不怕,但是这种会话保持不符合系统高可用的需求。这种方案有着致命的缺陷:一旦某台服务器发生宕机,则该服务器上的所有session信息就会不存在,用户请求就会切换到其他服务器,而其他服务器因为没有其对应的session信息导致无法完成相关业务。所以这种方法基本上不会被采纳。3.利用cookie记录session 早期的企业应用系统使用C/S架构,管理session的方法就是将session记录在客户端,每次请求服务器的时候将session放在请求中发送给服务器,服务器处理过请求后再将修改过的session返回给客户端。网站虽然没有客户端,但是可以利用浏览器支持的cookie记录session。 利用cookie记录session是存在很多缺点:比如cookie的大小存在限制能记录的信息不能超过限制;比如每次请求都要传输cookie影响性能;比如cookie可被修改或者存在破解的可能,导致cookie不能存重要信息,安全系数不够。但是由于cookie简单易用,支持服务器的线性伸缩,而且大部分的session信息相对较小,所以其实很多网站或多或少的都会使用cookie来记录部分不重要的session信息。4.session服务器(集群)目前最理想的服务器集群的session管理应该是session服务器,集成了高可用、伸缩性好、对保存信息大小没有限制、性能也相对很好。这种统一管理session的方式将应用服务器分离,分为无状态的应用服务器和有状态的session服务器。如下图所示:二、SpringBoot+Redis+Nginx实现分布式Session1、环境准备(1)SpringBoot2.1.8.RELEASE(2)Redis5(3)Nginx1.17.8(4)Tomcat92、实现基本原理使用redis实现session共享是基于session集中存储的实现方案,即把session放在一个公共的redis服务器里,所有Web服务器节点都连接着这个公用redis服务器,从而在请求时从公用的redis里查询存放的session值。这就是实现了session共享。思路在用户登录成功时,把用户的信息设置到redis服务器里,然后每次请求时都在过滤器(或拦截器)里获取该值,若有值继续操作,没值跳转到登录页面重新登录。关于Redis的配置这里就不赘述了,不清楚的小伙伴自行百度或Google~~这里重点说一下登录逻辑中如何处理Session问题以及如果在之后如何验证用户身份第一步、写一个登录拦截器(过滤器),检查用户是否登录,如果没有登录就重定向用户到登录也爱你登录去,如果登录了返回true,放行拦截器编写完成后注意在WebConfiguration的addInterceptor中注册一下:第二步、实现正常登录逻辑,主要就是检查用户密码是否匹配,如果检查没问题的话,就继续下一步第三步、用户名密码检查无误之后,设置cookie值,把作为保存Session信息在redis中的key值存入cookie,刷新浏览器的时候,过滤器可以从cookie中取到key值,进而去redis取对应的value值,即Session完成这一操作,用户的session信息已经存入到redis中,可在redis中查看是否存入参考【1】程序猿中的小白.管理分布式session的四种方式.博客园【2】零度编程.详解Session分布式共享.搜狐【3】任枫丶.Java中使用Redis实现Session共享.CSDN
LoveIT 2020-11-16Redis -
Redis的过期键删除策略以及内存淘汰策略
Redis的数据已经设置了TTL,不是过期就已经删除了吗?为什么还存在所谓的淘汰策略呢?这个原因我们需要从redis的过期策略聊起。一、Redis过期键策略redis键的过期时间都保存在过期字典中,过期键的删除策略有3种:定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即删除对键的删除操作。惰性删除:放任键过期不管,但每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键。如果没有过期,就返回该键。定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及检查多少数据库,有算法决定。1、定时删除特点优点:对内存友好,通过定时器可以保证过期键过期键会尽可能快的删除,并释放过期键占用的空间。缺点:1)cpu不友好,在过期键比较多的情况下,删除过期键可能会占用相当一部分cpu时间;在内存不紧张cpu紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,无疑会对服务器响应时间和吞吐量造成影响。2)创建定时器需要Redis服务器中的时间事件,而现在时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效的处理大量时间事件。2、惰性删除优点:1)对cpu友好,程序只在取出键时才对建进行过期检查,删除的目标仅限于当前处理的键。缺点:1)对内存不友好,当数据库中有大量的过期键,而这些键又没有被访问到,那么他们也许会永远不会被删除。3、定期删除定期删除是前两种删除策略的一种折中。会每隔一段时间执行一次删除过期键操作,并通过限制操作执行的时长和频率来减少删除操作对cpu时间的影响。定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况二、Redis内存淘汰策略Redis内存淘汰策略有了以上过期策略的说明后,就很容易理解为什么需要淘汰策略了,因为不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,如果大量过期key堆积在内存里,导致redis内存块耗尽了,怎么办?所以就需要内存淘汰策略进行补充,内存淘汰策略就是当redis使用的内存到达了最大内存阈值之后移除键的一种策略。Redis有如下几种内存淘汰策略:volatile-lru:从所有设置了过期时间的key中,选择删除最近最少使用的keyallkeys-lru:从所有的key中选择删除最近最少使用的keyvolatile-lfu:从所有设置了过期时间的key中,选择删除使用频率最低的keyallkeys-lfu:从所有key中选择删除使用频率最低的keyvolatile-random:从所有设置了过期时间的key中,随机选择删除keyallkeys-random:从所有key中随机选择删除keyvolatile-ttl:从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。noeviction:当内存达到阈值的时候,新的写入操作报错总结Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。参考【1】幂次方.彻底弄懂Redis的内存淘汰策略.博客园【2】小小码农甲.Redis的过期策略和内存淘汰策略.简书
LoveIT 2020-10-10Redis -
Redis五种常见数据结构的实现及使用场景
一、Redis对象底层数据结构Redis的八种编码类型,如下表所示:编码类型编码所对应的底层数据结构REDIS_ENCODING_INT8字节的long长整型REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串REDIS_ENCODING_RAW简单动态字符串REDIS_ENCODING_HT字典REDIS_ENCODING_LINKEDLIST双端链表REDIS_ENCODING_ZIPLIST压缩列表REDIS_ENCODING_INTSET整数集合REDIS_ENCODING_SKIPLIST跳跃表和字典1、SDS(简单动态字符串)字符串对象的编码可以是int、raw或者embstr(专门保存段字符串的优化编码方式)1.1、raw重点说一下SDS,当字符串长度大于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39)(39字节)的时候,底层实现为SDS,encoding编码设置为rawRedis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simpledynamicstring,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示.在Redis里面,C字符串只会作为字面量(stringliteral)用在一些无须对字符串值进行修改的地方。在一个可以被修改的字符串值里面,Redis就会使用SDS来表示字符串值,比如:Redis数据库里面,包含字符串值得健值对在底层得实现都是由SDS实现的注意:更正一下,buf在Redis中实质是一个字节数组,它里面保存的是二进制数据。SDS与C字符串的区别(1)常数O(1)复杂度获取字符串长度C字符串不记录自身的长度信息,获取字符串长度时会遍历字节数组,直到遇到空字符为止.复杂度为O(N)SDS直接通过len属性获取字符串长度.复杂度为O(1)(2)杜绝缓冲区溢出C字符串不记录自身长度,修改字符串时不会判断本身是否拥有足够的内存空间,当内存空间不足时,则会造成缓冲区的溢出.SDS对字符串进行修改时,先检查内存空间是否满足修改的需要,若不满足,则自动扩展SDS的内存空间.所以使用SDS既不需要手动修改内存空间的大小,也不会出现缓冲区溢出的情况.(3)空间预分配第一次创建字符串对象时,SDS不会分配冗余空间,即len=0,当SDS的API修改SDS时,则会为其分配冗余空间.当修改后的SDS的len属性小于1MB时,则为其分配和len同样大小的冗余空间,即free=len,此时buf[]的实际长度=len(实际长度)+free(冗余空间)+1(空字符)当修改后的SDS的len属性大于等于1MB时,则为其分配1MB的冗余空间.buf[]的实际长度=len(实际长度)+free(1MB)+1(空字符)(4)惰性空间释放SDS的API缩短SDS的字符串时,不会立即使用内存分配回收缩短后多出来的字节,而是记录在free属性中,并等待将来使用.(5)二进制安全C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。SDS的API都是二进制安全的。所有SDSAPI都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。1.2、embstr从Redis3.0版本开始字符串引入了EMBSTR编码方式,长度小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(3.2以前是39字节、3.2以后是44字节)的字符串将以EMBSTR方式存储。EMBSTR方式的意思是embeddedstring,字符串的空间将会和redisObject对象的空间一起分配,两者在同一个内存块中。Redis中内存分配使用的是jemalloc(内存分配器),jemalloc分配内存的时候是按照8,16,32,64作为chunk的单位进行分配的。为了保证采用这种编码方式的字符串能被jemalloc分配在同一个chunk中,该字符串长度不能超过64,故字符串长度限制OBJ_ENCODING_EMBSTR_SIZE_LIMIT我们就可以来计算一下在redis3.2版本以前structSDS{unsignedintcapacity;//4byteunsignedintlen;//4bytebyte[]content;//内联数组,长度为capacity}这里的unsignedint一个4字节,加起来是8字节.内存分配器jemalloc分配的内存如果超出了64个字节就认为是一个大字符串,就会用到raw编码。前面提到SDS结构体中的content的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用glibc的字符串处理函数,以及为了便于字符串的调试打印输出。所以我们还要减去1字节64byte-16byte-8byte-1byte=39byte在redis3.2版本之后这里unsignedint变成了uint8_t、uint16_t的形式,还加了一个int8flags标识,总共只用了3个字节的大小。相当于优化了sds的内存使用,相应的用于存储字符串的内存就会变大。然后进行计算:64byte-16byte-3byte-1byte=44byte。所以,redis3.2版本之后embstr最大能容纳的字符串长度是44,之前是39。长度变化的原因是SDS中内存的优化2、intint编码以整数的方式保存字符串数据,即使在写的时候加上了引号,Redis也会把这些数字当作整数类型来保存。0-10000之间的OBJ_ENCODING_INT编码的字符串对象将进行共享。注意:当整数的范围超过long了,还是会使用embstr或raw来保存。3、双向链表Redis中的双链表是这样式的:每个节点都是一个listNode,拥有前驱节点,后继节点和值。这就是C语言中的双向链表只要有多个节点就可以组成一个链表了,但是redis再在外面封装了一层,也就是使用adlist.h/list来实现,这样操作起来更加方便。链表结构比较简单,这里不多说了。4、ziplist(压缩列表)压缩列表。redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。Emtry结点的内部结构是这样的:元素的遍历先找到列表尾部元素:然后再根据ziplist节点元素中的previous_entry_length属性,来逐个遍历:连锁更新再次看看entry元素的结构,有一个previous_entry_length字段,他的长度要么都是1个字节,要么都是5个字节:前一节点的长度小于254字节,则previous_entry_length长度为1字节前一节点的长度大于254字节,则previous_entry_length长度为5字节5、哈希表哈希表的结构是这样的:蓝色部分很好理解,数组就是bucket,一般不同的key首先会定位到不同的bucket,若key重复,就用链表把冲突的key串起来。熟悉HashMap的同学应该不陌生,这个结构和HashMap的结构几乎一样,就连处理冲突的的方式都是采用一样的方式:拉链法。rehash再来看看哈希表总体图中左边橘黄色的部分,其中有两个关键的属性:ht和rehashidx。ht是一个数组,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx与哈希表的扩容有关,具体来说,ht[1]自相的是扩容后的hash表。rehash指的是重新计算键的哈希值和索引值,然后将键值对重排的过程。加载因子(loadfactor)=ht[0].used/ht[0].size。扩容和收缩标准扩容:没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于1。正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于5。收缩:加载因子小于0.1时,程序自动开始对哈希表进行收缩操作。扩容和收缩的数量扩容:第一个大于等于ht[0].used*2的2^n(2的n次方幂)。收缩:第一个大于等于ht[0].used的2^n(2的n次方幂)。扩容过程收缩过程渐进式rehash上面说到,扩容或者收缩哈希表时需要将ht[0]中的所有键值对迁移到ht[1]中,如果ht[0]中的数据量不是很大,几十几百甚至几万个,对于redis来说都不是大问题;但是,如果hash表中存储的键值对数量是几百万、几千万甚至上亿的数据,那么要一次性将这些键值对迁移到ht[1]中,庞大的数据可能会使redis服务器在一段时间内停止服务。因此为了避免rehash对服务器性能造成影响,服务器不会一次将ht[0]中的数据迁移到ht[1]中,而是分多次,渐进式的完成。以下是哈希表渐进式rehash的详细步骤:1)为ht[1]分配空间,让字段同事持有ht[0]和ht[1]两个hash表。2)在字典中维持一个索引计数器变量rehashidx,并将他的值设置为0,表示rehash开始。3)在rehash期间,每次对ht[0]字典执行添加、删除、查找或者更新操作时,程序除了执行指定操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对迁移到ht[1]上,完成后,rehashidx值+1。4)随着字典操作不断进行,最总在某个时间点上,ht[0]上的所有键值对都将会被rehash到ht[1],这是程序设置rehashidx为-1,表示rehash操作完成。渐进式rehash的好处在于它采用分而治之的方式,将rehash键值对所需的计算工作量均摊到了对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash带来的性能影响。渐进式refresh和下图中左边黄色的部分中的rehashidx密切相关:rehashidx的数值就是现在rehash的元素位置rehashidx等于-1的时候说明没有在进行refresh甚至在进行期间,每次对哈希表的增删改查操作,除了正常执行之外,还会顺带将ht[0]哈希表相关键值对rehash到ht[1]。已扩容过程为例:6、intset整数集合是集合键的底层实现方式之一。7、skiplist(跳表)skiplist是一种基于有序列表发展而来的数据数据结构,他可以支持平均O(logN),最坏O(N)的时间复杂度。大部分情况下,skiplist的效率可以和平衡树相媲美,而且skiplist的实现更加简单,只要你能熟练操作链表,就能轻松实现一个skiplist。有序表的搜索考虑一个有序表:从该有序表中搜索元素<23,43,59>,需要比较的次数分别为<2,4,6>,总共比较的次数为2+4+6=12次。有没有优化的算法吗?链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:这里我们把<14,34,50,72>提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:元素越多skiplist的优势就越明显skiplist跳表具有如下性质:(1)由很多层结构组成,支持平均O(logN),最坏O(N)的时间复杂度(2)每一层都是一个有序的链表(3)最底层(Level1)的链表包含所有元素(4)如果一个元素出现在Leveli的链表中,则它在Leveli之下的链表也都会出现。(5)每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。插入操作上图所示的skiplist有9个结点,一共4层,可以说是理想的跳跃表了,不过随着我们对跳跃表进行插入/删除结点的操作,那么跳跃表结点数就会改变,意味着跳跃表的层数也会动态改变。这里我们面临一个问题,就是新插入的结点应该跨越多少层?这个问题已经有大牛替我们解决好了,采取的策略是通过抛硬币来决定新插入结点跨越的层数:每次我们要插入一个结点的时候,就来抛硬币,如果抛出来的是正面,则继续抛,直到出现负面为止,统计这个过程中出现正面的次数,这个次数作为结点跨越的层数。例如,我们要插入结点3,4,通过抛硬币知道3,4跨越的层数分别为0,2(层数从0开始算),则插入后skiplist如下:删除操作解决了插入之后,我们来看看删除,删除就比较简单了,例如我们要删除4,那我们直接把4及其所跨越的层数删除就行了redis中把跳表抽象成如下所示:看这个图,左边黄色部分:header:跳表表头tail:跳表表尾level:层数最大的那个节点的层数length:跳表的长度右边蓝色部分:表头:是链表的哨兵节点,不记录主体数据。是个双向链表分值是有顺序的o1、o2、o3是节点所保存的成员,是一个指针,可以指向一个SDS值。层级高度最高是32。每次创建一个新的节点的时候,程序都会随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是“高度”二、Redis五种数据结构的实现Redis对象头结构体redis中并没有直接使用以上所说的各种数据结构来实现键值数据库,这里我们首先来了解一下Redis对象头结构体,所有的Redis对象都有下面的这个结构头:不同的对象具有不同的类型**type(4bit)**,同一个类型的type会有不同的存储形式encoding(4bit),为了记录对象的LRU信息,使用了24个bit来记录LRU信息。每个对象都有个引用计数refcount(4字节),当引用计数为零时,对象就会被销毁,内存被回收。*ptr指针将指向对象内容(body)的具体存储位置。这样一个RedisObject对象头就占用16字节的空间。1、字符串(string)字符串对象的编码可以是int、raw或者embstr。如果一个字符串的内容可以转换为int,那么该字符串就会被转换成为int类型,对象的ptr就会指向该int,并且对象类型也用int类型表示。普通的字符串有两种,embstr和raw。embstr应该是Redis3.0新增的数据结构,在2.8中是没有的。如果字符串对象的长度小于39字节,就用embstr对象。使用场景:缓存功能:字符串最经典的使用场景,redis做为缓存层,Mysql作为储存层,绝大部分请求数据都是redis中获取,由于redis具有支撑高并发特性,所以缓存通常能起到加速读写和降低后端压力的作用。计数器:许多运用都会使用redis作为计数的基础工具,他可以实现快速计数、查询缓存的功能,同时数据可以一步落地到其他的数据源。如:视频播放数系统就是使用redis作为视频播放数计数的基础组件。共享session:出于负载均衡的考虑,分布式服务会将用户信息的访问均衡到不同服务器上,用户刷新一次访问可能会需要重新登录,为避免这个问题可以用redis将用户session集中管理,在这种模式下只要保证redis的高可用和扩展性的,每次获取用户更新或查询登录信息都直接从redis中集中获取。限速:处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。2、listlist对象的编码可以是ziplist或者linkedlist。ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。当list对象同时满足以下两个条件时,列表对象使用ziplist编码:(1)列表对象保存的所有字符串元素的长度都小于64字节(2)列表对象保存的元素数量小于512个当有任一条件不满足时将会进行一次转码,使用linkedlist。使用场景:消息队列:redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端是用lupsh从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性3、hash哈希对象的底层实现可以是ziplist或者hashtable。zipList编码的哈希对象使用压缩列表作为底层实现,每当有新的健值对要加入到哈希对象时,程序会先将保存了key的压缩列表节点推入到压缩列表尾,然后再将保存了value的压缩节点推入到压缩列表尾,因此:保存了同一key:vlaue对的两个节点总是紧挨在一起,保存key的节点在前,保存value的节点在后,如下图所示:当对象数目不多且内容不大时,这种方式效率是很高的。此时redis会使用哈希表(hashtable)作为底层数据结构hashtable编码的哈希独享使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典健值对来保存。字典的每个键(key)和每个值(value)都是一个字符串对象,对象中保存了键或值。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:当键的个数小于hash-max-ziplist-entries(默认512)当所有值都小于hash-max-ziplist-value(默认64)使用场景:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷。所以常常用于用户信息等管理,但是哈希类型和关系型数据库有所不同,哈希类型是稀疏的,而关系型数据库是完全结构化的,关系型数据库可以做复杂的关系查询,而redis去模拟关系型复杂查询,开发困难,维护成本高。4、setset对象的编码可以是intset或者hashtable。intset是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:intset编码使用的条件:(1)集合对象保存的元素全部都是整数(2)集合对象保存的元素不超过512个不满足以上条件,集合对象需要使用hashtable注:第二个条件的上限可以修改,set-max-intset-entries默认值为512。表示如果entry的个数小于此值,则可以编码成REDIS_ENCODING_INTSET类型存储,节约内存。否则采用dict的形式存储。使用场景:标签(tag):集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及曾强用户粘度比较重要。(用户和标签的关系维护应该放在一个事物内执行,防止部分命令失败造成数据不一致)sadd=tagging(标签)生成随机数,比如抽奖:spop/srandmember=randomitemsadd+sinter=socialGraph(社交需求)5、zsetzset的编码有两种,一种是ziplist,另一种是skiplist与dict的结合。ziplist编码的压缩对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个原属则保存元素的分值(score)压缩列表内的集合原属按分值从小到大进行排序,分值较小的元素放置在靠近表头的方向,而分值较大的元素则放置在靠近表位的方向ziplist编码的使用条件:(1)有序集合保存的元素数量小于128个(2)有序集合保存的所有元素成员长度小于64字节上限值可以根据配置文件中的配置进行调整skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。它的结构比较特殊。使用场景:排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。参考【1】妖四灵.Shuen.Redis5种数据结构(底层原理,性能分析,使用场景.CSDN)【2】RyuGou.图解redis五种数据结构底层实现(动图版).今日头条【3】唐宋缘明卿_cris.redis五种数据结构的实现及使用场景.CSDN【4】Redis---String.码迷
LoveIT 2020-10-06Redis -
一文让你理解高并发缓存中的一致性Hash算法原理
一、从Web系统的演进说起单机时代在当今的互联网项目中,对于缓存的使用已经是”标配“了,我们开发一个平台刚开始访问量很小只需要一个缓存服务器就够用了(系统架构如下图所示)负载均衡随着系统的发展,访问量越来越大,这是我们的服务撑不住了,此时我们考虑给系统做负载均衡增加应用服务器来提高系统的并发量,此时缓存还够用,因此几个应用服务器共用一个缓存服务器就好了。分布式缓存可是好景不长,随着服务的时间越长,缓存的东西也越来越多,终于有一个天,缓存服务器撑不住了!此时我们考虑增加缓存服务器,并将缓存的数据拆分并缓存到不同的缓存服务器中。(下图中黄色背景的表示被拆分的数据)这样我们的系统就可以使用处理一定量的高并发和海量数据了。但是问题又来了,我们现在的系统有多个缓存服务器,我们的一个请求下来要到那个缓存服务器中去写\读数据呢?对于这个问题,有一个简单的方法就是使用数据的key的hashCode与缓存服务器结点个数取余。比如:现在有一个key="java"求hashCode:key.hashCode()=100求余:index=100%3==>index=1因此key="java"这个缓存数据就在第1个缓存服务器中现在系统改进的可以平稳运行了因对日常级别的大流量已经没有问题了,但是突然要搞活动,比如像双11这样的活动,系统的访问量邹增,为了系统能够平稳运行,这时我们考虑给系统增加配置,其中一项就是缓存服务器增加了缓存服务器之后,我们按照原来的对key的hashCode()取余的思路来计算一下:现在有一个key="java"求hashCode:key.hashCode()=100//hashCode()在一个系统不会改变取余:index=100%4==>index=0//由于缓存服务器结点增加了,因此问题就在这里这样一来,原本在第1个服务器中缓存的数据现在计算出来要去第0个服务器中拿,这就会引起错误通过上面的分析,我们就可以感受到,使用这种简单的hash算法就会导致增加或减少服务结点之后大部分结点之前的数据不可用,因此为了解决这个问题就出现了一致性hash算法,这个算法原本就是用来解决分布式缓存问题的。二、一致性hash算法背景 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点问题,初衷和CARP(CommonAccessRedundancyProtocol,共用地址冗余协议)十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT,DistributedHashTable)可以在P2P环境中真正得到应用。一致性hash算法提出了在动态变化(分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来)的环境中,判定哈希算法好坏的四个定义:平衡性(Balance)平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。单调性(Monotonicity)单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。分散性(Spread)在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。负载(Load)负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。三、一致性Hash算法1、一致性Hash算法基本原理其实,一致性哈希算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性哈希算法是对2^32取模,什么意思呢?我们慢慢聊。首先,我们把二的三十二次方想象成一个圆,一个整数就是一个点,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2^32个点组成的圆,示意图如下:圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32^-1,也就是说0点左侧的第一个点代表2^32^-1,我们把这个由2^32^个点组成的圆环称为hash环。有了这个Hash环之后,我们首先按照一定的规则把服务节点通过普通的hash算法把结点记录到hash环的结点上,如下图所示:之后对数据的key同样做hash运算,拿到key的hash值,那么这个key必然就会在hash环上有一个“落点”,我们不要求它能一次性命中缓存结点,只需要顺着落点顺时针遍历每一个结点,当遍历到下一个缓存结点时就把值发入这个缓存结点后从这个缓存结点获取值。当一个key顺着hash环遍历到2^32^-1时又从0开始遍历,知道找到下一个结点。示意图如下:了解了一致性Hash算法的基本工作原理之后,我们来看看它是如何解决结点动态改变二造成大规模结点数据不同用的问题的。我们还是模拟上面的情景,在活动开始之前给系统增加缓存服务器,添加后的示意图:此时我们可以按照Hash一致性算法的原理分析不难看出:添加结点后可能产生问题的地方就在node3到node4之间,因为这个区间的数原本应该是映射到node1的,但是现在都要映射到node4新节点上去了,不过相对于简单的hash取余,这个方法使得动态增加或减少结点之后数据的可用性提升了不少。2、一致性Hash算法改进至此问题基本解决,使用Hash一致性算法可以很轻松的解决分布式缓存带来的问题。然而,我们的分析还只是一种过于理想化的模型,即服务节点均匀的分布在Hash环的4周,假如结点没有均匀分布呢?我们来看看下面的示意图:当结点没有均匀分布时,一旦添加新的结点,仍然会有大部分数据会失效,这肯定不行啊!那么与上面办法解决这个问题呢?目前常用的解决办法就是使用虚拟结点来解决服务节点分布不均匀的问题,具体原理是:给每个真实的物理结点增加一组虚拟结点,将虚拟结点也放置到Hash环上,这样当一个key遍历到虚拟结点了也就表示找到了真实的物理结点。还是用示意图来说明:通过示意图可以直观的看出来,增加虚拟结点之后,我们人为的让有限的物理结点均匀分布了,这样一来当添加新结点之后受影响的数据很少。通过分析我们不难得出结论:对于每个物理节点对应的虚拟节点越多,各个物理节点之间的负载越均衡,新加入物理服务器对原有的物理服务器的影响越保持一致(这就是一致性Hash这个名称的由来)。那么在实践中,一台物理服务器虚拟为多少个虚拟服务器节点合适呢?太多会影响性能,太少又会导致负载不均衡,一般说来,经验值是150,当然根据集群规模和负载均衡的精度需求,这个值应该根据具体情况具体对待。参考资料:[1]https://www.bilibili.com/video/BV1yb411u7bz/?spm_id_from=trigger_reload[2]https://www.cnblogs.com/lpfuture/p/5796398.html[3]https://blog.csdn.net/zhaohong_bo/article/details/90519123?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase[4]https://blog.csdn.net/jiankunking/article/details/85111367
LoveIT 2020-06-21Redis -
单线程的Redis为什么高并发场景下还是很快
缓存在高并发的场景的作用不言而喻,号称高并发架构的基石,其中最为典型代表非Redis莫属。无论你是想面试通关,还是实战中用好Redis,理解Redis的设计精髓,就变得很重要了。今天主要分享Redis关于单线程以及高并发场景的核心设计。一、Redis到底有多快?Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的KV数据库Memcached差!有兴趣的可以参考官方的基准程序测试《HowfastisRedis?》(https://redis.io/topics/benchmarks),下图是Redi官方给出的一个在不同连接数下Redis吞吐量的曲线,横纵表示连接数,纵轴表示是吞吐量(q/s):这张图反映了一个数量级,希望大家在面试的时候可以正确的描述出来,不要问你的时候,你回答的数量级相差甚远!几个关键的数量级:最大QPS:100万+,30_0000连接:60_0000QPS,60_0000连接:50_0000万QPS一、Redis的高并发和快速原因(1)redis是基于内存的非关系型数据库,内存的读取速度非常快;(2)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;(3)最重要的原因是redis使用了IO多路复用模型,非阻塞IO,可以处理并发的连接。下面我们重点来了解一下Redis设计者把Redis设计成单线程以及IO多路复用技术的原理。二、为什么Redis要采用单线程的模式1、官方的解释官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。可以参考:https://redis.io/topics/faq并且在截图中我们也可以看到,Redis官方给我们建议:为了最大程度地利用CPU,您可以在同一框中启动多个Redis实例,并将它们视为不同的服务器。在某个时候,单个Redis可能还不够,因此,如果您要使用多个CPU,则可以开始考虑更早地分片的某种方法。因此,Redis采用线程的原因就是因为Redis目前在爱单线程下性能已经很高了,也就没必要着急着使用多线程了。不过在截图的最后官方表示在Redis未来的版本中会逐渐的使用上多线程,这一目标在Redis6版本中首次被实现了,感兴趣的小伙伴可以去了解一下。注意!这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis服务运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行....2、我的理解1)不需要各种锁的性能消耗Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。2)单线程多进程集群方案单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。所以单线程、多进程的集群不失为一个时髦的解决方案。3)CPU消耗采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU。但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。三、IO多路复用技术Redis采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。多路指的是多个socket连接,复用指的是复用一个线程。意思就是使用一个线程处理多个socket连接。Linux系统中IO多路复用提供了三个系统调用:select、poll、epoll。epoll是目前最先进的多路复用技术。select/poll/epoll核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。1、select机制函数定义如下:(1)基本原理select调用过程示意图:客户端在操作服务器的时候会产生三种文件描述符fd:writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞并分别监视这3类文件描述符,等有数据可读、可写、出异常或超时的时候就会返回。返回后通过遍历fd_set整个数组来找到就绪的描述符fd,然后进行对应的IO操作。(2)优点支持几乎所有的平台,跨平台性好。(3)缺点由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。每次调用select(),需要把fd集合从用户态拷贝到内核态,并进行遍历默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢2、poll机制和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。(1)基本原理基本原理与select一致,也是轮询+遍历;唯一的区别就是poll采用链表的方式替换select中的fd_set(数组)数据结构,而使其没有连接数的限制3、epoll机制函数定义如下:epoll_create&epoll_create1用于创建一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待可以执行IO操作的文件描述符直到超时。(1)基本原理epoll调用过程示意图:epoll也没有FD个数限制,用户态到内核态拷贝只需要一次,使用时间通知机制来触发。通过epoll_ctl()方法注册FD,一旦FD就绪就会通过回调机制来激活对应的FD,进行相关的IO操作。epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fdepoll_ctl()每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd,绑定一个callback函数epoll_wait()轮训所有的callback集合,并完成对应的IO操作(2)优点没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄效率提高,使用回调通知而不是轮询的方式,性能不会随着FD数目的增加效率下降内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)例子:100万个连接,里面有1万个连接是活跃,我们可以对比select、poll、epoll的性能表现 select:不修改宏定义默认是1024,则需要100w/1024=977个进程才可以支持100万连接,会使得CPU性能特别的差。 poll:没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。 epoll:请求进来时就创建fd并绑定一个callback,只需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝。参考资料[1]Redis、Zookeeper、Kafka、Nginx、Netty、Epoll、NIO、分布式、Hbase技术串讲[2]Redis为什么是单线程,高并发快的3大原因详解[3]为什么说Redis是单线程的以及Redis为什么这么快![4]https://zhuanlan.zhihu.com/p/115220699[5]https://blog.csdn.net/nanxiaotao/article/details/90612404
LoveIT 2020-06-20Redis -
Redis缓存和MySQL数据一致性解决方案探究
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。然而在工程技术领域并没有100%完美的解决方案,正如这里,读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。我们所说的一致性就是在要求:缓存不能读到脏数据缓存可能会读到过期数据,但要在可容忍时间内实现最终一致这个可容忍时间尽可能的小要想同时满足上面三条,可以采用读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。但是,串行化之后,就会导致系统的吞吐量会大幅度的降低,要用比正常情况下多几倍的机器去支撑线上请求。无脑增加机器肯定不是我们软件开发人员应该考虑的,所以,在这里,我们讨论三种常见方法:先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存在分析之前需要说明的是:如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,那就不要使用缓存方案来解决数据库的瓶颈问题,但是如果你的系统不是严格要求缓存和数据库必须保持一致性的话,那么可以尝试使用这里讨论的方案尝试解决缓存和数据库数据一致性问题。1、先更新数据库,再更新缓存这种方法是大家普遍反对的,原因集中在下面两点:从线程安全角度看:如果有同时两个线程A和B更新数据库,正常情况下我们希望A首先执行完毕,然后B再执行,但是由于网络抖动等原因,B先执行了更新,接着A又去更新,那么就会有如下情形:B更新了数据库------>B更新缓存---->A更新数据库------>A更新缓存这么操作加大了脏数据产生的可能性,因为A、B线程可能就是对同一个数据进行了更新,那么后发出B请求的数据就理论上就比A的新,因此按照这种策略解决缓存一致性不可取。从业务角度看:如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致数据压根还没读到,缓存就被频繁的更新,浪费性能。如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。相比之下,删除缓存更为适合。2、先删除缓存,再更新数据库该方案同样会导致不一致。同时有请求A和请求B进行更新操作,那么会出现:请求A进行写操作,检查发现缓存存在,于是首先了删除缓存请求B接下来查询发现缓存不存在请求B去数据库查询得到旧值请求B将旧值写入缓存请求A将新值写入数据库上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。解决方案:先删除缓存再写数据库休眠一定时间(例如1秒或200ms),再次删除缓存。这么做,可以将缓存脏数据再次删除。然而,这种方式回影响到系统的吞吐量,读入高并发场景不是特别适用。3、先更新数据库,再删除缓存这种方案是很多工程采用的方案,我们来看下是否一定安全。假设有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:缓存刚好失效请求A查询数据库,得一个旧值请求B将新值写入数据库请求B删除缓存请求A将查到的旧值写入缓存这样,脏数据就产生了,然而上面的情况是假设在数据库写请求比读请求还要快。实际上,工程中数据库的读操作的速度远快于写操作的。如果缓存删除失败了怎么办?答:启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。读取binlog后分析,利用消息队列,推送更新各台的redis缓存数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。阿里有一款开源框架—canal就是来做这个事情的,通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
LoveIT 2020-06-18Redis -
Redisson 实现分布式锁原理浅析
在上一文中分布式锁的三种实现方案总结了目前业界常见的实现分布式锁的三种方案:1、基于数据库表排他锁2、基于Redis的setNX命令+过期时间+lua脚本3、基于Zookeerper的临时结点。本文我们继续通过源码分析一下Redisson实现分布式锁的原理。回顾:Redis实现分布式锁主要步骤指定一个key作为锁标记,存入Redis中,并且指定一个唯一的用户标识作为value。当key不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足互斥性特性。给key设置一个过期时间,防止因系统异常导致没能删除这个key,满足防死锁特性。当处理完业务之后需要清除这个key来释放锁,清除key时需要校验value值,需要满足只有加锁的人才能释放锁。Redisson实现分布式锁下面从加锁机制、锁互斥机制、Watchdog机制、可重入加锁机制、锁释放机制等五个方面对Redisson实现分布式锁的底层原理进行分析。1、加锁原理Redisson加锁其实是通过一段lua脚本实现的,如下:我们可以把这一段lua脚本拿出来分析一下:在Redisson中,加锁需要以下三个参数:(1)KEYS[1]:需要加锁的key,这里需要是字符串类型。这个参数就是我们给Redisson加锁时传入的那个key,比如:(2)ARGV[1]:锁的超时时间,防止死锁。默认时间是30s,这个也可以在加锁的时候设置(3)ARGV[2]:锁的唯一标识,id(UUID.randomUUID())+“:”+threadId,比如:285475da-9152-4c83-822a-67ee2f116a79:52。通过这段脚本可以看到,Redsson在实现分布式锁的时候没有使用SETNX,而是使用了hincrby这个命令。命令语法:HINCRBYkeyfiledincrement这个命令可以为哈希表key中的域field的值加上增量increment。增量也可以为负数,相当于对给定域进行减法操作。如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。如果域field不存在,那么在执行命令前,域的值被初始化为0。上面这一段加锁的lua脚本的作用是:第一段if判断语句,就是用existsproduct命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。如何加锁呢?使用hincrby命令设置一个hash结构,类似于在Redis中使用下面的操作:接着会执行pexpiremyLock30000命令,设置myLock这个锁key的生存时间是30秒。到此为止,加锁完成。有的小伙伴可能此时就有疑问了,如果此时有第二个客户端请求加锁呢?这就是下面要说的锁互斥机制。2、锁互斥机制此时,如果客户端2来尝试加锁,会如何呢?首先,第一个if判断会执行existsproduct,发现product这个锁key已经存在了。接着第二个if判断,判断一下,product锁key的hash数据结构中,是否包含客户端2的ID,这里明显不是,因为那里包含的是客户端1的ID。所以,客户端2最后会执行:返回的一个数字,这个数字代表了product这个锁key的剩余生存时间。了解了上面这些知识点以后,接下来分析一下阻塞锁和非阻塞锁的逻辑。阻塞锁从lock()方法开始追溯源码,RedissonLock类中有的两个重载的方法分别来自java的Lock接口和Redsson的RLock接口,他们处理逻辑都在下面那个私有的lock()方法中Redisson阻塞锁上锁的逻辑:可以看到代码很长,但是只要你清楚如何使用Redis实现一把分布式锁,看懂应该不难。大致流程如下:当一个线程尝试获取锁时,首先会尝试调用tryAcquire()方法尝试获取锁,如果获取锁成功就会返回null(这个在lua脚本中可以看到,成功加锁会返后nil)。如果首次尝试获取锁失败,表示已经有别的线程在使用锁,那就会进入到自旋过程中,直到加锁成功返回null。非阻塞锁流程分析:尝试获取锁,返回null则说明加锁成功,返回一个数值,则说明已经存在该锁,ttl为锁的剩余存活时间。如果此时客户端2进程获取锁失败,那么使用客户端2的线程id(其实本质上就是进程id)通过Redis的channel订阅锁释放的事件。如果等待的过程中一直未等到锁的释放事件通知,当超过最大等待时间则获取锁失败,返回false,也就是第286行代码。如果等到了锁的释放事件的通知,则开始进入一个不断重试获取锁的循环。循环中每次都先试着获取锁,并得到已存在的锁的剩余存活时间。如果在重试中拿到了锁,则直接返回。如果锁当前还是被占用的,那么等待释放锁的消息,具体实现使用了JDK的信号量Semaphore来阻塞线程,当锁释放并发布释放锁的消息后,信号量的release()方法会被调用,此时被信号量阻塞的等待队列中的一个线程就可以继续尝试获取锁了。特别注意:以上过程存在一个细节,这里有必要说明一下,也是分布式锁的一个关键点:当锁正在被占用时,等待获取锁的进程并不是通过一个while(true)死循环去获取锁,而是利用了Redis的发布订阅机制,通过await方法阻塞等待锁的进程,有效的解决了无效的锁申请浪费资源的问题。3、Watchdog机制客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?Redisson提供了一个续期机制,只要客户端1一旦加锁成功,就会启动一个WatchDog。从以上源码我们看到leaseTime必须是-1才会开启WatchDog机制,也就是如果你想开启WatchDog机制必须使用默认的加锁时间为30s。如果你自己自定义时间,超过这个时间,锁就会自定释放,并不会延长。WatchDog机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔10秒(internalLockLeaseTime/3)检查一下,如果客户端1还持有锁key(判断客户端是否还持有key,其实就是遍历EXPIRATION_RENEWAL_MAP里面线程id然后根据线程id去Redis中查,如果存在就会延长key的时间),那么就会不断的延长锁key的生存时间。异步续命也是通过一段lua脚本实现的,lua脚本如下:注意:这里有一个细节问题,如果服务宕机了,WatchDog机制线程也就没有了,此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程就可以获取到锁。4、可重入加锁机制Redisson支持重入锁,比如下面的代码:我们再来分析一下那段加锁的lua脚本:第一个if判断肯定不成立,existsproduct会显示锁key已经存在。第二个if判断会成立,因为product的hash数据结构中包含的那个ID即客户端1的ID,此时就会执行可重入加锁的逻辑,使用:hincrbyproduct285475da-9152-4c83-822a-67ee2f116a79:521对客户端1的加锁次数加1。锁重入的逻辑演示如下:看到这里,小伙伴就应该明白了:Redisson分布式锁时使用Redishash结构的incrby命令来实现的,其中key表示锁的名字,filed表示客户端唯一标识(UUID:线程id),value表示锁重入次数,每重入一次就计数就加1。5、锁释放机制执行lock.unlock()就可以释放锁,我们来看一下释放锁的流程代码:大致流程:调用unlockInnerAsync异步释放锁关闭该线程的WatchDog,取消异步更新锁的过期时间上面得代码核心调用就是unlockInnerAsync,这个方法也是通过一段lua脚本实现实际解锁逻辑的:lua脚本的流程:判断要释放的锁标志key是否存在,如果不存在返回nil;如果要释放的锁标志key还存在,使用hincrbyKEYS[1]ARGV[3]-1减给锁重入次数减1如果锁的重入次数还是大于0就返回锁的过期时间;否者删除key,并且通过Redis的发布机制通知阻塞的进程去竞争锁从代码来看,释放锁的步骤也主要分三步:删除锁(这里注意可重入锁,在上面的脚本中有详细分析)。广播释放锁的消息,通知阻塞等待的进程(向通道名为redisson_lock__channelpublish一条UNLOCK_MESSAGE信息)。关闭这个线程WatchDog,即将RedissonLock.EXPIRATION_RENEWAL_MAP里面的线程id删除,并且cancel掉Netty的那个定时任务线程。方案优点Redisson通过WatchDog机制很好的解决了锁的续期问题。和Zookeeper相比较,Redisson基于Redis性能更高,适合对性能要求高的场景。通过Redisson实现分布式可重入锁,比原生的SETmylockuserIdNXPXmilliseconds+lua实现的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节。在等待申请锁资源的进程等待申请锁的实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。方案缺点使用Redisson实现分布式锁方案最大的问题就是如果你对某个RedisMaster实例完成了加锁,此时Master会异步复制给其对应的slave实例。但是这个过程中一旦Master宕机,主备切换,slave变为了Master。接着就会导致,客户端2来尝试加锁的时候,在新的Master上完成了加锁,而客户端1也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁,这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。所以这个就是RedisCluster或者说是RedisMaster-Slave架构的主从异步复制导致的Redis分布式锁的最大缺陷(在RedisMaster实例宕机的时候,可能导致多个客户端同时完成加锁)最后一张图总结:参考[1]https://zhuanlan.zhihu.com/p/135864820[2]https://juejin.im/post/5e828328f265da47cd355a5d#heading-6
LoveIT 2020-06-16Redis -
分布式锁的三种实现方案
一、为什么要使用分布式锁我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行!注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:上图我们可以看到,在集群环境下不同服务器中的变量是单独的JVM中用于一块独立的空间,如果仅仅使用Java内置的锁他只是JVM级别的锁,也即使说只有分布式服务里面,多个服务属于不同进程,用普通的同步锁会失效,因为不同进程里面,就没有什么内存共享之类的说法。因此为了保证一个方法或属性在高并发情况下的同一时间只能被一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的JavaAPI并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!二、分布式锁应该具备哪些条件分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些特性:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;2、高可用的获取锁与释放锁;3、高性能的获取锁与释放锁;4、具备可重入特性;5、具备锁失效机制,防止死锁;6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。三、分布式锁的三种实现方式以及Redis分布式锁的具体实现目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、**可用性(Availability)**和分区容错性(Partitiontolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行,当前业内公认的实现分布式锁有三套方案:基于数据库实现分布式锁基于缓存(Redis等)实现分布式锁基于Zookeeper实现分布式锁1、基于数据库实现分布式锁方案一:基于数据库表的实现要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。创建这样一张数据库表:当我们想要获取锁时,执行以下SQL:因为我们对method_name做了唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行临界区内容。当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:上面这种简单的实现有以下几个问题:1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。当然,我们也可以有其他方式解决上面的问题。针对数据库是单点问题可以搞数据库集群,数据之前双向同步。一旦挂掉快速切换到备库上。针对没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。针对非阻塞的?搞一个while循环,直到insert成功再返回成功。针对非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。方案二:基于数据库排他锁的实现除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作(伪代码):在查询语句后面增加forupdate,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:这种数据库分布式锁方案有效解决上面提到的无法释放锁和阻塞锁的问题。阻塞锁:forupdate语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。服务宕机自动释放锁:使用这种方式,服务宕机之后数据库会自己把锁释放掉。但是还是无法直接解决数据库单点可用性和可重入问题。方案总结总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。这两种方式各有各自的优缺点,但是我想说使用数据库实现分布式锁的方案虽然可行但是你要想想,我们的业务系统都搞成分布式的了,那就说明我们的业务系统已经有很大的流量了,这时候数据库的压力本生就很大了,此时在人为的给数据库增加压力,我个人认为这种方式可行但不可取!2、基于缓存(Redis等)实现分布式锁相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。目前有很多成熟的缓存产品,包括Redis,memcached等。这里我主要讲解一下使用Redis做分布式锁的方式——通过Redis的SETNXkeyvalue命令配合lua脚本保证命令执行的原子性。下面我们来一步步分析这其中的”坑“,并解决它们。首先按照基本想法我实现一个简单版本的如下:这个版本中很明显的有很多错误,首先一个可能的错误就是获取锁的线程在执行程序是突然发生异常了咋办?那么解决问题我们可以使用try-catch-finally来解决,更该版本如下:这般代码还不够健壮,因为如果在线程释放锁的时候突然停电了或者其他意外导致这个锁标志没法移除,那么这就是比较糟糕的,对于其他线程来说这个锁就不可用了。解决这个问题我们可以在设置锁的时候给锁设置一个过期时间,比如30s过期。这样一来就保证了锁的可用性,即使一个节点突然发生问题导致锁没有主动释放,那么也可通过Redis的超时机制来保护锁的可用性。由于我们目前这个分布式锁还只是一个非阻塞锁,也就是当线程没有获取到锁就立即返回失败,这在有些场景下是非常不友好的一种设计,因此我们需要把它改造成阻塞式锁。现在这把Redis实现的分布式锁足够安全了吗?我认为还不够!因为在释放锁的时候没有控制,任何线程都可以把这个key为“product”的锁标志删除了,这样看来,我们的这个锁还是不够安全。解决这个问题的方法是在设置key的时候向vlaue写一个唯一的随机值(比如UUID),在删除的时候需要比对key的值是否是在上锁的时候的值一样,如果一样才可以删除这个值,否者不允许删除。现在我们实现这把锁安全了吗?表面上看似安全了,但是仔细一想就会发现:我们的锁是有一个过期时间的,在比较繁杂的业务执行过程中执行业务的时间可能就会超过锁的过期时间,锁突然过期了这对于业务系统安全也是一种实实在在的威胁。解决这个问题的方法就是在主线程执行业务的时候再开一个线程当检测到锁快过期了就异步的为锁”续命“,直到主线程把任务执行完之后,异步线程死亡。这里我直接使用一个定时任务线程池,每当锁时间大约剩下1/3的时候就给锁续命,而后当执行完任务之后主线程关闭定死任务并释放锁,下一个线程就可以进来处理业务了。下面是执行截图:方案总结到这里,我们已经基本实现了redis分布式锁,Redis实现分布式锁主要步骤如下:指定一个key作为锁标记,存入Redis中,并且指定一个唯一的用户标识作为value。当key不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足互斥性特性。给key设置一个过期时间,防止因系统异常导致没能删除这个key,满足防死锁特性。当处理完业务之后需要清除这个key来释放锁,清除key时需要校验value值,需要满足只有加锁的人才能释放锁。3、利用zookeeper实现分布式锁让我们来回顾一下Zookeeper节点的概念:Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。Znode有可以分为四种类型:持久节点(PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在。持久节点顺序节点(PERSISTENT_SEQUENTIAL):所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号。临时节点(EPHEMERAL):和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。临时顺序节点(EPHEMERAL_SEQUENTIAL):临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。Zookeeper分布式锁的原理恰恰应用了临时顺序节点的特性,并且根据CAP理论,它是基于CP的一种分布式锁实现。具体实现原理和代码可以参考我的另一篇博文Zookeeper分布式锁的原理和具体实现参考链接[1]https://blog.csdn.net/wuzhiwei549/article/details/80692278[2]https://blog.csdn.net/weixin_42567141/article/details/103730590?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1[3]https://www.bilibili.com/video/BV1iE411b7Fv
LoveIT 2020-06-14Redis -
布隆过滤器(Bloom Filter)的原理和实现
海量数据处理以及缓存穿透这两个场景让我认识了布隆过滤器,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它!下面我们将分为几个方面来介绍布隆过滤器:什么是布隆过滤器?布隆过滤器的原理介绍。布隆过滤器使用场景。通过Java编程手动实现布隆过滤器。利用Google开源的Guava中自带的布隆过滤器。Redis中的布隆过滤器。一、什么是布隆过滤器?在学习任何东西之前我们都应该先去了解一下它是什么?能干什么?布隆过滤器(BloomFilter)是一个叫做Bloom的老哥于1970年提出的。布隆过滤器是一个神奇的数据结构,可以用来判断一个元素是否在一个集合中。很常用的一个功能是用来去重\过滤。布隆过滤器的本质是由一个位数组和一系列哈希函数组成的数据结构。所谓位数组,就是指数组中的每个元素只占用1bit,每个元素只能是0或1。因此相对于List、Set、Map等结构,位数组的显著优势就是占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。二、布隆过滤器的原理介绍。当一个元素添加进入布隆过滤器的时候户进行如下操作:使用K个哈希函数根据元素计算出K个hash值根据得到的hash值,将位数组中对应的下标值置为1举个例子,比如现在有一个布隆过滤器有3个哈希函数:hash1,hash2,hash3和一个位数组array,现在要把www.easyblog.top插入到布隆过滤器中,则有如下操作:对字符串进行三次hash计算,得到3个hash值:h1,h2,h3根据hash值将对应位数组中的下标值置为1:array[h1]=1,array[h2]=1,array[h3]=1当需要判断在布隆过滤器中是否存在某个关键字的时候,只需要对关键字再次hash,得到值之后判断位数组中的每个元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中。看不懂文字?没关系,让灵魂画手画给你看看到这里,我们就会发现一个问题:当插入的元素原来越多,位数组中被置为1的位置就越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为1。这样一个不存在布隆过滤器中的也有可能被误判成在布隆过滤器中。但是如果布隆过滤器判断说一个元素不在布隆过滤器中,那么这个值就一定不在布隆过滤器中。简单来说:布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。三、布隆过滤器使用场景。判断给定数据是否存在:比如判断一个数字是否在于包含海量数字的数字集中(数字集很大,亿级以上!)防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)邮箱的垃圾邮件过滤、黑名单功能等等。去重:比如爬给定网址的时候对已经爬取过的URL去重。四、通过Java编程手动实现布隆过滤器。上面我们了解布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。根据原理我们向手动实现一个布隆过滤器的话,我们需要考虑:1、需要K个哈希函数2、一个合适大小的位数组3、添加元素到位数组(布隆过滤器)的方法4、判断关键字在位数组(布隆过滤器)中是否存在如果明白了原理实现起来还是比较简单的,实现如下:测试执行结果:布隆过滤器会有一定的误判,它说一个元素在其内部存在不一定存在,但是如果他说不能存在,那这个元素就一定不存在。五、使用Google开源的Guava中自带的布隆过滤器。自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。首先我们需要在项目中引入Guava的依赖:使用方法如下:我们创建了一个最多存放10000个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)执行结果:在我们的示例中,当mightContain()方法返回true时,我们可以99%确定该元素在过滤器中,当过滤器返回false时,我们可以100%确定该元素不存在于过滤器中。Guava提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用,而现在互联网一般都是分布式的场景。为了解决这个问题,我们可以使用Redis实现的布隆过滤器。六、Redis中的布隆过滤器。Redis4.0之后有了Module(模块/插件)功能,RedisModules让Redis可以使用外部模块扩展其功能。布隆过滤器就是其中的Module。详情可以查看Redis官方对RedisModules的介绍:https://redis.io/modules。RedisBloom提供了多种语言的客户端支持,包括:Python、Java、JavaScript和PHP。6.1、使用Docker启动RedisBloom可以参考dockerhub上给出的示例:https://hub.docker.com/r/redislabs/rebloom/第一次上面命令Redis会从Docker仓库下载RedisBloom。启动成功之后进入容器的交互界面RedisBloom常用命令注意:key表示布隆过滤器名,value是添加的元素(1)BF.ADD{key}{value}:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。(2)BF.MADD{key}{value}[value...]:将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。(3)BF.EXISTS{key}{value}:确定元素是否在布隆过滤器中存在。(4)BF.MEXITS{key}{vlaue}[value...]:确定一个或者多个元素是否在布隆过滤器中存在。(5)BF.RESERVE{key}{error_rate}{capacity}[EXPANSIONexpansion]:创建一个布隆过滤器。key:表示布隆过滤器的名字error_rate:表示误报的期望概率,这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。capacity:过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。expansion:可选参数。如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以expansion。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍使用示例:添加元素到布隆过滤器,添加成功返回1,失败返回0批量添加元素到布隆过滤器判断元素在布隆过滤器中是否存在,存在返回1,否者返回0批量判断元素在布隆过滤器中是否存在6.2整合SpringBoot使用详情请参考我在GitHub上的Demohttps://github.com/LoverITer/redis-blooomFilter参考链接[1]https://segmentfault.com/a/1190000021194652[2]https://segmentfault.com/a/1190000016721700[3]https://blog.csdn.net/lifetragedy/article/details/103945885
LoveIT 2020-06-11Redis -
秒懂,Redis缓存穿透、缓存击穿、缓存雪崩概念以及应对策
如上图所示,我们在应用程序和Mysql数据库中建立一个中间层,即Redis缓存。通过Redis缓存可以有效减少查询数据库的时间消耗,这极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求极高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存击穿和缓存雪崩。本文将简单介绍缓存穿透、缓存雪崩和缓存击穿这三者之间的区别以及这三类问题的解决方法。一、缓存穿透缓存穿透是指用户访问了不存在的数据,导致缓存无法命中,大量的请求都要穿透到数据库进行查询,从而使得数据库压力过大,甚至挂掉。比如:数据库使用了id为正整数作为键,但是黑客使用负整数向服务器发起请求,这时所有的请求都没有在缓存中命中,从而导致大量请求数据库,如果超过了数据库的承载能力,会导致数据库服务器宏机。一般解决缓存穿透的方法有:(1)缓存空对象这是一个简单粗暴的方法,方法是如果一个查询返回的结果是空的,仍然把这个空结果进行缓存,这样的话缓存层就存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个比较短的过期时间,让其自动过期。(2)使用布隆过滤器拦截这是一种常见而且有效的策略,它将所有可能存在的数据哈希到一个足够大的bitmap中,当查询一个不存在的key时会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。Redis实现了布隆过滤器,我们可以直接使用来达到过滤的目的。具体的原理和使用方法可以参考我的另一篇博文布隆过滤器(BloomFilter)的原理和实现二、缓存击穿(缓存并发)缓存击穿,是指当某个key在过期的瞬间,有大量的请求并发访问过期的键,这类数据一般是热点数据,由于缓存过期了,会同时访问数据库来查询数据,并写回缓存,从而导致数据库瞬间压力过大。缓存击穿的解决方法也有两个:(1)设置热点数据永不过期当遇到这种情况的时候,数据库很难扛下来这么大的并发。最简单的方法就是将热点数据缓存永不过期就好了。(2)使用互斥锁业界比较常用的方法,使用锁控制访问后盾服务的并发量。分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其线程没有获取到分布式锁等待就好了。这种方式将高并发的压力转移到了分布式锁,因此对系统的分布式锁是否合格考验很大。关于分布式锁内容可以参考我的另一篇博文分布式锁的三种实现方案本地锁:与分布式锁的作用类似,我们通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程去数据库中查询,如果一个服务有多个节点,则还会有多个数据库查询操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发的问题。基于Redis实现的分布式锁伪代码:三、缓存雪崩缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询同一时刻都落到了数据库上,造成了数据库压力过大。缓存雪崩与缓存击穿的区别在于这里针对很多key同时失效,前者则是针对某一个热点key失效解决方案:(1)不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀(2)在缓失效后,通过分布式锁或者分布式队列的方式控制数据库写缓存的线程数。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(和缓存击穿中使用互斥锁类似)(3)如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。随机过期时间伪代码:解释:在同一分类中的key,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。这种情况就应该考虑使用主备缓存策略了。最后再介绍几个有关缓存的概念缓存预热缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!解决思路:1、直接写个缓存刷新页面,上线时手工操作下;2、数据量不大,可以在项目启动的时候自动进行加载;3、定时刷新缓存;缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:(1)定时去清理过期的缓存;(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。总结本文介绍了缓存应用中比较典型的是三个问题:缓存穿透,缓存击穿和缓存雪崩的概念和基本的解决方法。最后还介绍了缓存预热、缓存更新以及缓存降级等概念。在缓存的应用中还有一个比较典型的问题就是如何保证数据的一致性问题,也欢迎大家参考我的另一篇博文Redis缓存和MySQL数据一致性方案详解参考[1]https://www.pieruo.com/13549.html[2]https://www.cnblogs.com/midoujava/p/11277096.html
LoveIT 2020-06-09Redis -
Redis的Java客户端—Jedis和Lettuce
Jedis连接Redis1.添加Jedis依赖2.在虚拟机端配置:将bind注释掉,然后改protected-mode为no改了之后保存并重启Redis。3.使用Jedis提供的Jedis这个工具类来连接Jedis,首先在虚拟机使用ifconfig命令查看虚拟机的ip,然后向Redis发送一个ping命令,测试一下是否可以连接上远程的Redis:可以连接上Redis的标志是程序运行后打印”PONG“如果程序运行出现了JedisConnectionException,这种情况要么是你代码中把ip或端口写错了,要不就是由于Linux的防火墙导致的,在你确定你没有写错ip或端口的前提下,你可以直接关闭防火墙或者为了安全你可以开放6379这个端口给远程,Centos7上开放端口有关的命令操作如下:Jedis常用APIJedis操作Redis的常用API几乎和Redis的命令是一样的,比如操作String:可以看到通过Jedis操作Redis所调用的API和Redis的命令是一样的,所以只要熟悉Redis的关于5大常用数据类型的命令,那么使用Jedis操作Redis就没有大的问题。如果你还不是熟悉Redis的关于5大常用数据类型的命令,可以参考我的这篇笔记:Redis五大常用数据类型Jedis事务Redis中和事务有关的命令:mulit、exec,discard,watch和unwatch,然而Jedis中操作Redis事务的API也和这几个命令是一样的,比如我们实现一个简单的事务:有两个关键字balance表示信用卡的余额(初始值为1000),debt表示信用卡的欠额(初始值为0),使用redis提供的乐观锁watch来实现对消费的记录正常情况下(就是没有别的线程干扰):异常情况下:解释一下这两种不同的结果:当开启监控后,如果期间别的线程把监控的关键字的值改变了,那么Redis就会在本次事务期间不执行任何操作,即使使用exec提交事务了,也不会执行(这时返回exec的返回值是null),这种基于CAS的监控,不仅保证了共享数据的安全,而且还提高了响应速速。这也正是程序所体现的,当jedis.mset("balance","100","debt","400");这条语句被注释掉以后,程序可以正常执行,执行后返回true,程序结束;当jedis.mset("balance","100","debt","400");语句没有注释以后,在开启watch以后,相当于别的进程改变了监控关键字的值,那么这时Redis就不会在执行事务了,exec就会返回false,然后while(!txTest.coustmer());就又再次调用方法,直到执行成功,然后结束程序。JedisPool连接池类似于mysql的数据库连接池c3p0、Durid等,JedisPool是java连接Redis的连接池,基本的使用方式如下:一般我们可以各种配置的代码抽取出来写一个工具类,下面是一个基于单例模式的JedisPoolUtils:首先我们需要一个redis.properties的配置文件,用于配置JedisPool的一些属性:JedisPoolUtils.java然后测试一下我们的JedisPoolUtils工具类:测试结果:SpringBoot连接Redis导入redis的相关依赖在application.yml中配置redis的有关连接信息自定义RedisTemplate SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。我们可以使用RedisTemplate来像Jedis一样操作Redis。但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。 看到这个@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了,这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置RedisTemplate。RedisConfig.java
LoveIT 2019-10-14Redis -
Redis主从复制的几种形式和原理详解
主从复制概述 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(salve)。数据的复制是单向的,只能从主节点到从结点。 默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从结点(或没有从结点),但是一个从结点只能有一个主节点。主从复制的作用数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。读写分离:可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;高可用的基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。主从复制启用前面提到过,默认情况下每台Redis服务器都是主节点(master),而且如果没有配置的话,是没有从服务器的。(可以使用inforeplication命令查看一个Redis服务器的复制有关的信息)有三种方式可以开启主从:配置文件:在从服务器的配置文件中加入slaveof<主节点ip><主节点port>。启动命令:redis-server启动命令后面加入--slaveof<主节点ip><主节点port>。客户端命令:Redis服务器启动后,直接通过客户端执行命令slaveof<主节点ip><主节点port>,返回OK后该Redis实例就成为了从节点。Redis常用的主从拓扑1主N从所谓的1主N从指的是一个主Redis服务器(master)可以有一个或多个从Redis服务器(salve),这种拓扑关系的特点是:只有一个主节点,有一个或多个从结点;主节点可读可写,从结点只能从主节点读数据,不能自己写数据;当主服务节点宕机后(无论各种原因,反正主节点不能正常运行了),从服务节点不会自动变成主节点,而是保持自己从结点的身份继续运行(而且他从主节点复制的数据不会丢失,可一继续对外提供服务),直到主节点恢复后这些从结点又可一继续从主节点读数据;当一以从服务器结点“挂掉”以后,再次重启后,他与先前的主节点没有任何关系了(在没有在配置文件中配置的前提下),除非在配置文件中配置过或者再次使用命令slaveof<主节点ip><主节点port>连上主节点。演示一:在客户端使用命令行在127.0.0.1:3679开启master,然后在127.0.0.1:3680/3681开启两个salve然后关闭master节点,查看从服务节点,发现从服务节点没有自动升级为master,并且他之前从主节点复制来的数据还在,还可以向外提供服务:之后重新启动master,查看从服务节点,发现从服务节点有重新连接上主服务节点了:演示二:恢复到127.0.0.1:3679是master,127.0.0.1:3680/3681是127.0.0.1:3679的两个salve的状态,然后任意重启一个从服务器,观察发现这个服务器结点如果之前没有在配置文件中配置过,那么他将和master没有任何关系了:"薪火相传"所谓”薪火相传“,指的是那种一个master连接了一个slave,然后这个slave结点有作为另一个slave的master结点.......依次向链表一样传递下去,这种拓扑的特点是:只有这个传递链上的第一个master结点具有写的权限,其他的结点都是由读的权限;这种模式下,减轻了master结点的压力,但是与之而来的问题是越往后的结点同步延时越大;如果其中一个节点“挂了”,那么他后面的结点就无法同步到最新的数据了演示:让127.0.0.1:3679作为127.0.0.1:3680的master,然后让127.0.0.1:3680作为127.0.0.1:3681的master:"反客为主"“反客为主”说的就是当主服务器结点“挂了”以后,可以手动将一个从服务器节点指定为主服务器节点,然后让其他的从服务节点从这个新的master上复制:演示:在客户端使用命令行在127.0.0.1:3679开启master,然后在127.0.0.1:3680/3681开启两个salve然后主服务节点突然“挂了”,手动使用命令slaveofnoone将原本的slave转成master,停止与其他数据库的同步,然后将其他的slave和这个新的master交互:这种模式下,在之后以前的主服务器再次启动后,它就与这个新建立的主从关系没有任何关系了。Redis哨兵模式 Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis的高可用性解决方案。Sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下: Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-Slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。Sentinel由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。说的简单点,哨兵模式就是监控+自动版“反客为主”比如下图的过程:演示:首先我们在新建一个sentinel.conf配置文件,配置有关哨兵监控的信息,然后保存退出让127.0.0.1:3679作为127.0.0.1:3680的master,然后让127.0.0.1:3680作为127.0.0.1:3681的master一切设置好后,使用redis-sentinelsentinel.conf配置文件路径启动哨兵,让他监控master的状态:之后关闭master,模拟服务器突然宕机等情况,发现哨兵自动通过投票选举出了新的master,并且把其他从服务器(slave)都拉到了这个新的master“旗下”:那么如果之前的master重启回来,会不会有两个master冲突?答案是不会,之前的master会在哨兵模式下变为slave从机:主从复制的原理Redis主从复制的过程大体可以分成3个阶段:建立连接阶段、数据同步阶段和命令传播阶段。建立连接step1:保存主节点信息从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。(用inforeplication命令就可以查看)slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。step2:建立socket连接 从节点(slave)每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功:*从节点(slave):为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。主节点(master):接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。step3:发送ping命令 从节点(slave)成为主节点(master)客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。从节点发送ping命令后,可能出现3种情况:*(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。step4:身份验证如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。step5:发送从节点端口信息身份验证之后,从节点会向主节点发送其监听的端口号,主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行infoReplication时显示以外,没有其他作用。数据同步阶段 主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。redis同步有2个命令:sync和psync,前者是redis2.8之前的同步命令,后者是redis2.8为了优化sync新设计的命令。 数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。 在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。全量复制和增量复制 在Redis2.8以前,从结点向主节点发送的是sync命令同步数据的,这种同步方式是全量复制;但是在Redis2.8以后,从结点可以发送psync命令请求同步数据,此时根据主节点当前状态的不同,同步方式可能是全量复制和增量服饰:*全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,这是一个非常耗费资源的操作。流程如下:发送psync命令(spync?-1)主节点根据命令返回FULLRESYNC从节点记录主节点ID和offset主节点bgsave并保存RDB到本地主节点发送RBD文件到从节点从节点收到RDB文件并加载到内存中主节点在从节点接受数据的期间,将新数据保存到“复制客户端缓冲区”,当从节点加载RDB完毕,再发送过去。(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)从节点清空数据后加载RDB文件,如果RDB文件很大,这一步操作仍然耗时,如果此时客户端访问,将导致数据不一致,可以使用配置slave-server-stale-data关闭.从节点成功加载完RBD后,如果开启了AOF,会立刻做bgrewriteaof。以上红色字体的部分是整个全量同步耗时的地方。增量复制:当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将复制缓冲区的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行增量复制,仍使用全量复制。当从节点出现网络中断,超过了repl-timeout时间,主节点就会中断复制连接。主节点会将请求的数据写入到“复制积压缓冲区”,默认1MB。当从节点恢复,重新连接上主节点,从节点会将offset和主节点id发送到主节点。主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送cuntinue响应—表示可以进行部分复制。主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态。命令传播阶段 数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONFACK。心跳机制主从节点在建立复制后,他们之间维护着长连接并彼此发送心跳命令。心跳的关键机制如下:主从都有心跳检测机制,各自模拟成对方的客户端进行通信,通过clientlist命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点的连接状态是flags=S。主节点默认每隔10秒对从节点发送ping命令,可修改配置repl-ping-slave-period控制发送频率。从节点在主线程每隔一秒发送replconfack{offset}命令,给主节点上报自身当前的复制偏移量。主节点收到replconf信息后,判断从节点超时时间,如果超过repl-timeout60秒,则判断节点下线。注意事项: 延迟与不一致:命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。 repl-disable-tcp-nodelayno:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
LoveIT 2019-10-12Redis -
Redis的事务控制
事务的基本概念事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发来的命令请求打断。事务是一个原子操作:事务中的命令要么全部执行,要么全部不执行。Redis事务相关的几个命令multi:MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。exec:执行所有的事物命令,EXEC命令的回复是一个数组,数组中的每个元素都是执行事务中的命令所产生的回复。其中,回复元素的先后顺序和命令发送的先后顺序一致。当客户端处于事务状态时,所有传入的命令都会返回一个内容为QUEUED的状态回复(statusreply),这些被入队的命令将在EXEC命令被调用时执行。discard:通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务。watchkey[key...]:监视一个或多个key(类似于乐观锁)unwatch:取消watch对所有key的监视Redis事务的三个阶段开始事务:使用multi命令开启一个事务,当一个事务被exec或discard后,改事务就宣告结束(无论有没有成功执行),下次在向开启事务就必须在使用这个命令开启事务。命令入队:简单点说就是,开启事务后输入的命令不会立即执行,而是先入队,执行当exec后在一次性执行。执行事务:使用exec命令执行事务Redis事务使用示例:1、正常执行2、取消事务3、事务在执行EXEC之前,入队的命令可能会出错(语法上就是错误的),执行exec时,整个事务都会失败。 对于发生在EXEC执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回QUEUED,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。 不过,从Redis2.6.5开始,服务器会对命令入队失败的情况进行记录,并在客户端调用EXEC命令时,拒绝执行并自动放弃这个事务。4、事命令可能在EXEC调用之后失败(语法上没有错误,但是调用执行的时候出错了),在执行exec命令时,其他正确的命令可以正确执行,错误命令抛出错误为什么Redis不支持事务回滚?通过上面的案例我们可以看到redis在事务中发生错误后是没有回滚的,而是继续执行余下的命令,那么redis为什么不支持事务回滚呢?从各方面考虑有以下两点原因:Redis命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,也就是说,从实用的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发过程中别发现,而不应出现在生产环境中。而且需要注意的是在通常情况下,回滚并不能解决编程错误带来的问题。因为redis不需要支持事务回滚,所以他可以在内部保持简单和快捷。5、使用watch监控WATCH命令可以为Redis事务提供check-and-set(CAS)行为。被WATCH的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在EXEC执行之前被修改了,那么整个事务都会被取消,EXEC返回nil-reply来表示事务已经失败。下面使用很典型的账户和消费问题来展示一下watch的作用。案例一:使用watch检测balance,事务期间balance数据未变动,事务执行成功案例二:使用watch检测balance,在开启事务后(标注1处),在新窗口执行标注2中的操作,更改balance的值,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行标注1后命令,执行EXEC后,事务未成功执行。
LoveIT 2019-10-11Redis -
神秘的Redis持久化机制
Redis与传统数据的一个主要区别就是Redis将所有的数据都存储在内存中,而传统数据库通常只会把数据存储到磁盘上,这就使得Redis的数据存储和读取有着极快的速度,但是由于内存属于易失存储器,它记录的所有数据一旦断电就消失了,这对于想把Redis作为数据库而不仅仅是缓存的用户来说是不能容忍的。为了满足不同场景下的持久化需求,Redis提供了RDB(RedisDataBase)持久化、AOF(AppendOnlyFile)持久化和RDB-AOF混和持久化等多种持久化机制。如果用户不需要持久化,也可以完全关闭持久化功能。一、RDB(RedisDataBase)持久化RDB持久化是Redis默认使用的持久化方式,它是将当前Redis进程中的数据生成快照保存到硬盘,默认会在当前工作目录下生成dump.rdb文件,当Redis重启的时候会自动读取快照文件来恢复数据。Redis提供了多种创建RDB文件的方法,用户既可以使用SAVE命令或BGSAVE命令手动创建RDB文件,也可以在redis.conf配置文件中设置save来配置选项让服务器在满足指定条件后自动触发BGSAVE命令。1、手动输入命令创建RDB文件SAVE命令和BGSAVE命令都可以生成RDB文件。SAVE和BGSAVE的区别是:SAVE:接收到SAVE命令后,Redis服务器将遍历数据库包含所有数据库,并将各个数据库包含的键值对全部记录到RDB文件中。在SAVE命令执行期间,Redis服务器将阻塞,直到RDB文件创建完毕为止。如果在创建RDB文件时已经有了对应的RDB文件,那么服务器将会新创建RDB文件代替已有的RD文件。BGSAVE:BGSAVE是SAVE命令的异步版本,当Redis服务器收到BGSAVE命令是,将会执行以下操作:1)创建一个子进程(拷贝一份父进程)。2)子进程执行SAVE命令,创建RDB文件。3)RDB文件创建完毕之后,子进程退出并通知Redis服务器进程(父进程)RDB文件已经创建完毕。一般来说,在生产环境很少直接使用SAVE命令,因为它会阻塞Redis服务器进程,保存RDB文件的任务通常由BGSAVE命令异步地执行。然而,如果负责保存数据的后台子进程不幸出现问题时,SAVE可以作为保存数据的最后手段来使用。2、通过配置选项自动创建RDB文件用户除了可以使用SAVE命令和BGSAVE命令手动创建RDB文件之外,还可以通过在配置文件中设置save选项,让Redis服务器在满足指定条件后自动触发BGSAVE命令:save<seconds><changes>save选项接收两个参数:seconds和changes,即服务器的数据只要在指定的seconds秒内只要发生了changes次变化后,就会触发BESAVE。Redis默认的配置有三个:save9001//900秒内Redis数据发生了至少1次变化,则执行bgsavesave30010//300秒内Redis数据发生了至少10次变化,则执行bgsavesave6010000//60秒内Redis数据发生了至少10000次变化,则执行bgsave注意,为了避免由于同时使用多个触发条件而导致服务器频繁的执行BGSAVE命令,Redis服务器会在每次成功创建RDB文件之后将负责自动触发BGSAVE命令的时间计数器清0并重新开始计数:无论这个RDB文件是由自动触发的BGSAVE命令创建的,还是有用户执行的SAVE或BGSAVE命令创建的,都是如此。3、自动触发的原理Redis的自动触发是通过serverCron函数、dirty计数器和lastave时间戳来实现的。serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查save<seconds><changes>配置的条件是否满足,如果满足就执行bgsave。dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。也就是每隔100ms,执行serverCron函数;在serverCron函数中,遍历save<seconds><changes>配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save<seconds><changes>条件,只有下面两条同时满足时才算满足: (1)当前时间-lastsave>seconds (2)dirty>=changes4、其他自动触发机制除了在配置文件中配置save<seconds><changes>来触发BGSAVE以外,还有别的情况会触发BGSAVE:在主从复制场景下,如果从结点执行全量复制操作,则主结点会执行BGSAVE命令,并将dump.rdb文件发送给从结点。在执行shutdown命令时,会自动执行RDB持久化,这一点通过redis的日志看到5、RDB文件结构RDB的文件格式RDB文件是经过压缩的二进制文件,RDB的的文件格式如下图所示其中重要字段的含义说明如下:Redis文件标识符:文件最开头是RDB文件标识符,这个标识符内容为"REDIS"5个字符。Redis会在尝试加载RDB文件的时候通过标识符快速判断这是不是一个RDB文件。版本号:跟在RDB文件标识符后面的是RDB文件的版本号,这个版本号是一个字符串数字,长度为4个字符。比如Redis5.0的RDB版本号是"0009",不同版本的RDB文件结构会有些许不同,但是新版的RDB都会在旧版本上添加更多信息,而且新版的Redis服务器总是能够向下兼容旧版的Redis服务器生成的RDB文件。比如在Redis5.0的服务器可以正常读取Redis4.0生成的"0008"版本的RDB文件。数据库数据:SELECTDB0pairs表示一个完整的数据库(0号数据库),同理SELECTDB3pairs表示3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。EOF:常量,标志RDB文件正文内容结束,他的实际值为二进制0xFF。当Redis读取到EOF,它就知道RDB文件的正文部分已经全部读取完毕了。CRC64校验和:RDB文件的末尾是一个一无符号64整数表示的CRC64校验和,在载入RBD文件时,会重新计算校验和并与CRC64值比较,判断文件是否损坏。RDB文件的存储路径RDB文件的存储路径既可以在redis.conf配置文件中配置,也可以在客户端通过命令动态设定:在配置文件中可以设置dir来指定RDB文件的存放路径,redis默认是存放在当前工作目录下。也可以在配置文件中通过设置dbfilename指定RDB文件的名字,redis默认的文件名是dump。动态设定:Redis启动后也可以在客户端使用configsetdir/path来动态的改变RDB的存放路径,当然也可以通过configsetdbfilenamenewfilenaem来设置RDB文件的名字。RDB文件的压缩Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩会有一定的性能消耗,但是这样可以大大减小RDB文件的大小。但是需要特别注意的是:RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符进行的,且只有在字符串达到一定长度(20字节)时才会进行压缩。6、载入RDB文件RDB文件的载入工作是在服务器启动的时候自动进行的,并没有专门的命令。但是当开启AOF后,由于AOF文件的优先级更高,Redis会优先加载AOF文件来恢复数据,只有当AOF关闭时,才会在Redis服务器启动的时候检测RDB文件,并自动加载。服务器载入RDB文件期间处于阻塞状态,直到加载完毕阻塞解除。Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。具体步骤如下:首先在工作目录中寻找是否有RDB文件出现,如果有就打开它,,然后读取文件的内容并执行以下载入操作1)检查文件开头的表示符是否是"REDIS",如果是则据需后续载入操作,不是则抛出错误终止载入操作。2)检查RDB文件版本号,判断当前Redis服务器能否读取这一版本的RDB文件。3)根据RDB文件中记录的设备附加信息,执行相应的操作和设置。4)检查数据库数据部分是否为空,如果不为空着执行以下操作(1)根据文件记录的数据库号码,切换到对应数据库。(2)根据文件记录的键值对总数量以及带有过期时间的监视对数量,设置数据库底层数据结构。(3)一个接一个的载入文件记录的所有键值对数据,并在数据库中重建这些键值对。5)如果服务器启动了复制功能,那么将之前缓存的Lua脚本重新载入缓存中。6)遇到EOF结束标记符,确认RDB文件已经全部读取完毕。7)载入CRC64校验和,把它和载入数据期间计算出的CRC64比对,以此判断被载入的数据是否完好。8)RDB文件载入完毕,等待客户端的请求。7、RDB优缺点总结最后以一幅图的方式总结RDB的优缺点:二、AOF(AppendOnlyFile)持久化方式与全量式的RDB持久化功能不同,AOF提供的是增量式持久化功能,这种持久化的核心原理在于:服务器每次执行完写命令之后,都会将命令追加到AOF文件尾部。这样一来服务器停机之后,只要重新执行AOF文件中保存的Redis命令,就可以将数据恢复值停机之前的状态。与RDB相比较AOF具有更好的实时性,也是当前主流的持久化方案1、开启AOF持久化功能Redis服务器默认只开启了RDB持久化方式,要开启AOF,需要在redis.conf中修改appendonly为yes,并且还可以在配置文件中修改AOF文件的名字等等,具体的可以参考我的这篇笔记:Redis配置文件redis.conf详解2、AOF的执行流程AOF的执行流程包括:命令追加(append):将Redis的写操作追加到缓冲区aof_buf文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步带硬盘文件重写(rewrite):当AOF文件过大的时候重写AOF文件,达到压缩的目的。命令追加(append)Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点。在AOF文件中,除了用于指定数据库的select命令(如select0为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。文件写入(write)和文件同步(sync)Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数:write函数:为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失fsync函数:为了解决write函数数据丢失的问题,因此系统提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。为了消除操作系统的写缓存机制带来的不确定性,Redis向用户提供了appendfsync选项,以此来控制系统写AOF的频率:appendfsync<value>appendfsync选项拥有always、no、everysec3个值可选,他们代表的含义分别为:always命令写入aof_buf后立即调用系统的fsync函数同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。no命令写入aof_buf后调用系统的wirte函数,不对AOF文件做fsync同步,同步操作由系统负责,通常同步周期为30s。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。everysec命令写入aof_buf后调用系统的write函数,write完成后返回,fsync同步文件操作,有专门的线程每一秒调用一次。everysec是前面两种策略的折中,兼顾了性能和数据安全,也是Redis的默认配置。3、AOF文件重写(rewrite)AOF文件重写主要的作用就是对AOF文件进行压缩,减小AOF文件的体积。需要注意的是,AOF重写只会把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作。为什么文件重写可以压缩AOF文件?过期的数据不需要再写入文件无效的命令不再写入文件多条命令可以合并为一条命令,比如saddstuv1,saddstuv2,saddstuv2,这三条操作可以合并为一条saddstuv1v2v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义总之压缩的原理就是通过重写减小命令的数量从而减少了文件的大小。文件重写的触发用户可以通过执行BGREWRITEAOF命令或者在配置文件中设置对应的选项来触发AOF文件重写操作。1、BGREWRITEAOF命令用户可以通过在Redis客户端手动指定BGREWRITEAOF命令显式的触发AOF操作,该命令是一个无参数命令。BGREWRITEAOF命令是一个异步命令,Redis服务器在接收到命令后会创建一个子进程,由他扫描数据库并生成新的AOF文件。当新的AOF文件生成完毕,子进程就会退出并通知主进程,然后主进程会使用新的AOF文件代替旧旧的AOF文件。BGREWRITEAOF命令执行时服务器的日志另外关于BGREWRITEAOF命令需要注意两点:首先,如果用户发送BGREWRITEAOF命令请求时,服务器正在创建RDB文件,那么服务器将会把AOF的重写操作延后到RDB文件创建完成之后再执行,以此避免两个写操作同时执行导致性能的下降;其次,如果服务器在执行重写的过程中,又接收到了新的BGREWRITEAOF命令请求,那么服务器将会返回错误信息。2、AOF重写配置选项除了可以手动执行BGREWRITEAOF命令重写AOF文件之外,还可以通过配置选项自动的触发BGREWRITEAOF命令重写,主要配置的参数如下:auto-aof-rewrite-min-size<value>:执行AOF重写时,文件体积最小体积,默认为64MB。auto-aof-rewrite-percentage<value>:执行AOF重写时,当前AOF大小和上一次重写AOF大小的比值,默认大小100。这些参数都可通过`configget参数`来查看。其中auto-aof-rewrite-min-size用于设置自动触发BGREWRITEAOF命令的最小AOF文件体积,当AOF文件的体积喜小于给定值时,服务器将不会自动执行BGREWRITEAOF命令。默认值是64MB,含义就是如果AOF文件的体积超过64MB后就会自动触发BGREWRITEAOF命令执行AOF重写。另一个选项auto-aof-rewrite-percentage它控制的是触发自动AOF文件重写所需的文件体积增大比例。默认值是100,表示如果当前AOF文件的体积比最后一次AOF文件重写后的体积增大了一倍(100%),那将自动触发BGREWRITEAOF命令执行AOF重写。如果之前还没有执行过重写,那么服务器启动时的AOF文件大小会被当做最后AOF重写的体积。文件重写的流程对照上图,可以总结出AOF文件的重写流程如下:Redis父进程首先判断当前是否存在正在执行bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。父进程执行fork操作创建子进程,这个过程中会阻塞Redis主进程。父进程fork后,执行bgrewriteaof命令返回”Backgroundappendonlyfilerewritestarted”信息并不再阻塞父进程,此时Redis主进程恢复可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。子进程根据内存快照,按照命令合并规则写入到新的AOF文件。子进程重写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过infopersistence查看。接着父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。4、AOF文件启动加载当开启AOF后,Redis重启会默认优先加载AOF文件来恢复数据;只有当AOF关闭时参会加载RDB文件。Redis加载AOF文件时,会对AOF文件进行校验,如果文件损坏,则日志中就会打印错误,并且Redis会启动失败。当AOF文件损坏后,我们可以使用redis-check-aof这个工具来修复AOF文件。5、AOF优缺点总结最后以一幅图片总结AOF的优缺点:三、RDB-AOF混合持久化在前面我们分析了Redis两种持久化方式,他们都有各自的优缺点:RDB持久化可以生成紧凑RDB文件,并且使用RDB文件进行数据恢复的速度也非常快,但是RDB的全量持久化模式可能会让服务器在宕机是丢失大量数据。与RDB相比,AOF持久化可以将丢失数据的时间窗口限制在1s内,但是AOF文件的体积要比RDB文件的体积大的多,并且数据恢复过程也相对较慢。为了解决RDB和AOF两种持久化方式选择问题,Redis从4.0版本开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化模式构建而来的,在打开AOF持久化的基础上,在配置上一下信息就可以打开混合持久化:aof-use-rdb-preamble<value>选项如果设置成为了yes,那么Redis服务器将在执行AOF重写操作时,向执行BGSAVE命令那样,根据数据库当前状态生成出相应的RDB数据,并将这些数据写入新建的AOF文件。换句话说,如果开启了混合持久化功能之后,服务器生成的AOF文件将由廊部分组成,其中AOF开头的是RDB格式的数据,RDB-AOF混合持久化生成的AOF文件通过使用RDB-AOF混合持久化功能,用户可以同时获得RDB持久化和AOF持久化的优点:服务器既可以通过AOF文件包含的RDB数据快速恢复数据,有可以通过AOF文件包含的AOF数据来讲数据丢失控制到1s之内。需要注意的是,RDB-AOF混合持久化生成的AOF文件同时包含RDB格式的数据和AOF个格式的数据,而传统的AOF持久化生成的AOF文件只包含AOF格式的数据。四、同时使用RDB持久化和AOF持久化在Redis4.0的RDB-AOF混合持久化出现之前,许多追求安全性的Redis使用者都会同时开启RDB和AOF阆中持久化方式,但是随着Redis4.0的RDB-AOF混合持久化的推出,同时使用两种持久化机制已经没有必要。对于使用Redis4.0的系统,优先使用RDB-AOF混合持久化是个不错的选择。如果使用的是Redis4.0之前的版本,那么在RDB和AOF之间如何选择,下面是Redis官方给出的建议:原文链接我的理解就是Redis4.0版本之前具体选择哪种持久化方式要看你的业务类型,如果你的业务对于数据一致性没有那么高的要求、网站访问量非常有限,那么仅仅开启RDB就足够了,比如像我的这个博客网站;但是如果你的业务对数据一致性要求非常高、网站访问量有十分巨大,那么RDB配合AOF是个不错的选择。特别的,Redis官方建议不要单独使用AOF,因为定时生成RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。参考【1】黄健宏.Redis使用手册[M].北京:机械工业出版社,2019:380-399.【2】Redis官网.RedisPersistence
LoveIT 2019-10-09Redis -
手把手教你在Centos 7上安装、配置、启动Redis
一、什么是Redis?Redis是RemoteDictionarySevery的缩写,中文名称:远程字典服务器,它是一个基于C语言编写的完全开源免费,并且遵循BSD协议的一个高性能的key-value型分布式内存数据库。Redis使用key-value的形式保存值,常用的数据类型有String、list、hash、set、zset等数据结构,因此也被人们称为数据结构服务器。二、在Linux(Centos7)安装Redis1、首先去Redis的官方网站下载需要redisredis官网的镜像网址:http://download.redis.io/releases/,在这里有redis的各个版本:在Liunx上使用wget命令下载(我这个下载它当前的最新的镜像redis-5.0.5-tar.gz版本):wgethttp://download.redis.io/releases/redis-5.0.5.tar.gz2、下载好后解压tar-zxvfredis-5.0.5.tar.gz三、安装、配置、启动Redis1、执行make命令编译redis源文件在编译之前请检查一下你的Linux上是否已经安装了GCC编译器,如果没有请先安装GCC编译器。在命令行输入gcc-v后如果打印出如下类似信息说明你的Linux上已经安装了gcc了如果没问题了,进入到刚刚解压出来的文件夹根目录下执行make命令编译Redis2、makeinstallPREFIX=/usr/local/redis安装Redismake执行成功后文件夹找就会多一个src文件夹,进入src文件夹,执行makeinstallPREFIX=/usr/local/redis命令,把redis安装到/usr/local/redis/其实1、2两步可以合并到一步执行:make&&makeinstallPREFIX=/usr/local/redis3、拷贝配置文件到安装目录安装好的redis目录下默认没有配置文件,我们需要复制一份到安装目录下4、配置redis为后台启动将刚在复制到安装目录的那个redis.conf打开,并把其中的daemonizeno改成daemonizeyes5、设置redis开机自动启动打开/etc/rc.local在里面添加:/usr/local/redis/bin/redis-server/usr/local/redis/redis.conf(rc.local这个脚本会在开机的时候执行)6、启动redis服务执行命令redis-server/usr/local/redis/redis.conf启动redis服务。注意!如果按照上面的正常的流程安装下来,但是在执行redis-server启动redis的时候提示redis-server不是命令,不要慌张,这是由于这个redis-server不是全局的命令不能在每一个目录下使用,当在别的目录下使用的时候系统在/usr/bin/找不到这个命令,此时我们需要把安装目录下的redis-server链接到到到/usr/bin目录下就可以了。比如我的redis安装目录是/home/myredis/redis/redis-5.0.5/src/redis-server,那就可以执行下面的命令:ln-s/home/myredis/redis/redis-5.0.5/src/redis-server/usr/bin/redis-server解决问题后再来执行上面那个命令启动redis服务,启动后我们可以使用命令ps-ef|grepredis来查看服务有没有启动:redis启动成功了,之后执行redis-cli-p6379进入redis的命令行模式四、redis-benchmarkredis自带了一个性能测试工具redis-benchmark,他有丰富的模拟组件和指令可以使用。中文链接如下:http://www.redis.cn/topics/benchmarks.html。redis-benchmark程序模拟N个客户端同时发出M个请求来测试在本机上redis可以达到的吞吐量从而间接的对给定硬件条件下面的机器计算出性能参数。表现为Responsetime和完成request的数量等等。redis-benchmark可以使用到的参数:五、redis入门必会必知(0)什么是Redis?redis是一个key-value型的高性能内存数据库,同时Redis也是一个优秀的缓存中间件,类似于memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的Key-ValueDB。(1)redis的默认端口是多少?redis出厂的默认端口是6379,这个端口可以在redis的配置文件中修改。(2)redis的常用五大数据类型Redis的五大常用数据类型是:string(字符串)、list(列表)、hash(散列表)、set(集合)、zSet(sortedSet,有序集合)(3)redis是以单线程来处理客户端的请求。对读写等事件的响应式通过对epoll函数的包装来实现到的。Redis的实际处理速度完全依靠主进程的执行效率。epoll是Linux内科为处理大批量文件描述符伟做了改进的epoll,是Linux下多路复用IO接口select/poll的增强版本,他能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。(4)默认有16个数据库。默认的数据库从DB0开始(默认登录也是0号库),切换可以使用select<dbid>,这些在redis.conf这个文件中有详细的说明:(5)一个字符串类型的值能存储最大容量是多少?512MB(6)一个Redis实例最多能存放多少的keys?List、Set、SortedSet他们最多能存放多少元素?和内存大小有关,内存越大可以存放的key就越多,能存储的元素就越多。(7)Redis集群最大结点数是多少?16384个(8)Redishash槽的概念?Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
LoveIT 2019-10-08Redis -
Redis配置文件redis.conf详解
Redis脚本简介在我们介绍Redis的配置文件之前,我们先来说一下Redis安装完成后生成的几个可执行文件:redis-server、redis-cli、redis-benchmark、redis-stat、redis-check-dump、redis-check-aof:Redis配置文件详解开头说明开头说明中主要就是要注意在redis中内存大小写k和kb是不一样的,前者是1000的倍数,后者超时1024的倍数。INCLUDES INCLUDES的作用就是把其他关于redis的配置文件引入到redis.conf文件中使其生效,redis.conf就作为一个总闸一样,配置的方法是使用include来引入一个路径下配置文件(比如:include/path/aaa/other.conf)。 需要注意的是,如果将此配置写在redis.conf文件的开头,那么后面的配置会覆盖引入文件的配置,如果想以引入文件的配置为主,那么需要将include配置写在redis.conf文件的末尾。MODULES这个部分是用来引入自定义的模块的。通过这里的`loadmodule`配置将引入自定义模块来新增一些功能。NETWORKbind:绑定redis服务器网卡IP,默认为127.0.0.1,即本地回环地址。这样的话访问redis服务只能通过本机的客户端连接,而无法通过远程连接。如果bind被注掉了或者为空时会接收所有来自于可用网络的连接。port:指定redis运行的端口,默认的是6379。由于redis是单线程模型,因此单机开多个redis运行的时候会修改端口,除此而外一般保持默认的即可。protected-mode:是否开启保护模式,默认是yes表示开启保护模式timeout:设置客户端连接时的超时时间,单位:秒。当客户端在这段时间没有任何操作(空闲的),那么就会关闭连接。默认为0,表示永不关闭。tcp-backlog:此参数确定了TCP连接中已完成的队列长度,这个值必须不能大于Linux系统中定义的/proc/sys/net/core/somaxconn值,默认是511。tcp-keepalive:表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康状态,避免服务器一直阻塞,官方默认是300s,如果设置为0,表示不周期性检测。GENERALdaemonize:设置为yes表示指定Redis以守护进程的方式启动(后台启动)。默认为nopidfile:配置pid文件路径,当Redis作为守护进程运行的时候,会把pid默认写到/var/redis/run/redis_6379.pid文件里面loglevel:定义日志级别。默认为notice。Redis中有4中日志级别:debug:记录详细的日志,使用与开发、测试阶段varbose:较多的日志notice:适量的日志信息,适用于生产环境warning:仅有部分重要、关键的才会被记录logfile:配置日志文件默认存放的位置,默认会直接打印在终端的屏幕上databases:设置数据库的数目。默认的数据库是DB0,有16个,可以使用select<dbid>命令选择不同的数据库。always-show-logo:是否在启动的时候显示Redis的logo,默认为yes,即显示logo。SNAPSHOTTINGSnapshotting:快照。主要是用来配置持久化策略的。save:用来配置触发Redis的做持久化的条件,也就是什么时候将内存中的数据保存到硬盘中。默认配置如下:*save9001:表示900s内如果有1个key变化,到时间(900s)后就把这段时间内的变化保存到磁盘*save30010表示300s内如果有10个key变化,到时间(300s)后就把这段时间内的变化保存到磁盘*save6010000表示60s内如果有10000个key变化,到时间(60s)后就把这段时间内的变化保存到磁盘当然如果只是使用Redis的缓存功能,不需要持久化,那么可以把这些save注释掉,然后使用一个空字符串实现停用:save""stop-writes-on-bgsave-error:当启用了RDB且最火一次后台保存数据失败,Redsi是否停止接收数据。默认值为yes,这会让用户意识到数据没有正确持久化到硬盘上,从而可以排错,否者没有人会注意到灾难发生了。rdbcompression:对于存储到磁盘中的快照,可以设置时候惊进行压缩存储。默认值是yes,redis会使用LZF算法进行压缩。但是压缩会带来一定的CPU消耗,如果关闭后存储在磁盘上的快照将会非常大。rdbchecksum:在存储快照后,我们还可以让Redis使用CRC64算法来进行数据校验。默认是yes,这样会带来10%的性能消耗。dbfilename:设置快照的文件名,默认名字是dump.rdb。dir:设置快照文件的存放路径,这个配置项必须自定的是一个目录,而不能是一个文件名。保存的是上面dbfilename,默认保存到当前目录下。REPLICATIONslave-serve-stale-data:当一个slave和一个master失去联系,或者复正在进行的时候,slave可能会有两种表现:*如果是yes,slave任然会应答客户端请求,但是返回的数据是过时的。*如果是no,在执行除了infohesalvaof之外的其他命令时,slave都将返回一个“SYNCwithmasterinprogress”错误slave-read-only:配置Redis的Slave示例是否接受写操作,即Slave是否为只读Redis。默认值是yes,Slave为只读。repl-diskless-sync:主从复制是否使用无硬盘复制功能。默认值为no。repl-disless-sync-delay:当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从站传送RDB文件,这个等待时间是可配置的。这一点很重要,因为一旦传送开始,就不可能再为一个新到达的从站服务。从站则要排队等待下一次RDB传送。因此服务器等待一段时间以期更多的从站到达。延迟时间以秒为单位,默认为5秒。要关掉这一功能,只需将它设置为0秒,传送会立即启动。默认值为5。repl-disalbe-tcp-nodelay:同步之后是否禁用从站上TCP_NODELAY。如果yes,表示redis会使用较少的TCP包和带宽向从站发送数据。但是这回导致从站增加数据延时;如果选择no,从站的数据延时不会那么多,但备份需要的带宽相对较多。Redis默认设置是no。SECURITYrename-command:从命名命令。例如对于一些危险的命令:*flushdb:清空当前数据库*flushall:清空所有数据库*config:客户端连接后可配置服务器*keys:查看数据库中所有的键requirepass:设置Redis连接密码,如果配置了连接密码,客户端在连接Redis的时候需要通过auth<password>命令来验证。默认是关闭的。作为服务端redis-server,常常需要禁用以上命令来使得服务器更加安全,禁用的具体做法是:(比如禁用FLUSHALL命令):*rename-commandFLUSHALL""也可以保留这个命令但是把它重命名,一般人没有权限使用:*rename-commandFLUSHALLsfr443g432这样,重启服务器后则需要使用新命令来执行操作,否则服务器会报错unknowncommand。CLIENTSmaxclients:设置客户端最大的连接数,默认是10000个连接。当客户端连接数到达限制是,Redis会关闭新的连接并向客户端返回maxnumberofclientsreached错误信息。如果设置为0,表示不作限制。MEMORYMANAGEMENTmaxmemory:指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区maxmemory-policy:当内存使用达到最大值时,redis应该采用的内存清理策略。有以下几种可以选择:volatile-lru:从所有配置了过期时间的键中移除最近很少使用的键allkeys-lru:从所有键中移除任何最近很少使用过的键volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键allkeys-lfu:从所有键中驱逐使用频率最少的键volatile-random:随机移除设置了过期时间的keyallkeys-random:随机移除任何keyvolatile-ttl:移除距过期时间最近的keynoeviction:不移除任何key,仅仅返回写错误,默认值。APPENDONLYMODEappendonly:默认redis使用的是RDB方式持久化,这种方式在许多应用中已经足够用了。但是对于数据一致性要求很高的应用,如果还是只使用RDB,一旦redis宕机,会导致可能有几分钟的数据丢失,这种场景下就需要使用AOF(另一种持久化方式),可以提供更好的持久化特性以及更高的数据一致性。将appendonly置为yes开启AOF,Redis将会把每次写入的数据在接收后都写入appendonly.aof文件(默认的文件名),每次启动的时候会优先加载appendonly.aof这个文件到内存中。默认值是no。appendfilename:aof文件的默认文件名,默认值是appendonly.aofappendfsync:aof持久化化策略配置。有三个值可以选:*no:不执行fsync,有操作系统保证数据同步到磁盘,速度最快*always:每次写入都执行fsync,以保证数据同步到磁盘,速度最慢*everysec:每秒执行一次fsync,这样aof就可能会对时1s的数据(默认值,通常来说能在速度和数据安全性之间取得比较好的平衡。)no-appendfsync-on-rewirite:如果有子进程在进行保存操作,那么Redis就处于"不可同步"的状态。这实际上是说,在最差的情况下可能会丢掉30秒钟的日志数据。(默认Linux设定)如果把这个设置成"yes"带来了延迟问题,就保持"no",这是保存持久数据的最安全的方式。auto-aof-rewrite-percentage:aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。默认值是100auto-aof-rewrite-min-size:AOF文件到达重写的阈值,避免了达到约定百分比但尺寸仍然很小的情况还要重写,默认64m,这点内存啥都干不了,一般设置都是以GB为单位。aof-load-truncated:如果设置为yes,如果一个因异常被截断的AOF文件被redis启动时加载进内存,redis将会发送日志通知用户;如果设置为no,erdis将会拒绝启动。此时需要用"redis-check-aof"工具修复文件。LUASCRIPTINGlua-time-limit:一个lua脚本执行的最大时间,单位:ms。默认值5000。REDISCLUSTERcluster-enable:是否开启集群,默认是不开启的。cluster-config-file:集群配置文件名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。这个文件不需要手动配置,它由redis生成并更新。默认配置为nodes-6379.confcluster-node-timeout:可以设置值为15000。节点互连超时的阈值,集群节点超时毫秒数。cluster-slave-validity-factor:在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:比较slave断开连接的时间和(node-timeout*slave-validity-factor)+repl-ping-slave-period如果节点超时时间为三十秒,并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移。cluster-migration-barrier:master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2个可工作的从节点时,它的一个从节点会尝试迁移。cluster-require-full-coverage:默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。
LoveIT 2019-10-08Redis -
Redis五种常用数据类型及命令详解
Redis键(key)的通用命令,所有的数据类型都可以使用string有关命令字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key-value,一般来存字符串,整数和浮点数。value最多可以容纳的数据长度为512MB应用场景:很常见的场景用于统计网站访问数量,当前在线人数等。incr命令(++操作)SETkeyvalue[Options]将字符串value关联到键key。如果key已经持有其他值,SET就覆写旧值,无视类型。可选参数:EXseconds:将键的过期时间设置为seconds秒。执行SETkeyvalueEXseconds的效果等同于执行SETEXkeysecondsvalue。PXmilliseconds:将键的过期时间设置为milliseconds毫秒。执行SETkeyvaluePXmilliseconds的效果等同于执行PSETEXkeymillisecondsvalue。NX:只在键不存在时,才对键进行设置操作。执行SETkeyvalueNX的效果等同于执行SETNXkeyvalue。XX:只在键已经存在时,才对键进行设置操作。当SET命令对一个带有生存时间(TTL)的键进行设置之后,该键原有的TTL将被清除。GETkey返回与键key相关联的字符串值,如果没有这个key,返回nil。APPENDkeyvalue如果键key已经存在并且它的值是一个字符串,APPEND命令将把value追加到键key现有值的末尾。如果key不存在,APPEND就简单地将键key的值设为value,就像执行SETkeyvalue一样。执行成功后会返回当前value的长度。STRLENkey返回与key关联的value的字符串的长度,当key不存在的时候返回0,当key不是字符串的时候使用这个命令会报错。INCRkey为键key储存的数字值加上一。如果键key不存在,那么它的值会先被初始化为0,然后再执行INCR命令。如果键key储存的值不能被解释为数字,那么INCR命令将返回一个错误。INCRBYkeyincrement为键key储存的数字值加上增量increment。如果键key不存在,那么键key的值会先被初始化为0,然后再执行INCRBY命令。如果键key储存的值不能被解释为数字,那么INCRBY命令将返回一个错误DECRkey为键key储存的数字值减去一。如果键key不存在,那么键key的值会先被初始化为0,然后再执行DECR操作。如果键key储存的值不能被解释为数字,那么DECR命令将返回一个错误。DECRBYkeydecrement将键key储存的整数值减去减量decrement。如果键key不存在,那么键key的值会先被初始化为0,然后再执行DECRBY命令。如果键key储存的值不能被解释为数字,那么DECRBY命令将返回一个错误。注意:上面这4个命令只能用于value是数字值的,而且这些操作执行后都会返回加/减操作后的值,且仅仅支持64位(bit)有符号数字表示之内。GETRANGEkeystartend返回键key储存的字符串值的指定部分,字符串的截取范围介于start和end两个偏移量。SETRANGEkeyoffsetvalue从偏移量offset开始,用value参数覆写(overwrite)键key储存的字符串值。不存在的键key当作空白字符串处理。Redis允许的字符串最大的512M,即:能够使用的最大偏移量为2^29-1(536870911),但是请别这样做,除非你想上午还在写码,下午就被开除!!!MSETkeyvalue[keyvalue…]同时为多个键设置值。如果某个给定键已经存在,那么MSET将使用新值去覆盖旧值。MSET是一个原子性(atomic)操作,所有给定键都会在同一时间内被设置,不会出现某些键被设置了但是另一些键没有被设置的情况。MGETkey[key…]返回给定的一个或多个字符串键的值。如果给定的字符串键里面,有某个键不存在,那么这个键的值将以特殊值nil表示MSETNXkeyvalue[keyvalue…]当且仅当所有给定键都不存在时,为所有给定键设置值。即使只有一个给定键已经存在,MSETNX命令也会拒绝执行对所有键的设置操作。MSETNX是一个原子性(atomic)操作,所有给定键要么就全部都被设置,要么就全部都不设置,不可能出现第三种状态。列表listRedis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。List中可以包含的最大元素数量是4294967295。应用场景:1.最新消息排行榜。2.消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)LPUSHkeyvalue[value…]将一个或多个值value插入到列表key的表头。如果有多个value值,那么各个value值按从左到右的顺序依次插入到表头,List中允许有重复的值。RPUSHkeyvaluevalue…]将一个或多个值value插入到列表key的表尾(最右边)。如果有多个value值,那么各个value值按从左到右的顺序依次插入到表尾LRANGEkeystartend返回列表key中指定区间内的元素,区间以偏移量start和end指定。下标(index)参数start和end都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。LPUSHXkeyvalue当且仅当key存在并且是一个列表时才将值value插入到列表key的表头。RPUSHkeyvalue当且仅当key存在并且是一个列表时才将值value插入到列表key的表尾。LPOPkey移除并返回列表key的头元素。当元素不存在时返回nil。RPOPkey移除并返回列表key的尾元素。当元素不存在时返回nil。RPOPLPUSHsourcedestination将source弹出的元素插入到列表destination,作为destination列表的的头元素。如果source和destination相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。LLENkey返回列表key的长度。如果key不存在,则key被解释为一个空列表,返回0。如果key不是列表类型,返回一个错误。LREMkeycountvalue当count>0表示从表头开始搜索并删除count个和value相等的元素当count<0表示从表尾开始搜索并删除count个和vlaue相等的元素当count=0表示删除表中所有的和vlaue相等的元素Listtrimstartend对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。LINDEXkeyindex返回列表key中,下标为index的元素。下标(index)参数以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。LINSERTkeyBEFORE|AFTERpivotvalue将值value插入到列表key当中,位于值pivot之前或之后。当pivot不存在于列表key时,不执行任何操作。当key不存在时,key被视为空列表,不执行任何操作LSETkeyindexvlaue将列表key下标为index的元素的值设值为value。当index参数超出范围,或对一个空列表(key不存在)进行LSET时,返回一个错误。字典hashRedis中的hash可以看成具有Stringkey和Stringvalue的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。应用场景:例如存储、读取、修改用户属性(name,age,pwd等)HSETkeyfiledvlaue[filedvalue...]将哈希表key中域field的值设置为value。如果给定的哈希表并不存在,那么一个新的哈希表将被创建并执行HSET操作。如果域field已经存在于哈希表中,那么它的旧值将被新值value覆盖。HGETkeyfiled返回哈希表中给定域的值。如果给定的域或者hash表不存在,返回nil。HGETALLkey返回哈希表key中,所有的域和值。在返回值里,紧跟每个域名(fieldname)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。HMSETkeyfiledvalue[filedvalue...]同时将多个field-value(域-值)对设置到哈希表key中。此命令会覆盖哈希表中已存在的域。如果key不存在,一个空哈希表会被创建并执行HMSET操作。这个的使用和HSET的用法一样。HMGETkeyfiled回哈希表key中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个nil值。这个命令的作用和HEGT的作用一样。HSETNXkeyfiledvalue当且仅当域field尚未存在于哈希表key中的情况下,将它的值设置为value。如果给定域已经存在于哈希表当中,那么命令将放弃执行设置操作。如果哈希表hash不存在,那么一个新的哈希表将被创建并执行HSETNX命令HEXISTSkeyfiled检查给定域field是否存在于哈希表hash当中。存在返回1,不存在返回0。HLENkey返回哈希表key中域的数量。HSTRLENkeyfiled返回哈希表key中,与给定域field相关联的值的字符串长度(stringlength)。如果给定的键或者域不存在,那么命令返回0。HINCRBYkeyfiledincrement为哈希表key中的域field的值加上增量increment。增量也可以为负数,相当于对给定域进行减法操作。如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。如果域field不存在,那么在执行命令前,域的值被初始化为0。HINCRBYFLOATkeyfiledincrement和HINCRBYkeyfiledincrement的作用一样,都是哈希表key中的域field的值加上增量increment,但是这里的增量是浮点数。HKEYSkey返回哈希表key中的所有域。HVALSkey返回哈希表key中的所有域的值。集合setRedis的集合是无序不可重复的,和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。应用场景:1.利用交集求共同好友。2.利用唯一性,可以统计访问网站的所有独立IP。3.好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。SADDkeymember[member…]将一个或多个member元素加入到集合key当中,已经存在于集合的member元素将被忽略。假如key不存在,则创建一个只包含member元素作成员的集合。会返回被添加到集合中元素的个数,不包括重复的元素。SMEMBERSkey返回集合key中的所有成员,不存在的key被视为空集合。SISMEMBERSkeymember判断member元素是否集合key的成员。如果member元素是集合的成员,返回1。如果member元素不是集合的成员,或key不存在,返回0。SCARDkey返回集合key的基数(集合中元素的数量)。SREMkey[key...]移除集合key中的一个或多个member元素,不存在的member元素会被忽略。SRANDMEMBERkeycount如果命令执行时,只提供了key参数,那么返回集合中的一个随机元素。从Redis2.6版本开始,SRANDMEMBER命令接受可选的count参数:如果count为正数,且小于集合基数,那么命令返回一个包含count个元素的数组,数组中的元素各不相同。如果count大于等于集合基数,那么返回整个集合。如果count为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为count的绝对值。SPOPkeycount移除并返回集合中的一个或多个随机元素。SMOVEsourcedestination将member元素从source集合移动到destination集合。SMOVE是原子性操作。如果source集合不存在或不包含指定的member元素,则SMOVE命令不执行任何操作,仅返回0。否则,member元素从source集合中被移除,并添加到destination集合中去。当destination集合已经包含member元素时,SMOVE命令只是简单地将source集合中的member元素删除。集合的数学操作命令SDIFFkey[key…]返回一个集合的全部成员,该集合是所有给定集合之间的差集。不存在的key被视为空集。SINTERkey[key...]返回一个集合的全部成员,该集合是所有给定集合的交集。不存在的key被视为空集。当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。SUNIONkey[key...]返回一个集合的全部成员,该集合是所有给定集合的并集。不存在的key被视为空集。有序集合zset和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是唯一的,但是分数(score)却可以重复。应用场景:可以用于一个大型在线游戏的积分排行榜,每当玩家的分数发生变化时,可以执行zadd更新玩家分数(score),此后在通过zrange获取几分topten的用户信息。ZADDkeyscoremember[scoremember...]将一个或多个member元素及其score值加入到有序集key当中。如果某个member已经是有序集的成员,那么更新这个member的score值,并通过重新插入这个member元素,来保证该member在正确的位置上。score值可以是整数值或双精度浮点数。如果key不存在,则创建一个空的有序集并执行ZADD操作。ZRANGEkeystartend[withscore]返回有序集key中,指定区间内的成员。其中成员的位置按score值递增(从小到大)来排序。具有相同score值的成员按字典序(lexicographicalorder)来排列。如果需要成员按score值递减(从大到小)来排列,可以使用ZREVRANGEkeystartendwithscoreZREVRANGEkeystartend[withscores]返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的逆序(reverselexicographicalorder)排列。ZRANGEBYSCOREkeyminmax[withscores][limitoffsetcount]返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。具有相同score值的成员按字典序(lexicographicalorder)来排列(该属性是有序集提供的,不需要额外的计算)。可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECTLIMIToffset,count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。ZREMkeymember[menber...]移除有序集key中的一个或多个成员,不存在的成员将被忽略。ZCARDstu当key存在且是有序集类型时,返回有序集的基数。当key不存在时,返回0。ZCOUNTstustartend返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员的数量。ZRANKkeymember返回有序集key中成员member的排名。其中有序集成员按score值递增(从小到大)顺序排列。排名以0为底,也就是说,score值最小的成员排名为0。ZREVRANKkeymember返回有序集key中成员member的排名。其中有序集成员按score值递减(从大到小和默认的逆序))排序。排名以0为底,也就是说,score值最大的成员排名为0。ZRANGEBYSOCREkeyminmax[withscores][limitoffsetcount]返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。具有相同score值的成员按字典序(lexicographicalorder)来排列:*可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECTLIMIToffset,count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。*可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。ZREVRANGESCOREkeyminmax[withsocres][limitoffsetcount]使用方法和ZRANGEBYSOCRE的用法一样,还是这个是前者的逆序更多Redis命令参考:http://redisdoc.com/。另外这里只是介绍了一下Redis中5种常用数据类型的有关命令和简单的使用场景,关于他们的底层实现我把他放到了下一节:Redis五种常见数据结构的实现及使用场景,感兴趣的小伙伴可以去看看,图文配合非常简单易懂~~参考【1】dijia478.Redis的五种数据类型及方法.博客园
LoveIT 2019-10-08Redis