redis原理(2)

1redis原理

为什么是单线程

Redis 的数据存储在内存中,如果数据全都在内存里,单线程的去操作就是效率最高的。

因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。因为上下文切换所花费的时间远大于直接从内存中读取数据所花费的时间

Redis 用单个 CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案。

Redis 单线程如何处理那么多的并发客户端连接?

redisio多路复用:

持久化:RDB和AOF

key值得策略:

# volatile-lru -> 利用LRU算法移除设置过过期时间的key 最近时间不经常用的
# allkeys-lru -> 利用LRU算法移除任何key 使用频率最低
# volatile-lfu -> 利用lfu算法移除设置过过期时间的key 使用频率最低
# allkeys-lfu -> 利用lfu算法移除任何key 最近时间不经常用的
# volatile-random -> 移除设置过过期时间的随机key
# allkeys-random -> 在所有键值对中,随机移除某个key
# volatile-ttl -> 移除即将过期的key(minor TTL)
# noeviction -> 不移除任何可以,只是返回一个写错误 (默认策略)

7种数据类型:

  String,hash,list,

集合set(无序不重复)

场景:存放数据不重复,以及多个数据源的交集和并集场景。

hash(购物车):

redis数据结构就是map ,而 hash就是在map上又加了一个map (<key,<key,value>>)

 购物车是按照店铺分类商品的

(<用户id,<店铺id,List<good> 商品集合>>)

集合 zset (加了权重分数,排行榜场景应用)

Zset是有序的,自动去重的集合类型,Zset数据结构底层实现为,字典(dict)+跳表(skiolist),当数据比较少时,用ziplist编码结构存储。

元素个数超过128,将skiplist编码

zset-max-listpack-entries 128

单个元素大小超过64byte,将用skiplist
zset-max-listpack-value 64

具体原理请看数据结构篇章

bitmap(位图):

bit是计算机最小存储单元,bitMap提供了对bit位的设置,存储只有0或者1。 8个bit组成一个byte,所以bitmap本身极大节省了存储空间。

 

应用场景:(是,否,两种状态下才能应用)
 

海量用户系统的日活,周活的统计

    根据日期,每天对应一个key。用户登录时将用户存到对应位图中,采用bitcount即可获取当 日登录用户数。

统计最近7天连续登录的用户数

    根据日期,每天对应一个key。若想知道最近7天连续登录的用户个数,获取7天对应的    bitmap的key,进行求交集

海量用户会员判断

判断是否状态的记录,例如签到

总结:根据以上应用场景也能发现,都是  两个状态(是或不是) 这种情况下才能使用

BitMap 的操作命令有:
SETBIT:向指定位置(offset)存入一个 0 或 1
GETBIT:获取指定位置(offset)的 bit 值
BITCOUNT:统计 BitMap 中值为 1 的 bit 位的数量
BITFIELD:操作(查询、修改、自增)BitMap 中的 bit 数组中的指定位置(offset)的值
 

BITFIELD_RO:获取 BitMap 中的 bit 数组,并以十进制形式返回
BITOP:将多个 BitMap 的结果做位运算(与、或、异或)
BITPOS:查找 bit 数组中指定范围内第一个 0 或 1 出现的位置

举例:2022-07-25这天 登录的用户记录一下  (统计的话版本6.0才能用)

    //用户主键
        int userId1 = 0;
        int userId2 = 2;
        int userId3 = 3;
        //用户1 2022-07-25登录了
         redisTemplate.opsForValue().setBit("2022-07-25", userId1, true);
        //用户2 2022-07-25登录了
         redisTemplate.opsForValue().setBit("2022-07-25", userId2, true);
        Boolean bit = redisTemplate.opsForValue().getBit("2022-07-25", userId2);
        System.out.println(bit);

geo(地理位置:)

计数器:

INCR article:readcount:101                            文章阅读数 场景

INCR article:readcount:{文章id}

get article:readcount:{文章id}

2 redis主从复制(读写分离)

优缺点:

优点:

  1.   读写分离,减轻主库压力
  2. 数据容灾,减轻主服务器压力

缺点:

  1. 只能一主多从
  2. 数据一致性

读写分离:主库主写,从库只读    主库可读可写,但是从库只能读,写报错

数据容灾:我们知道redis持久化默认是RDB方式,所以会丢数据,但是从库的数据会相对比较完整。

数据一致性:因为主库会异步将数据文件发送给从库,从库读取获取数据。所以从库越多性能就会下降一些,并且如果数据量比较大,异步还未同步给从库的时候就获取数据,会获取不到,本人实测,十万个对象集合保存就会发生。

解决办法:

一些比较数据不是很重要,或者是数据设置和取数据得间隔时间比较长的场景。都是没有问题的。

