Rax是Redis内部比较特殊的一个数据结构,它是一个有序字典树(基数树RadixTree),按照key的字典序排列,支持快速地定位、插入和删除操作。Redis五大基础数据结构里面,能作为字典使用的有hash和zset。hash不具备排序功能,zset则是按照score进行排序的。rax跟zset的不同在于它是按照key进行排序的。Redis作者认为rax的结构非常易于理解,但是实现却有相当的复杂度,需要考虑很多的边界条件,需要处理节点的分裂、合并,一不小心就会出错。应用你可以将一本英语字典看成一棵radixtree,它所有的单词都是按照字典序进行排列,每个词汇都会附带一个解释,这个解释就是k
Redis5.0又引入了一个新的数据结构listpack,它是对ziplist结构的改进,在存储空间上会更加节省,而且结构上也比ziplist要精简。它的整体形式和ziplist还是比较接近的,如果你认真阅读了ziplist的内部结构分析,那么listpack也是比较容易理解的。structlistpack<T>{ int32total_bytes;//占用的总字节数 int16size;//元素个数 T[]entries;//紧凑排列的元素列表 int8end;//同zlend一样,恒为0xFF }首先这个listpack跟ziplist的结构几乎一摸一样,只是少
Redis的zset是一个复合结构,一方面它需要一个hash结构来存储value和score的对应关系,另一方面需要提供按照score来排序的功能,还需要能够指定score的范围来获取value列表的功能,这就需要另外一个结构「跳跃列表」。zset的内部实现是一个hash字典加一个跳跃列表(skiplist)。hash结构在讲字典结构时已经详细分析过了,它很类似于Java语言中的HashMap结构。本节我们来讲跳跃列表,它比较复杂,读者要有心理准备。基本结构上图就是跳跃列表的示意图,图中只画了四层,Redis的跳跃表共有64层,意味着最多可以容纳2^64次方个元素。每一个kv块对应的结构如下面
Redis早期版本存储list列表数据结构使用的是压缩列表ziplist和普通的双向链表linkedlist,也就是元素少时用ziplist,元素多时用linkedlist。//链表的节点 structlistNode<T>{ listNode*prev; listNode*next; Tvalue; } //链表 structlist{ listNode*head; listNode*tail; longlength; }考虑到链表的附加空间相对太高,prev和next指针就要占去16个字节(64bit系统的指针是8个字节),另外每个节点的内存都是单独分配
Redis为了节约内存空间使用,zset和hash容器对象在元素个数较少的时候,采用压缩列表(ziplist)进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。>zaddprogrammings1.0go2.0python3.0java (integer)3 >debugobjectprogrammings Valueat:0x7fec2de00020refcount:1encoding:ziplistserializedlength:36lru:6022374lru_seconds_idle:6 >hmsetbooksgofastpythonsl
dict是Redis服务器中出现最为频繁的复合型数据结构,除了hash结构的数据会用到字典外,整个Redis数据库的所有key和value也组成了一个全局字典,还有带过期时间的key集合也是一个字典。zset集合中存储value和score值的映射关系也是通过dict结构实现的。structRedisDb{ dict*dict;//allkeys key=>value dict*expires;//allexpiredkeyskey=>long(timestamp) ... } structzset{ dict*dict;//allvalues value=>
Redis中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的。我们知道C语言里面的字符串标准形式是以NULL作为结束符,但是在Redis里面字符串不是这么表示的。因为要获取NULL结尾的字符串的长度使用的是strlen标准库函数,这个函数的算法复杂度是O(n),它需要对字节数组进行遍历扫描,作为单线程的Redis表示承受不起。Redis的字符串叫着「SDS」,也就是SimpleDynamicString。它的结构是一个带长度信息的字节数组。structSDS<T>{ Tcapacity;//数组容量 Tlen;//数组长度 byteflags;//特殊标识位,不
想象这样一个应用场景,公司有两个机房。因为一个紧急需求,需要跨机房读取Redis数据。应用部署在A机房,存储部署在B机房。如果使用普通tcp直接访问,因为跨机房所以传输数据会暴露在公网,这非常不安全,客户端服务器交互的数据存在被窃听的风险。Redis本身并不支持SSL安全链接,不过有了SSL代理软件,我们可以让通信数据透明地得到加密,就好像Redis穿上了一层隐身外套一样。spiped就是这样的一款SSL代理软件,它是Redis官方推荐的代理软件。spiped原理让我们放大细节,仔细观察spiped实现原理。spiped会在客户端和服务器各启动一个spiped进程。左边的spiped进程A负责
本节我们来谈谈使用Redis需要注意的安全风险以及防范措施,避免数据泄露和丢失,避免所在主机权限被黑客窃取,以及避免人为操作失误。指令安全Redis有一些非常危险的指令,这些指令会对Redis的稳定以及数据安全造成非常严重的影响。比如keys指令会导致Redis卡顿,flushdb和flushall会让Redis的所有数据全部清空。如何避免人为操作失误导致这些灾难性的后果也是运维人员特别需要注意的风险点之一。Redis在配置文件中提供了rename-command指令用于将某些危险的指令修改成特别的名称,用来避免人为误操作。比如在配置文件的security块增加下面的内容:rename-com
本节面向Java用户,主题是如何优雅地使用Jedis编写应用程序,既可以让代码看起来赏心悦目,又可以避免使用者犯错。Jedis是Java用户最常用的Redis开源客户端。它非常小巧,实现原理也很简单,最重要的是很稳定,而且使用的方法参数名称和官方的文档非常match,如果有什么方法不会用,直接参考官方的指令文档阅读一下就会了,省去了非必要的重复学习成本。不像有些客户端把方法名称都换了,虽然表面上给读者带来了便捷,但是需要挨个重新学习这些API,提高了学习成本。Java程序一般都是多线程的应用程序,意味着我们很少直接使用Jedis,而是要用到Jedis的连接池——JedisPool。同时因为Je
一直以来我们认为Redis是单线程的,单线程为Redis带来了代码的简洁性和丰富多样的数据结构。不过Redis内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作。Redis为什么要懒惰删除(lazyfree)?删除指令del会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的key是一个非常大的对象,比如一个包含了千万元素的hash,那么删除操作就会导致单线程卡顿。Redis为了解决这个卡顿问题,在4.0版本引入了unlink指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存。>unlinkkey OK如果有多线程的开发经验,
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap)。交换会让Redis的性能急剧下降,对于访问量比较频繁的Redis来说,这样龟速的存取效率基本上等于不可用。在生产环境中我们是不允许Redis出现交换行为的,为了限制最大使用内存,Redis提供了配置参数maxmemory来限制内存超出期望大小。当实际内存超出maxmemory时,Redis提供了几种可选策略(maxmemory-policy)来让用户自己决定该如何腾出新的空间以继续提供读写服务。noeviction不会继续服务写请求(DEL请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是
Redis所有的数据结构都可以设置过期时间,时间一到,就会自动删除。你可以想象Redis内部有一个死神,时刻盯着所有设置了过期时间的key,寿命一到就会立即收割。你还可以进一步站在死神的角度思考,会不会因为同一时间太多的key过期,以至于忙不过来。同时因为Redis是单线程的,收割的时间也会占用线程的处理时间,如果收割的太过于繁忙,会不会导致线上读写指令出现卡顿。这些问题Antirez早就想到了,所有在过期这件事上,Redis非常小心。过期的key集合redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除了定时遍历之外,它还会使用惰性策略来
在第三节,我们细致讲解了分布式锁的原理,它的使用非常简单,一条指令就可以完成加锁操作。不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。比如在Sentinel集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。不过这种不安全也仅仅是在主从发生failover的情况下才会产生,而且持续时间极短,业务系统多数情况下可
在使用Redis时,时常会遇到很多问题需要诊断,在诊断之前需要了解Redis的运行状态,通过强大的Info指令,你可以清晰地知道Redis内部一系列运行参数。Info指令显示的信息非常繁多,分为9大块,每个块都有非常多的参数,这9个块分别是:1、Server服务器运行的环境参数2、Clients客户端相关信息3、Memory服务器运行内存统计数据4、Persistence持久化信息5、Stats通用统计数据6、Replication主从复制相关信息7、CPUCPU使用情况8、Cluster集群信息9、KeySpace键值对统计数量信息Info可以一次性获取所有的信息,也可以按块取信息。#获取所
Redis5.0被作者Antirez突然放了出来,增加了很多新的特色功能。而Redis5.0最大的新特性就是多出了一个数据结构Stream,它是一个新的强大的支持多播的可持久化的消息队列,作者坦言RedisStream狠狠地借鉴了Kafka的设计。RedisStream的结构如上图所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后,内容还在。每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。每个Stream都可以挂多个消费组,每个消费组会有个游标last_deliver
在平时线上Redis维护工作中,有时候需要从Redis实例成千上万的key中找出特定前缀的key列表来手动处理数据,可能是修改它的值,也可能是删除key。这里就有一个问题,如何从海量的key中找出满足特定前缀的key列表来?Redis提供了一个简单暴力的指令keys用来列出所有满足特定正则字符串规则的key。127.0.0.1:6379>setcodehole1a OK 127.0.0.1:6379>setcodehole2b OK 127.0.0.1:6379>setcodehole3c OK 127.0.0.1:6379>setcode1holea OK 127.0
Redis在3.2版本以后增加了地理位置GEO模块,意味着我们可以使用Redis来实现摩拜单车「附近的Mobike」、美团和饿了么「附近的餐馆」这样的功能了。用数据库来算附近的人地图元素的位置数据使用二维的经纬度表示,经度范围(-180,180],纬度范围(-90,90],纬度正负以赤道为界,北正南负,经度正负以本初子午线(英国格林尼治天文台)为界,东正西负。比如掘金办公室在望京SOHO,它的经纬度坐标是(116.48105,39.996794),都是正数,因为中国位于东北半球。当两个元素的距离不是很远时,可以直接使用勾股定理就能算得元素之间的距离。我们平时使用的「附近的人」的功能,元素距离都
漏斗限流是最常用的限流方法之一,顾名思义,这个算法的灵感源于漏斗(funnel)的结构。漏斗的容量是有限的,如果将漏嘴堵住,然后一直往里面灌水,它就会变满,直至再也装不进去。如果将漏嘴放开,水就会往下流,流走一部分之后,就又可以继续往里面灌水。如果漏嘴流水的速率大于灌水的速率,那么漏斗永远都装不满。如果漏嘴流水速率小于灌水的速率,那么一旦漏斗满了,灌水就需要暂停并等待漏斗腾空。所以,漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着系统允许该行为的最大频率。下面我们使用代码来描述单机漏斗算法。#coding:utf8 importtime classFunnel(obj
限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时,如何阻止计划外的请求继续对系统施压,这是一个需要重视的问题。老钱在这里用“断尾求生”形容限流背后的思想,当然还有很多成语也表达了类似的意思,如弃卒保车、壮士断腕等等。除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。比如在UGC社区,用户的发帖、回复、点赞等行为都要严格受控,一般要严格限定某行为在规定时间内允许的次数,超过了次数那就是非法行为。对非法行为,业务必须规定适当的惩处策略。如何使用Redis来实现简单限流策略?首先我们来看一个常见的简单的限流策略。系统要限定用户的某个行为在指定的时间里只能允许发生N
上一节我们学会了使用HyperLogLog数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在HyperLogLog结构里面了,它就无能为力了,它只提供了pfadd和pfcount方法,没有提供pfcontains这种方法。讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户
在开始这一节之前,我们先思考一个常见的业务问题:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的UV数据,然后让你来开发这个统计模块,你会如何实现?如果统计PV那非常好办,给每个网页一个独立的Redis计数器就可以了,这个计数器的key后缀加上当天的日期。这样来一个请求,incrby一次,最终就可以统计出所有的PV数据。但是UV不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的ID,无论是登陆用户还是未登陆用户都需要一个唯一ID来标识。你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的set集合来存储所有当
在我们平时开发过程中,会有一些bool型数据需要存取,比如用户一年的签到记录,签了是1,没签是0,要记录365天。如果使用普通的key/value,每个用户要记录365个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis提供了位图数据结构,这样每天的签到记录只占据一个位,365天就是365个位,46个字节(一个稍长一点的字符串)就可以完全容纳下,这就大大节约了存储空间。位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是byte数组。我们可以使用普通的get/set直接获取和设置整个位图的内容,也可以使用位图操作getbit/setbit等将byte数组看成「位数
我们平时习惯于使用Rabbitmq和Kafka作为消息队列中间件,来给应用程序之间增加异步消息传递功能。这两个中间件都是专业的消息队列中间件,特性之多超出了大多数人的理解能力。使用过Rabbitmq的同学知道它使用起来有多复杂,发消息之前要创建Exchange,再创建Queue,还要将Queue和Exchange通过某种规则绑定起来,发消息的时候要指定routing-key,还要控制头部信息。消费者在消费消息之前也要进行上面一系列的繁琐过程。但是绝大多数情况下,虽然我们的消息队列只有一组消费者,但还是需要经历上面这些繁琐的过程。有了Redis,它就可以让我们解脱出来,对于那些只有一组消费者的消
分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。(Wiki解释:所谓原子操作原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何contextswitch线程切换。)这个时候就要使用到分布式锁来限制程序的并发执行。Redis分布式锁使用非常广泛,它是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具体到细节的使用上往往并不完全正确。分布式锁分布式锁本质上要实
RedisCluster是Redis的亲儿子,它是Redis作者自己提供的Redis集群化方案。相对于Codis的不同,它是去中心化的,如图所示,该集群有三个Redis节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。RedisCluster将所有数据划分为16384的slots,它比Codis的1024个槽划分的更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像Codis,它不需要另外的分布式存储来存储节点槽位信息。当RedisCluster的客户端来连接
在大数据高并发场景下,单个Redis实例往往会显得捉襟见肘。首先体现在内存上,单个Redis的内存不宜过大,内存太大会导致rdb文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实例内存往往都是受限的。其次体现在CPU的利用率上,单个Redis实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作压力会非常大。正是在这样的大数据高并发的需求之下,Redis集群方案应运而生。它可以将众多小内存的Redis实例综合起来,将分布在多台机器上的众多CPU核心的计算能力聚集到一起,完成海量数据存储和高并发读写操作。Codis是Red
目前我们讲的Redis还只是主从方案,最终一致性。读者们可思考过,如果主节点凌晨3点突发宕机怎么办?就坐等运维从床上爬起来,然后手工进行从主切换,再通知所有的程序把地址统统改一遍重新上线么?毫无疑问,这样的人工运维效率太低,事故发生时估计得至少1个小时才能缓过来。如果是一个大型公司,这样的事故足以上新闻了。所以我们必须有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行从主切换,程序可以不用重启,运维可以继续睡大觉,仿佛什么事也没发生一样。Redis官方提供了这样一种方案——RedisSentinel(哨兵)。我们可以将RedisSentinel集群看成是一个ZooKeeper集群,它是集
很多企业都没有使用到Redis的集群,但是至少都做了主从。有了主从,当master挂掉的时候,运维让从库过来接管,服务就可以继续,否则master需要经过数据恢复和重启的过程,这就可能会拖很长的时间,影响线上业务的持续服务。在了解Redis的主从复制之前,让我们先来理解一下现代分布式系统的理论基石——CAP原理。CAP原理CAP原理就好比分布式领域的牛顿定律,它是分布式存储的理论基石。自打CAP的论文发表之后,分布式存储中间件犹如雨后春笋般一个一个涌现出来。理解这个原理其实很简单,本节我们首先对这个原理进行一些简单的讲解。C-Consistent,一致性A-Availability,可用性P-
Redis是一个非常耗费内存的数据库,它所有的数据都放在内存里。如果我们不注意节约使用内存,Redis就会因为我们的无节制使用出现内存不足而崩溃。Redis作者为了优化数据结构的内存占用,也苦心孤诣增加了非常多的优化点,这些优化也是以牺牲代码的可读性为代价的,但是毫无疑问这是非常值得的,尤其像Redis这种数据库。32bitvs64bitRedis如果使用32bit进行编译,内部所有数据结构所使用的指针空间占用会少一半,如果你对Redis使用内存不超过4G,可以考虑使用32bit进行编译,可以节约大量内存。4G的容量作为一些小型站点的缓存数据库是绰绰有余了,如果不足还可以通过增加实例的方式来解