提到 Redis,也许大家第一印象是:高性能的 K-V 缓存。其除了缓存业务上的热点数据还能做队列以及分布式锁。可大部分的我们在使用的时候也许都是公司封装好的 Redis,对于整个 Redis 的集群以及内部核心实现一知半解。只专注业务开发,那久而久之,在个人成长上变得帮组甚少。最明显的就是当业务量级上来后,在极端情况下,许多问题暴露出来,如果没有对 Redis 的底层有深入的了解,很难快速定位并解决问题
Redis 基础数据结构
- string
- Redis里的Redis的字符串是动态字符串,是可以修改的。类似于Go里面的切片Silence,如果长度不够则自动扩容,至于如何扩容:length小于1M的时候,扩容规则讲目前的字符串翻倍;如果大雨1M的话,则每次只会扩容1M,直到512M。
- list
- Redis里的list是一个链表,由于链表本身插入和删除比较块,但是查询的效率比较低,所以常常被用于做异步队列
- Redis里的List设计非常牛逼,当数据量比较小的时候,数据结构是压缩链表,如果数据量比较多的时候就是快速链表
- 可运用场景: 异步队列,使用rpush/lpush操作队列,使用lpop和rpop出队列
- set
- Redis中的set是一个无序Map,由于Go中没有set结构,所以这里只能类比Java中的HashSet。set底层也是一个Map结构,只是里面的Value是一个NULL,由于set的特性可以用于去重逻辑
- 可运用场景:活动抽奖去重
- hash
- Redis中的字典类型大家不陌生,也许其他语言都有这种结构,不同的稍微提一点就是hash的扩容rehash过程中和Go里面的设计颇有类似,就是维护了两个hash结构,如果需要扩容的时候,就把新的数据写入新字典中,然后后端起一个线程来逐步迁移,总体上来说就是采用了空间换时间的思想
- 可运用场景:记录业务中的不同用户/不同商品/不同场景的信息:如某个用户的名称,或者用户的历史行为
- zset
- Redis中的zset是一个比较特殊的数据结构(跳跃列表),也就是我们了解到的跳表,底层由于set的特性保证了value唯一,同时也给了value一个得分,所谓的有序就是根据这个的得分排序。
- 至于跳跃表如何插入,其实内部采用了一个随机策略:L0:100%-L2:50%-L3:25%-....Ln:(n-1)value/2%
- 可运用场景: 榜单,总榜,热榜
Redis 进阶使用
布隆过滤器
redis在4.0以后支持布隆过滤,给Redis提供了强大的去重功能。在业务中,我们可能需要查询数据库判断历史数据是否存在,数据库的并发能力有限,这个时候我们采用了Redis中的set去做去重,这个时候如果缓存的数据过大,这个时候就需要遍历所以缓存数据,另外如果我们的历史数据缓存写不下,终究要去查询数据库,这个时候我们就可以使用布隆过滤器。当然布隆过滤器精确度不是100%准确,对于存在的数据也许这个值不一定存在。当然如果不存在肯定100%不存在了。
命令使用
csharp
bf.add #添加元素
bf.exists #判断元素是否存在
bf.madd #批量添加
bf.mexists #批量判断是否存在
原理
布隆过滤的组成可以当作一个位数组和几个无偏的hash函数(计算的hash比较均匀),每次添加key的时候,会把key通过多次hash来计算所的到的位置,如果当前位置不是0则表示存在,这也正式它的不准确性。
分布式锁
也许不陌生,现在主流实现分布锁的可以使用ZK或者Redis;本文就简单的介绍一下Redis实现分布式锁;
命令
csharp
setnx lock:mutex ture #加锁
del lock:mutex #删除锁
实现锁的核心就是请求的时候占用这个key,如果其他请求设置失败的时候,即拿不到锁,但是存在一个问题就是如果业务panic或者忘记调用del的话,这样就会产生死锁了;这个时候大家很容易想到我们可以expire一个过期时间,这样就可以保证请求不会一直独占锁并无法释放锁的逻辑;但是业务存在一种情况就是A请求在获取锁后,处理逻辑,由于逻辑过长,这个时候锁到期释放了,A这个时候刚刚处理完成,而B又去改这个数据,这就存在一个锁失效的问题,解决这种问题参考了CAS的方式,对锁设置一个随机数可以理解为版本号,如果释放的时候版本号不一致的时候,则表示数字已经在释放那一刻改掉了。
深入原理
IO模型
Redis是单线程模型(这里的单线程指的是IO和键值对的读写是一个线程完成的),当然如果严谨的说以多线程,之所以多线程不过是在数据备份的时候会fork一个子进程对数据进行从磁盘读取数据并组装RDB,然后同步给slaver节点。问题来了,单线程的模型能这么快?原因很简单Redis本身就是在内存运算,而对于上游的客户端请求,采用了多路复用的原理。Redis会给每一个客户端端套接字都关联一个指令队列。客户端的指令队列通过队列排队来进行顺序处理。同时Reids给米一个客户端的套件字关联一个响应队列。Redis服务器通过响应队列来将指令的接口返回回复给客户端。
Redis IO处理模型
通信协议
Redis采用了Gossip协议作为通信协议。Gossip是一种传播消息的方式,灵感来自于:瘟疫、社交网络等。使用Gossip协议的有:Redis Cluster、Consul、Apache Cassandra等。简单点意思就是类似病毒扩散的方式一样,将信息传播到其他的节点,这种协议效率很高,只需要广播到附件到节点,然后被广播的节点继续做同样的操作即可。当然这种协议也有一个弊端就是:存在浪费。
持久化
RDB
对当前Redis的存储数据进行一次快照(具体如何做和原理,这里不做过多复述)
AOF
日志只记录Redis对内存修改的指令记录;Redis提供了一个bgrewriteaif的指令对AOF进行压缩,原理就是开辟一个子进程对内存进行遍历后转换成一系列对Redis的操作指令,序列化到一个新的AOF的日志文件中。系列化完成后再将发送的增量的AOF日志追加到这个新的AOF的日志中,追加完成后用新的AOF日志代替旧的
混合持久化
Redis大概是4.0之后,采用混合持久化,也就是每次启动时候通过RDB+增量的AOF文件来进行回复;由于增量的AOF仅记录了开始持久化到持久化结束期间发生的增量,这样日志不会太大,性能相对较高
主从同步
Redis的同步方式:主从同步;从从同步(由于全部都由master同步的化,会损耗性能,所部分的slave会通过slave之间进行同步)
同步过程
- 建立连接,然后从库告诉主库我要同步啦,你给我准备好,然后主库和从库说知道了知道了。
- 从库拿到数据了,然后要把他保存到库里。这个时候就会在本地完成数据的加载,这个时候会用到RDB
- 主库把新来的数据,AOF同步给从库
也许有人会提问到如果在同步的时候主挂了或者从挂了,这样怎么办,其实上图主里面维护了一个环形的缓冲区,会记录主写的位置和从读的位置,如果从同步时候挂了,这个时候环形换从区就不会更新,再次重新同步。另外这个缓冲区会进行持久化。
Sentinel
Redis的主从切换通过哨兵来解决。这里哨兵主要解决的就是当主挂了的情况下,如果在短时间内重新选举出一个新的主。
Sentinel集群是一个3-5个节点组成的集群,监听整个redis的集群,如果master发现不可用的时候,则会关闭全部与主的旧的链接,并断开。这个时候sentinel会完成选举和故障转移。新的请求则会到新到master中。
Redis集群工作原理
Redis 集群通过槽指派机制来决定写命令应该被分配到那个节点。整个集群对应的槽是由 16384 大小的二进制数组组成,集群中每个主节点分配一部分槽,每条写命令落到二级制数组中的某个位置,该位置被分配给了那个节点则对应的命令就由该节点去执行。槽指派对应的二进制数组如下图所示:
从上图可以看到节点 1 执行 0 - 4999,节点 2 执行 5000 - 9999,节点 3 执行 9999- 16383, set key1 value1命令通过 CRC16(key1) & 16383 = 8876,即认为该命令最后落到二级制数组的 8876 位置,则该命令最终由节点 2 执行。在比如在节点 2 执行一条命令时,假设通过 CRC 计算后得到的值为 589,则其应该有节点 1 执行,此时命令会发生一个转向操作,将要执行的命令转向到节点 1 上去执行。
集群中每个主节点都会定时发送信息到其他主节点,如果其他主节点在规定时间内响应了发送消息的主节点,则发送消息的主节点认为响应了消息的这些主节点在正常工作,反之则认为响应消息的主节点疑似下线,则发送消息的主节点在其节点上将其标记疑似下线;当集群中超过半数的节点认为某个主节点被标记为疑似下线,则其中某个主节点将疑似下线节点标记为下线状态,并向集群广播一条下线消息,当下线节点对应的从节点接收到该消息时,则从从节点中选举出一个节点作为主节点继续对外提供服务。
Redis为什么变慢了
业务场景中,不知道是否碰到过Redis变慢的情况:
- 执行 SET、DEL 命令耗时也很久
- 偶现卡顿,之后又恢复正常了
- 在某个时间点开始变慢了
原因分析
查看慢查询,由于我本身机器没有慢查询,所以这里看到是空
- 由于Redis在IO操作和对键值对的操作是单线程的,所以在执行的 Redis 命令有可能会导致操作延迟变大:
- 经常使用 O(N) 以上复杂度的命令,,所以使用复杂的命令会对redis的处理变慢,以及CPU过高,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令
- 查询的数据量过大,更多时间花费在数据协议的组装和网络传输过程中。
- 大 key
- 比如一个很大的hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。
- 另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。
- 集中过期,变慢的时间统一,每间隔多久就会发生一波延迟
- 内存使用达到上限,当内存达到内存上限的时候,就不许淘汰一些数据,这个时候也可能导致Redis查询效率低;具体淘汰策略如下:
- allkeys-lru:不管 key 是否设置了过期,淘汰最近最少访问的 key
- volatile-lru:只淘汰最近最少访问、并设置了过期时间的 key
- allkeys-random:不管 key 是否设置了过期,随机淘汰 key
- volatile-random:只随机淘汰设置了过期时间的 key
- allkeys-ttl:不管 key 是否设置了过期,淘汰即将过期的 key
- noeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
- allkeys-lfu:不管 key 是否设置了过期,淘汰访问频率最低的 key
- volatile-lfu:只淘汰访问频率最低、并设置了过期时间 key
- 碎片整理,Redis在4.0版本后会自动整理碎片,而在整理碎片的过长中会消耗CPU的资源,从而影响了请求得到性能
- 网络带宽,Redis集群和业务混部,或者并发量过大以及每次返回的数据也很大,网卡带宽跑满的情况导致网络阻塞
- AOF的频率过高,由于AOF需要将全部的写命令同步,如果同步的间隔比较短,也会影响到Redis的性能
- Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。
Redis安全
- 默认会监听 6379端口,最好在 Redis 的配置文件中指定监听的 IP 地址,更进一步,还可以增加 Redis的ACL访问控制,对客户指定群组,并限限制用户对数据的读写权限。
- 访问Redis尽量走公司代理,由于Redis不慎不支持SSL的链接,所以走公司代理可以保证我们的安全