加一层保险还可以获取从库为null就在获取一次主库数据,在返回结果

对于比较及时的数据响应,重要的数据,秒杀场景的数据,我们要直接获取主库数据

因为;只要设计到并发安全会对结果产生影响的情况,就不要用从库了,例如库存超卖

明明主库已经卖出去10件了,但是从库只记录到5件。

原理:

总结:

  1. 全量复制
  2. 增量复制
  3. 只要复制修改数据记录集,get数据不同步
  4. 只要是持久化或同步数据都是异步---高效的原因

全量复制当我们设置redis主从关系的时候,我们的从库就会像主库发送一条命令SYNC,主库就会生成RDB文件,全量同步数据给从库。从库读取文件同步数据。通过日志可以查看什么时候开始同步,同步文件大小。

增量复制当我们的用户往主库持续增加数据时,我们不可能每次都同步全部数据,只会同步增量的数据(提升效率)。

增量复制如何实现呢?如何知道每个从库的复制节点呢?

主要是由三部分实现:

  1. 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
  2. 主服务器的复制积压缓冲区(replication backlog)
  3. 服务器的运行ID(run ID)

复制偏移量

执行复制的双方,主服务器和从服务器都会维护一个复制偏移量,主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加N。从服务器每次收到主服务器传播来的N个字节的数据的时,就将自己的复制偏移量的值加N。

如果主从服务器处于一致状态,那么从服务器两者的偏移量总是相同

相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态
说明,从库未收到文件或者从库下线又上线了,中间错过文件了。

既然知道了主从服务器是否处于一致状态,那么断线重新连接是该完整重同步还是部分重同步?如果部分重同步,主服务器又如何补偿从服务器丢失的那部分数据呢?

复制积压缓冲区

复制积压缓冲区时由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。

当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将命令入队到复制积压缓冲区里面,如下:

 主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量

当从服务器重新连接上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个偏移量来决定对从服务器执行何种同步操作:

    如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主从服务器将对从服务器执行部分重同步操作
    如果offset偏移量之后的部分数据已经不存在于复制积压缓冲区(部分重同步就无法保证主从数据一致性),那么主服务器将对
    从服务器执行完整重同步操作
 

主服务器又怎么区分从服务器呢?

服务器运行ID

每个Redis服务器,不论主服务器还是从服务器,都会有自己的运行ID。运行ID实在服务器启动的时候自动生成,由40个随机的十六进制字符组成

当从服务器对主服务器进行初次复制时,主服务器将会将自己的运行ID传送给从服务器,而从服务器会将这个运行ID保存起来

当从服务器断线并重新连接上主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:

    如果从服务器保存的运行ID与当前连接的ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。
    如果从服务器保存的运行ID与当前连接的主服务器运行ID并不相同,那么说明从服务器断线之前复制的主服务器不是当前连接,主服务器将对从服务器执行完整重同步

那么如果数据丢失咋办?那不是主服务器的偏移量和从服务器偏移量就不一样的?
心跳检测

从服务器默认会以每秒一次的频率,向主服务器发送心跳命令

REPLCONF ACK <replication_offset>(从服务器当前的复制偏移量)

作用:

    检测主从服务器的网络连接状态
    辅助实现min-slaves选项(防止主服务器在不安全的情况下执行写命令,保证至少有多少个从服务器的时候,才能执行写命令)
    检测命令丢失
 

3哨兵模式

架构图:

优缺点和主从一样,多了一项优点:

  1. 在主从复制基础上,哨兵能够监控redis,自动升级主从,排除故障,为了防止哨兵自己挂掉,在加一个哨兵集群更加稳健。

什么是哨兵模式: 

哨兵模式是Redis的高可用方式,哨兵节点是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。 哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点挂掉时,哨兵会第一时间感知到,并且在slave节点中重新选出来一个新的master,然后将新的master信息通知给client端,从而实现高可用。这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息。

哨兵的主要工作任务:

(1)监控:哨兵会不断地检查你的Master和Slave是否运作正常。

(2)提醒:当被监控的某个Redis节点出现问题时,哨兵可以通过 API 向管理员或者其他应用程序发送通知。

(3)自动故障迁移:当一个Master不能正常工作时,哨兵会进行自动故障迁移操作,将失效Master的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master;当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用新Master代替失效Master。

流程:

sentinel集群(相互监控)-------监控------->redis集群(master+slave)--------master(挂了以后)---选举--slave变master

监控redis集群sentinel工作方式:

1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。

2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值(默认30秒), 则这个实例会被 Sentinel 标记为主观下线。 

3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。 

4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。

5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。

6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。

7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。 

若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

