一 redis 线程模型

二 redis 数据类型

2.1 string

场景:
做简单的kv缓存
数据结构

  • int 可以存储long类型的整数
    • 保存的是可以用 long 类型表示的整数值
    • 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw
  • embstr 编码的简单动态字符串
    • 保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
    • embstr的使用只分配一次内存空间(因此redisObject和sds是连续的
    • 因此与raw相比embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便
    • embstr的坏处,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读
    • embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节
  • raw 简单动态字符串(SDS)

    • 保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
    • 数据结构是 free =空余的空间 len=使用的空间长度 buf=记录我们的字符串
    • 获取长度很容易(len)不需要遍历统计
    • 杜绝缓冲区溢出,分配空间会预判预留空间(free),即操作前先比较空间是否足够,不够则分配空间
    • 减少修改字符串时,带来的内存重分配次数
    • 惰性空间释放
    • 二进制安全,我们都知道C语言的字符串是靠空字符作为结尾的,而sds 是靠字符len长度结尾的,因此他可以存储任意的字符,即可以存储二进制文件

      2.2 hash

      场景:
      一般存做结构化的数据,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值
      数据结构
  • zipList 压缩列表

    • 哈希保存的每一个key value字符串长度都小于64字节 且 key-value 总数量小于512个,则使用压缩列表,反之使用字典表
  • hashTable

    2.3 list

    场景:
    有序列表 微博,某个大v的粉丝,就可以以list的格式放在redis里去缓存,可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询
    数据结构**:

  • zipList 压缩列表

  • linkedlisk 双向链表
  • quicklist 是ziplist组成的双向链表,它的每个节点都是一个 ziplist。

    2.4 set

    场景:
    无序集合,自动去重,可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁。
    数据结构**:

  • intset 整数集合

  • hashTable

    2.5 sorted set

    场景:
    排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序
    数据结构**:

  • zipList 压缩列表

  • skipList 跳跃表

    三 redis 过期策略

  1. 定期删除+惰性删除
    1. 定期删除:redis 默认每隔100毫秒随机抽取过期的key,检查是否过期,过期则删除。
    2. 惰性删除:即每次查询key的时候判断是否过期,过期则删除
  2. 走内存淘汰机制(redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略)

    1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错(一般不适用)
    2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
    3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key(一般不适用)
    4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(一般不适用)
    5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
    6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

      3.1 手写LRU算法

      1. //JDK LinkedHashMap来实现
      2. public class LRUCache extends LinkedHashMap<Integer, Integer>{
      3. private int cacheSize;
      4. // 这里就是传递进来最多能缓存多少数据
      5. public LRUCache(int capacity){
      6. //设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
      7. super(capacity, 0.75f, true);
      8. this.cacheSize = capacity;
      9. }
      10. public int get(int key){
      11. return super.getOrDefault(key, -1);
      12. }
      13. public void put(int key, int value){
      14. super.put(key, value);
      15. }
      16. @Override
      17. protected boolean removeEldestEntry(java.util.Map.Entry<Integer, Integer> eldest) {
      18. //当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
      19. return this.size() > cacheSize;
      20. }
      21. }

      四 redis 承载10万+ qps

      我们都知道mysql搞高并发,就算通过一系列复杂的分库分表,订单系统,事务要求的,QPS到几万,比较高了。当然redis单机版也不能搞定搞并发,比如qps10万+。除非机器很好,然后数据操作简单是有可能的。

      4.1 读写分离

      一般来说,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千,一两千大量的请求都是读,一秒钟二十万次读

  3. 一主多从,主负责写并且将数据同步复制到其他的slave节点,从节点负责读。所有的读请求全部走从节点。

  4. 主从同步机制
    1. 当启动一个slave node的时候,它会发送一个PSYNC命令给 master node
    2. 如果是第一次连接,则master node full resynchronization 进行全量复制
    3. 如果不是一次连接,则master node 部分复制
    4. 开始full resynchronization的时候,master启动一个后台线程,生成一份RDB快照文件,同时将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
    5. slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node
    6. 从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。即 master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制。但是如果没有找到对应的offset,那么就会执行一次full resynchronization
    7. master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了repl-diskless-sync yes repl-diskless-sync-delay 10,等待一定时长再开始复制,因为要等更多slave重新连接过来
    8. slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
  5. 复制流程
    1. slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始 master host和ip是从哪儿来的,redis.conf里面的slaveof配置的
    2. slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接
    3. slave node发送ping命令给master node
    4. 口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证
    5. master node第一次执行全量复制,将所有数据发给slave node
    6. master node后续持续将写命令,异步复制给slave node
  6. 数据同步机制
    1. offset: master和slave都会维护一个offset,master会在自身不断累加offset,slave也会在自身不断累加offset,slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset,它不只是用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况
    2. backlog:master node有一个backlog,默认是1MB大小,master node给slave node复制数据时,也会将数据在backlog中同步写一份,backlog主要是用来做全量复制中断候的增量复制的
    3. master run id:info server,可以看到master run id如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制,如果需要不更改run id重启redis,可以使用redis-cli debug reload命令
    4. psync:从节点使用psync从master node进行复制,psync runid offset,master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制
  7. 全量复制
    1. master执行bgsave,在本地生成一份rdb快照文件
    2. master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数
    3. 对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
    4. master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node
    5. client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败
    6. slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
    7. 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF
    8. rdb生成、rdb通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间.如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟
  8. 增量复制
    1. 如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制
    2. master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB
    3. sater就是根据slave发送的psync中的offset来从backlog中获取数据的
  9. heartbeat,主从节点互相都会发送heartbeat信息,master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat
  10. 异步复制,master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node

    4.2 99.99%高可用

    99.99%,公式=系统可用的时间/系统故障的时间,365天,在365天 * 99.99%的时间内,那就是高可用性。

  11. sentinal 是redis集群架构集群组件

    1. 集群监控,负责监控redis master和slave进程是否正常工作
    2. 消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
    3. 故障转移,如果master node挂掉了,会自动转移到slave node上
    4. 配置中心,如果故障转移发生了,通知client客户端新的master地址
  12. 哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作
    1. 故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
    2. 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了
  13. 哨兵的核心知识
    1. 哨兵至少需要3个实例,来保证自己的健壮性
    2. 哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性
    3. 对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练
  14. 哨兵集群必须部署2个以上节点

为什么redis哨兵集群只有2个节点无法正常工作?
2个哨兵实例,Configuration: quorum=1,master宕机,s1和s2中只要有1个哨兵认为master宕机就可以还行切换,同时s1和s2中会选举出一个哨兵来执行故障转移,且需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority(大部分)就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移。但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行。

  1. +----+ +----+
  2. | M1 |----- | R1 |
  3. | S1 | | S2 |
  4. +----+ +----+

3节点哨兵集群,Configuration: quorum = 2,如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和S3可以一致认为master宕机,然后选举出一个来执行故障转移,同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移

  1. +----+
  2. | M1 |
  3. | S1 |
  4. +----+
  5. |
  6. +----+ | +----+
  7. | R2 |----+----| R3 |
  8. | S2 | | S3 |
  9. +----+ +----+

五 redis数据丢失(异步复制&集群脑裂)

  1. 异步复制导致的数据丢失
    1. 因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了
  2. 脑裂导致的数据丢失
    1. 脑裂:master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着。此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。此时集群里就会有两个master,也就是所谓的脑裂。
    2. 此时虽然slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。

解决方式:至少有1个slave,数据复制和同步的延迟不能超过10秒
min-slaves-to-write 1
min-slaves-max-lag 10

  1. 减少异步复制的数据丢失:,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内
  2. 减少脑裂的数据丢失:如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失10秒的数据

    六 sdown和odown转换机制

    sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机。
    odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
    sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
    sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机。

    七 自动发现机制

  3. 哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在

  4. 每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的sentinel:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置
  5. 每个哨兵也会去监听自己监控的每个master+slaves对应的sentinel:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在
  6. 每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步

    7.1 slave配置的自动纠正

    哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上

    八 slave->master选举算法

    如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave。
    redis 会考虑slave的相关信息

  7. slave跟master断开连接的时长

  8. slave优先级
  9. 复制offset
  10. run id

    8.1 选举过程

  11. 过滤符合条件的slave:

    1. 如果一个slave跟master断开连接已经超过了(down-after-milliseconds的10倍+master宕机的时长),那么slave就被认为不适合选举为master (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
  12. 给slave 排排坐:
    1. 按照slave优先级进行排序,slave priority 越低,优先级就越高
    2. 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
    3. 如果上面两个条件都相同,那么选择一个run id 比较小的那个slave

      8.2 quorum和majority

      每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换。如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换。但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换。

      8.3 configuration epoch

      哨兵会对一套redis master+slave进行监控,有相应的监控的配置。执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。

      8.4 configuraiton传播

      哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制。这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的。其他的哨兵都是根据版本号的大小来更新自己的master配置的

      九 redis 持久化

      RDB持久化机制,对redis中的数据执行周期性的持久化。AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制

通过RDB或AOF,都可以将redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说云服务。如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整

RDB 优点:

  1. RDB会生成多个数据文件,每个文件代表某一个时刻redis中的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据
  2. RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可
  3. 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速

RDB 缺点:

  1. 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据
  2. RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。

AOF 优点:

  1. AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据
  2. AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
  3. AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,redis 后台线程会创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
  4. AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

AOF 缺点:

  1. 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
  2. AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
  3. 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。

RDB & AOF 选择

  1. 不要仅仅使用RDB,因为那样会导致你丢失很多数据
  2. 也不要仅仅使用AOF,因为那样有两个问题,1.AOF做冷备没有RDB做冷备恢复速度更快; 2. RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
  3. 综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复

    十 Redis 集群模式

    redis cluster(多master + 读写分离 + 高可用)。我们只要基于redis cluster去搭建redis集群即可,不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用。(redis cluster vs. replication + sentinal)如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了
    replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了。
    redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster。

在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,比如16379。16379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线。cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。cluster bus用了另外一种二进制的协议,主要用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

10.1 一致性算法

10.1.1 hash 算法

对key计算hash值,然后对节点数量取模计算。例如:3个节点,则取模3,其结果为0、1、2 。所以结果打到这三个节点。
缺点:
如果某一个节点宕机,就导致大部分数据全部失效(几乎是100%数据)且需要重新取模计算,再分布到其他节点上。导致大量缓存重建

10.1.2 一致性 hash 算法

对key计算hash值,然后会用hash值对应圆环上的节点。看看最终hash值落到这个圆环的那个节点。key落在圆环上后,就会顺时针旋转去寻找距离自己最近的一个节点。一致性hash算法保证任何一个master宕机,只会损失该节点的数据。
缺点:
可能集中在某个hash区间的值特别多,那么会导致大量的数据都涌入同一个master内,造成master的热点问题,性能出现瓶颈。

10.1.3 一致性hash算法虚拟节点

给每个master都做了均匀分布的虚拟节点。这样的话,每个区间内大量的数据会均匀的分布不同的节点内,而不是顺时针去走,避免了全部涌入同一个key。

10.1.4 redis cluster的hash slot算法

redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot。hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。移动hash slot的成本是非常低的。

任何一台机器宕机,另外两个节点不影响的,因为key找的hash slot,二不是找的机器。客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现

10.2 节点通信协议

集中式:将集群元数据(节点信息,故障,等等)集中存储在某个节点上,互相之间不断通信,保持整个集群所有节点的数据是完整。
好处在于:元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到;
不好在于:所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力
流言协议gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后

10000端口
每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口
每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong
交换的信息
故障信息,节点的增加和移除,hash slot信息,等等

gossip协议含多种消息,包括ping,pong,meet,fail,等等

  1. meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信redis-trib.rb add-node其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群
  2. ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据。每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
    1. ping很频繁,而且要携带一些元数据,所以可能会加重网络负担,每个节点每秒会执行10次ping,每次会选择5个最久没有通信的其他节点。当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率。每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换。至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
  3. pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新
  4. fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了

10.3 高可用性与主备切换原理

Redis cluster的高可用的原理,几乎跟哨兵是类似的:

  1. 判断节点宕机:如果一个节点认为另外一个节点宕机,那么就是pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown。在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail。如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail
  2. 从节点过滤:对宕机的master node,从其所有的slave node中,选择一个切换成master node。检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master。这个也是跟哨兵是一样的,从节点超时过滤的步骤
  3. 从节点选举:哨兵:对所有从节点进行排序,slave priority,offset,run id。每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master。从节点执行主备切换,从节点切换为主节点
  4. 与哨兵比较:整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能。

十一 缓存雪崩

缓存全部不可用,导致数据库打死了。

  1. 事前: redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
  2. 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
  3. 事后:redis持久化,快速恢复缓存数据

    十二 缓存穿透

    大量请求不存在的数据,即缓存中查不到,打死数据库。
    布隆选择器真香定律

    十三 数据一致性问题

    最经典的缓存+数据库读写的模式,cache aside pattern

  4. 读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

  5. 更新的时候,先删除缓存,然后再更新数据库
    1. 其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算

问题1:最初级的缓存不一致问题以及解决方案
先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
解决思路:
先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致
因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

问题2:读写并发复杂的数据不一致问题分析
数据变更,先删除缓存,然后去修改数据库,此时还没修改完成,另一请求去读缓存,然后发现缓存为空,去查询数据库,查到了修改前的旧数据,放到了缓存中,然后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样。

其实你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。

解决思路: ****据库与缓存更新与读取操作进行异步串行化

  1. 更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中,读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中
  2. 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
  3. 此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成
  4. 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个读请求操作进去了,直接等待前面的更新操作请求完成即可
  5. 待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
  6. 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值

该解决方案要注意的问题:

  1. 读请求长时间阻塞
    1. 读请求长时阻塞:由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的
    2. 因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据.这个时候就导致读请求的长时阻塞
    3. 一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会阻塞多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的。
    4. 如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少
    5. 其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了。一秒500的写操作,5份,每200ms,就100个写操作。单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成
  2. 读请求并发量过高
    1. 这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值。但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。
    2. 按1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作。如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存。
    3. 一般来说,1:1,1:2,1:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回。在同一时间最多hang住的可能也就是单机200个读请求,同时hang住单机hang200个读请求,还是ok的。
    4. 1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万
  3. 多服务实例部署的请求路由,可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上。
  4. 热点商品的路由问题,导致请求的倾斜。万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大

就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大。但是的确可能某些机器的负载会高一些。

十四 并发竞争问题

某个时刻,多个系统实例都去更新某个key,分布式锁,确保同一时间,只能有一个系统实例在操作某个key,别人都不允许读和写,每次写之前,先判断一下,当前这个value的时间戳是否比缓存里的value的时间戳要新,如果新,则更新。

十五 公司redis 架构

  1. redis cluster,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。
  2. 机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给redis进程的是10g内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。5台机器对外提供读写,一共有50g内存。因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务
  3. 你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。目前高峰期每秒就是3500左右的请求量

    十六 热点key 问题

    16.1 发现热点key

    预估

  4. 针对业务提前预估出访问频繁的热点key,例如秒杀商品业务中,秒杀的商品都是热点key。

  5. 当然并非所有的业务都容易预估出热点key,可能出现漏掉或者预估错误的情况。

客户端嵌入代码

  1. 在访问redis客户端之前加入一行代码进行数据统计,统计方式多种多样,有本地计数、发消息单独处理统计等。但是这种方式会对客户端代码造成入侵。

在Proxy(代理层)收集

  1. proxy层统一入口做统计,对业务代码无入侵。

redis命令
redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可,操作方便。该参数在执行的时候,如果key比较多,执行起来比较慢
日志采集分析
将日志通过agent采集,将采集的日志进行分析,对业务代码无入侵。

16.2 解决方案

使用二级缓存
可以使用 guava-cache或hcache,发现热点key之后,将这些热点key加载到JVM中作为本地缓存。访问这些key时直接从本地缓存获取即可,不会直接访问到redis层了,有效的保护了缓存服务器。
备份(不建议)
把热点key在多个redis上都存一份,当有热key请求进来的时候,在redis中随机选取一台,进行访问取值,返回数据。但是这种情况维护代价非常大,假设有100个备份KEY,那么在删除或者更新时,也需要更新100个KEY,所以这种方案不是很推荐。
有赞透明多级缓存解决方案(TMC)

十七 从海量key里查询出某一固定前缀的key

KEYS指令一次性返回所有匹配的key
键的数量过大会使得服务卡顿
scan xxx match xxxx count xxx 基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
以0 作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
不保证每次执行都返回某个给定数量的元素,支持模糊查询
一次返回的数量不可控,只能是大概率符合count参数

十八 分布式锁

  1. 互斥性
  2. 安全性
  3. 死锁
  4. 容错

方法一:两步操作不能保证原子性
SETNX key value :如果key 不存在,则创建并赋值。
EXPIRE xxxx :设置过期时间
方法二:
SET KEY value [EX seconds] [PX milliseconds][NX|XX]
EX seconds :设置键的过期时间为second秒
PX milliseconds: 设置键的过期时间为milliseconds毫秒
NX : 只在键不存在时,才对键进行设置操作
XX : 只在键已存在时,才对键进行设置操作
SET: 操作成功时,返回OK,否则返回nil

十九 大量的key同时过期

集中过期,由于清楚大量的key很耗时,会出现短暂的卡顿现象,过期时间加上随机值