sentinel选举(slave--升级---master):

   基于Raft算法选举领头sentinel: (只能有一台 sentinel 来完成故障转移,一定要是基数集群,避免票数持平重新选举)

到现在为止,已经知道了master客观下线,那就需要一个sentinel来负责故障转移,那到底是哪个sentinel节点来做这件事呢?需要通过选举实现,具体的选举过程如下:

(1)判断客观下线的sentinel节点向其他 sentinel 节点发送 SENTINEL is-master-down-by-addr ip port current_epoch runid

    注意:这时的runid是自己的run id,每个sentinel节点都有一个自己运行时id

(2)目标sentinel回复是否同意master下线并选举领头sentinel,选择领头sentinel的过程符合先到先得的原则。举例:sentinel1判断了客观下线,向sentinel2发送了第一步中的命令,sentinel2回复了sentinel1,说选你为领头,这时候sentinel3也向sentinel2发送第一步的命令,sentinel2会直接拒绝回复

(3)当sentinel发现选自己的节点个数超过 majority 的个数的时候,自己就是领头节点

(4)如果没有一个sentinel达到了majority的数量,等一段时间,重新选举
 

故障转移:

有了领头sentinel之后,下面就是要做故障转移了,故障转移的一个主要问题和选择领头sentinel问题差不多,到底要选择哪一个slaver节点来作为master呢?按照我们一般的常识,我们会认为哪个slave节点中的数据和master中的数据相识度高哪个slaver就是master了,其实哨兵模式也差不多是这样判断的,不过还有别的判断条件,详细介绍如下:

(1)在进行选择之前需要先剔除掉一些不满足条件的slaver,这些slaver不会作为变成master的备选

        剔除列表中已经下线的从服务
        剔除有5s没有回复sentinel的info命令的slave
        剔除与已经下线的主服务连接断开时间超过 down-after-milliseconds * 10 + master宕机时长 的slaver

(2)选主过程:

    ① 选择优先级最高的节点,通过sentinel配置文件中的replica-priority配置项,这个参数越小,表示优先级越高

    ② 如果第一步中的优先级相同,选择offset最大的,offset表示主节点向从节点同步数据的偏移量,越大表示同步的数据越多,(选举数据保存最全的从机)

    ③ 如果第二步offset也相同,选择run id较小的
 

主机宕机到能用的时间间隔:30秒标记主观下线时间 + 客观下线时间 + 选举时间  =宕机时间  (大于30秒)

down-after-milliseconds 5秒-10秒 (调小一点呢)

replica-priority 优先级可以设置一下,免去一些步骤。

简单的总结一下:

4集群模式

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5500号哈希槽.
  • 节点 B 包含5501 到 11000 号哈希槽.
  • 节点 C 包含11001 到 16384号哈希槽.

这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.

在我们例子中具有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用.

然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了

不过当B和B1 都失败后,集群是不可用的.

集群的故障检测与故障转恢复机制:

集群的故障检测:

        Redis集群的故障检测是基于gossip协议的,集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。

(1)主观下线(pfail):当节点A检测到与节点B的通讯时间超过了cluster-node-timeout 的时候,就会更新本地节点状态,把节点B更新为主观下线。

    主观下线并不能代表某个节点真的下线了,有可能是节点A与节点B之间的网络断开了,但是其他的节点依旧可以和节点B进行通讯。

(2)客观下线:

        由于集群内的节点会不断地与其他节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点,因此集群内的节点会不断收到下线报告。

        当半数以上的主节点标记了节点B是主观下线时,便会触发客观下线的流程(该流程只针对主节点,如果是从节点就会忽略)。将主观下线的报告保存到本地的 ClusterNode 的结构fail_reports链表中,并且对主观下线报告的时效性进行检查,如果超过 cluster-node-timeout*2 的时间,就忽略这个报告,否则就记录报告内容,将其标记为客观下线。

        接着向集群广播一条主节点B的Fail 消息,所有收到消息的节点都会标记节点B为客观下线。
集群地故障恢复:

当故障节点下线后,如果是持有槽的主节点则需要在其从节点中找出一个替换它,从而保证高可用。此时下线主节点的所有从节点都担负着恢复义务,这些从节点会定时监测主节点是否进入客观下线状态,如果是,则触发故障恢复流程。故障恢复也就是选举一个节点充当新的master,选举的过程是基于Raft协议选举方式来实现的。

投票选举:

(1)节点排序:

        对通过过滤条件的所有从节点进行排序,按照priority、offset、run id排序,排序越靠前的节点,越优先进行选举。

        priority的值越低,优先级越高
        offset越大,表示从master节点复制的数据越多,选举时间越靠前,优先进行选举
        如果offset相同,run id越小,优先级越高
 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值