redis深度

redis做分布式锁

redis的实现方式其实就是占坑,一般使用setnx(set if not exists)指令,只允许一个客户端占坑。

setnx lock:codehole true
做其他事情
del lock:codehole

bug:如果del前有异常,导致del无法调用,就会使得锁得不到释放,就会死锁。

所以考虑给锁加时间,例如加5s,哪怕有异常,5s后也释放

setnx lock:codehole true
expire lock:codehole 5
做其他事情
del lock:codehole

bug1:set和expire不是原子操作,可能执行expire前出问题了,还是会死锁。而且这里不能使用事务来解决,例如线程A先用锁,线程B进来要锁,如果是事务,线程B没抢到锁,是不应该执行expire操作的。

为解决这个问题,redis2.8之后加入了set指令扩展

set lock:codehole true ex 5 nx 
做其他事情
del lock:codehole

为了误删锁,建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断

可以借助Lua脚本实现,实现匹配随机数和删除锁的原子操作。

if redis.call("get",KEYS[1])==ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end

bug2:虽然解决了setnx和expire的问题,但是还有个超时的问题,线程A用时超过5s还没执行完,锁可能会被其他线程抢走

有个现成的方案,就是使用Redisson。Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
------
著作权归JavaGuide(javaguide.cn)所有
基于MIT协议
原文链接:https://javaguide.cn/distributed-system/distributed-lock-implementations.html

tips:不能给锁设置超时时间,不然不会出发看门狗。

锁冲突

其他线程没有获得锁怎么办:

1、直接抛出异常,通知稍后重试

2、sleep一会再重试

3、将请求转移到延时队列,过一会再试

redis数据过期

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key 。除了定时遍历之外,它还会使用惰性策略来删除过期的 key。
所谓惰性策略就是在客户端访问这个 key 的时候, redis key 的过期时间进行检查,如果过期了就立即删除。防卡顿
定时删除是集中处理,惰性删除是零散处理。

定时扫描策略

每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。扫描时间的上限,默认不会超过 25ms。
1 过期字典中随机 20 key
2 删除这 20 key 中已 经过期的 key
3 如果 过期的 key 比率超 1/4 那就重复步 1
如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。避免卡顿。

从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
因为指令同步是异步进行的,所以主库过期的 key del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。可能会导致分布式锁的异常。

内存淘汰机制

no-eviction: 不会继续服务写请求,读请求可以继续进行。(默认)
volatile-lru:从已 设置过期时间的数据集中挑选 最近最少使用的数据淘汰。
allkeys-lru :从 全部key中任意选择 最近最少使用的数据淘汰。 没有设置过期时间的 key 也可能被淘汰。(常用)
volatile-random :从已 设置过期时间的数据集中随机淘汰数据
allkeys-random:全部key中任意选择数据淘汰
volatile-ttl:从已 设置过期时间的数据集中挑选,ttl 越小越优先被淘汰。

Redis给每个 key 增加了一个额外的小字段,这个字段的长度是 24 bit ,也就是最后一次被访问的时间戳。
LRU 淘汰方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory ,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5( 可以配置 ) key ,然后淘汰掉最旧的 key ,如果淘汰后内存还是超出 maxmemory ,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。

redis做延时队列

适用于只有一组消费者的消息队列。redis做消息队列没有ack确认,不保证可靠性。

异步消息队列

1、用list列表,使用lpush/rpush、lpop/rpop来操作即可。

队列空了,就time.sleep()一会。

bug:如果有多个消费者,延时会加剧。采用阻塞读blocking。

使用blpush/brpush、blpop/brpop阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。

bug:一直阻塞的话,会产生空闲连接的问题,服务器会主动断开连接,因此要注意捕获异常

2、为了改进不能多播的问题,提供了一个模块PubSub,就是PublisherSubscriber。

消费者:

// python
import time
import redis
client = redis.StrictRedis()
p = client.pubsub()
p.subscribe("codehole")
for msg in p.listen():
    print msg

// 订阅多个主题
subscribe codehole.image codehole.text codehole.blog

psubscribe codehole.*

生产者: 

import redis
client = redis.StrictRedis()
client.publish("codehole", "python comes")
client.publish("codehole", "java comes")
client.publish("codehole", "golang comes")


// 发布多个主题
publish codehole.image https://www.google.com/dudo.png
publish codehole.blog '{"content": "hello, everyone", "title": "welcome"}'

3、近期 Redis5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了持久化消息队列,从此 PubSub 可以消失了。

支持多播的可持久化的消息队列Stream 

它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。

ID1、ID2代表着一个个消息,例如存储"hello"、 "hi"这类的消息。每个消息都有唯一的名称,它就是 Redis 的 key(ID1、ID2),在我们首次使用 xadd 指令追加消息时自动创建。
每个 Stream 都可以挂多个消费组,每个消费组会有个游标 l ast_delivered_id 在 Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。
每个消费组都有一个 Stream内唯一的名称,消费组不会自动创建,它需要单独的指令 xgroup create 进行创建,需要指定从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化 last_delivered_id 变量。
每个消费组 (Consumer Group) 的状态都是独立的,相互不受影响。也就是说同一份Stream 内部的消息会被每个消费组都消费到。
同一个消费组 (Consumer Group) 可以挂接多个消费者 (Consumer) ,这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动(和Kafka一个分区对应一个消费者类似)。每个消费者有一个组内唯一名称。
消费者 (Consumer) 内部会有个状态变量 pending_ids ,它记录了当前已经被客户端读取的消息,但是还没有 ack 。如果客户端没有 ack ,这个变量里面的消息 ID 会越来越多,一旦某个消息被 ack ,它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为 PEL ,也就是 Pending Entries List ,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

消息ID:1527846880572-5。它表示当前的消息在毫米时间戳 1527846880572 时产生,并且是该毫秒内产生的第 5 条消息。消息 ID 可以 由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面 加入的消息的 ID 要大于前面的消息 ID。

Redis提供了一个定长 Stream 功能,xadd 的指令提供一个定长长度 maxlen,就可以将老的消息干掉,确保最多不超过指定长度。

延时队列的实现

通过zset有序列表实现。将消息作为key,消息的到期时间作为score。用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。

位图

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
Redis 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位 置范围内 1 的个数, bitpos 用来查找指定范围内出现的第一个 0 1

HyperLogLog

Redis 提供了 HyperLogLog 数据结构就是用来解决统计问题的。HyperLogLog 提供不精确的去重计数方案。
HyperLogLog 提供了两个指令 pfadd pfcount ,根据字面意义很好理解,一个是增加计数,一个是获取计数。还提供了第三个指令 pfmerge,用于将多个 pf 计数值累加在一起形成一个新的 pf 值。
在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占 用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。

布隆过滤器

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。
布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
布隆过滤器有二个基本指令, bf.add 添加元素, bf.exists 查询元素是否存在,它的用法和 set 集合的 sadd sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。

原理

向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都位 1 ,只要有一个位为 0 ,那么说明布隆过滤器中这个key 不存在。如果都是 1 ,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。

空间估计

布隆过滤器有两个参数输入,第一个是预计元素的数量 n ,第二个是错误率 f
两个输出,第一个输出是位数组的长度 l ,也就是需要的存储空间大小 (bit), 第二个输出是 hash 函数的最佳数量 k

限流

1、java计数器

Java内部也可以通过原子类计数器AtomicIntegerSemaphore信号量来做简单的限流。

    // 限流的个数
    private int maxCount = 10;
    // 指定的时间内
    private long interval = 60;
    // 原子类计数器
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    // 起始时间
    private long startTime = System.currentTimeMillis();

    public boolean limit(int maxCount, int interval) {
        atomicInteger.addAndGet(1);
        if (atomicInteger.get() == 1) {
            startTime = System.currentTimeMillis();
            atomicInteger.addAndGet(1);
            return true;
        }
        // 超过了间隔时间,直接重新开始计数
        if (System.currentTimeMillis() - startTime > interval * 1000) {
            startTime = System.currentTimeMillis();
            atomicInteger.set(1);
            return true;
        }
        // 还在间隔时间内,check有没有超过限流的个数
        if (atomicInteger.get() > maxCount) {
            return false;
        }
        return true;
    }

2、redis做限流

用一个 zset 结构记录用户的行为历史,每一个行为都会作为 zset 中的一个key 保存下来。同一个用户同一种行为用一个 zset 记录。
zset 集合中只有 score 值非常重要, value 值没有特别的意义,只需要保证它是唯一的就可
以了。
zadd key score value
zset有两种 不同的实现,分别是 zipListskipList

zipList:
满足以下两个条件:

  • [score,value]键值对数量少于128个;
  • 每个元素的长度小于64字节;

skipList:
不满足以上两个条件时使用跳表(组合了hash和skipList)

  • hash用来存储value到score的映射,这样就可以在O(1)时间内找到value对应的分数;
  • skipList按照从小到大的顺序存储分数;
  • skipList每个元素的值都是[score,value]对

简单限流原理

到now_ts这里,前一分钟统计收到多少请求,如果请求多过5个,那么就会限流。
滑动窗口限流:
  1. 每次请求时,将当前时间戳作为 score,唯一标识符(如用户ID+请求类型)作为 member 添加到 Sorted Set 中。
  2. 移除时间戳早于当前时间减去窗口大小的所有元素。
  3. 检查 Sorted Set 的大小是否超过了限制。

public class SimpleRateLimiter {
    private Jedis jedis;
    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
 }
 
    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);
        // 时间戳
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        // 记录行为 value和score都是毫秒时间戳
        pipe.zadd(key, nowTs, "" + nowTs);
        // 移除时间窗口之前的行为记录  0-nowTs-60*1000分数范围删除元素
        pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
        // 统计窗口内的行为数量
        Response<Long> count = pipe.zcard(key);
        // 设置用户的过期时间61s,避免冷用户占用内存,宽限多1s
        pipe.expire(key, period + 1);
        // 执行
        pipe.exec();
        pipe.close();
        // 比较是否超标
        return count.get() <= maxCount;
     }

    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
        for(int i=0;i<20;i++) {
           // 60s内超5次就限流
            System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
        }    
    }
}

漏斗算法

漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

  1. 请求的流入:当请求到达时,它们被看作是流入桶中的水。
  2. 桶的容量:桶有一个固定的容量,这代表了系统能够处理的请求的最大数量。
  3. 漏出速率:桶底部有一个漏洞,水(请求)以固定的速率从桶中漏出,这个速率代表了系统处理请求的速率。
  4. 流量整形:如果桶满了,新流入的水(请求)会被丢弃,直到桶中有足够的空间。

Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell 。该模块也使用了漏斗算法,并提供了原子的限流指令。
该模块只有 1 条指令 cl.throttle
上面这个指令的意思是允许「用户老钱回复行为」的频率为每 60s 最多 30 ( 漏水速率) ,漏斗的初始容量为 15 ,也就是说一开始可以连续回复 15 个帖子,然后才开始受漏水速率的影响。

令牌桶

  1. 令牌生成:系统以固定的速率向桶中添加令牌,这个速率通常由网络策略或API限制决定。
  2. 桶的容量:桶有一个最大容量,当桶满时,新生成的令牌会被丢弃。
  3. 请求处理:当一个数据包或请求到达时,它需要从桶中取出一个令牌才能被处理。如果桶中有足够的令牌,请求可以立即被处理;如果桶空了,则请求必须等待,直到桶中再次有令牌可用。
  4. 突发传输:由于桶可以存放一定数量的令牌,系统可以在短时间内处理等于桶容量大小的突发流量,而不会因为短暂的流量高峰而完全阻塞。

scan

scan 相比 keys 具备有以下特点 :
1 杂度虽然也是 O(n) 但是它是通 过游标分步进行的 不会阻塞 线程 ;
2 提供 limit 参数 可以控制每次返回 结果的最大条数 limit 只是一个 hint 返回的结果可多可少;
3 keys 它也提供模式匹配功能 ;
4 务器不需要为游标保存状态 标的唯一状态就是 scan 返回 给客户端的游标整数 ;
5 返回的 结果可能会有重复 需要客 户端去重复 这点非常重要 ;
6 历的过程中如果有数据修改 动后的数据能不能遍历到是不确定的 ;
7 单次返回的结果是空的并不意味着遍历结束 而要看返回的游 标值是否为零 ;
scan 参数提供了三个参数,第一个是 cursor 整数值 ,第二个是 key 的正则模式 ,第三个是 遍历的 limit hint
第一次遍历时, cursor 值为 0 ,然后将返回结果中第一个整数值作为下一次遍历的 cursor 。一直遍历到返回的 cursor 值为 0 时结束。
从上面的过程可以看到虽然提供的 limit 1000 ,但是返回的结果只有 10 个左右。因为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量 ( 约等于 )
scan 指令返回的游标就是第一维数组的位置索引0-7,我们将这个位置索引称为槽 (slot)
如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。 limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。
每一次遍历都会将 limit数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。
scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。
高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复。 91+ 28 -->  9+2 = 12 进位1 。1+1+8 = 10,进位1 。 等于201;(不知道有没有理解错)

大key扫描

因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

定位大key

为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
如果你担心这个指令会大幅抬升 Redis ops 导致线上报警,还可以增加一个休眠参数。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1
上面这个指令每隔 100 scan 指令就会休眠 0.1s ops 就不会剧烈抬升,但是扫描的时间会变长

线程IO

Redis是一个单线程程序

为什么redis单线程也能跑这么快?

因为数据都存在内存中,运算都是内存级别的运算,对于On级别的指令一定要小心谨慎!避免造成redis卡顿

redis单线程如何处理并发请求?

多路复用、事件轮询、非阻塞IO(NIO)

非阻塞IO(NIO)

在套接字对象上提供了一个Non_Blocking,这个选项打开,意味着读写方法不会阻塞,变成能读多少读多少,能写多少写多少。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。

事件轮询

非阻塞IO有个bug,线程读数据,读了一部分就返回了,它怎么知道什么时候继续读。事件轮询API就是为了解决这个问题。

最简单的事件轮询 API select 函数,它是操作系统提供给用户程序的 API

输入是读写描述符列表 read_fds & write_fds,输出是与之对应的可读可写事件。

同时还提供了一个 timeout 参数,如果没有任何事件到来,那么就最多等待 timeout 时间,线程处于阻塞状态。

拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。

因为我们通过 select 系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。现代操作系统的多路复用 API 已经不再使用 select 系统调用,而改用epoll(linux)kqueue(freebsd & macosx)

定时任务

redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期, redis都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为知道未来时间内,没有其它定时任务需要处理,所以可以安心睡眠 timeout的时间。

通讯协议

RESP Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。
Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符
\r\n
1 单行字符串 + 符号开
2 多行字符串 以 $ 符号开 后跟字符串 长度
3 整数 : 符号开 后跟整数的字符串形式
4 错误消息 - 符号开
5 * 号开 后跟数 组的长度
客户端向服务器发送的指令只有一种格式,多行字符串数组。比如一个简单的 set 指令
set author codehole 会被序列化成下面的字符串。
服务器向客户端回复的响应要支持多种数据结构,所以消息响应在结构上要复杂不少。
不过再复杂的响应消息也是以上 5 中基本类型的组合。

持久化

Redis 的持久化机制有两种,第一种是快照,第二种是 AOF 日志   

快照是一次全量备份, 是内存数据的二进制序列化形式,在存储上非常紧凑。
AOF日志 记录的是内存数据修改的指令记录文本。AOF会在运行期间变得庞大,因此需要定期重写瘦身。重启时需要加载 AOF 日志进行指令重放。

快照原理

快照要求redis进行IO操作,但IO操作是不能进行多路复用API的。这因为着如果正在持久化一个大的字典,来了个请求把这个持久化打断了,这就白干了。

因此reids使用操作系统的多进程COW(Copy On Write)机制来实现快照。

持久化的时候调用glibc的函数fork产生一个子线程,快照任务交给子进程来解决,父进程继续处理客户端请求。子进程刚产生的时候,共享父进程的代码段和数据段。

子进程只负责将它有的所有数据序列化到磁盘里,不产生新数据。但是父进程一直在处理客户端请求,会对内存的数据进行不间断的修改。

这个时候就要用COW机制来实现数据段页面的分离。其实就是将共享的页面复制一份出来,父进程对这个复制出来的页面进行修改,子进程的数据还是那些。

AOP原理

AOF 日志存储的是 Redis 服务器的顺序指令序列, AOF 日志只记录对内存进行修改的指令记录。
Redis在收到客户端发来的修改请求后,先校验参数,没问题后,先将请求存储到AOF日志(磁盘中)然后再执行指令。

AOF重写

Redis 提供了 bgrewriteaof (Background rewrite AOF) 指令用于对 AOF 日志进行瘦身。
  •  AOF 重写机制,它可以压缩和优化 AOF 文件的内容,减少冗余和无效的命令,提高数据的存储效率和恢复速度 。
  • AOF 重写机制的原理是根据 Redis 进程内的数据生成一个新的 AOF 文件,只包含当前有效和存在的数据的写入命令,而不是历史上所有的写入命令 。
  • AOF 重写机制是通过 fork 出一个子进程来完成的,子进程会扫描 Redis 的数据库,并将每个键值对转换为相应的写入命令,然后写入到一个临时文件中 。
  • 在子进程进行 AOF 重写的过程中,主进程还会继续接收和处理客户端的请求,如果有新的写操作发生,主进程会将这些写操作追加到一个缓冲区中,并通过管道通知子进程
  • 子进程在完成 AOF 重写后,会将缓冲区中的写操作也追加到临时文件中,然后向主进程发送信号,通知主进程可以切换到新的 AOF 文件了 。
  • 主进程在收到子进程的信号后,会将缓冲区中的写操作再次追加到临时文件中(以防止在此期间有新的写操作发生),然后用临时文件替换旧的 AOF 文件,并关闭旧的 AOF 文件 。

混合持久化

快照和AOF日志一起使用。

AOF只存储快照持久化开始到持久化结束中间发生的增量AOF日志。

主从同步

CAP原理

C--一致性、A--可用性、P--分区容忍性

分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区 」。
在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致性」将无法满足,因为两个分布式节点的数据不再保持一致。一句话概括 CAP 原理就是—— 网络分区发生时,一致性和可用性两难全

Redis的主从同步是异步同步的,所以分布式的Redis系统不满足一致性要求。

当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节
点依旧可以正常对外提供修改服务,所以 Redis 满足「 可用性」。
Redis 保证「 最终一致性 」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。

主从同步和从从同步

增量同步

Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里了 ( 偏移量 )
因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容
如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis 的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 —— 快照同步

快照同步

快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。
从节点将快 照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步
在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。
当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增 量同步。

无盘复制

--主结点不是一次生成快照文件

无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。
只要你使用了 Redis 的持久化功能,就必须认真对待主从复制,它是系统数据安全的基础保障。

Sentinel哨兵

为了避免主节点挂掉,可以将从节点变成主节点。

它一般是由 3 5 个节点组成,这样挂了个别节点集群还可以正常运转。
它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。
客户端来连接集群时,会首先连接 sentinel ,通过 sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地址,sentinel 会将最新的主节点地址告诉客户端。
问题: 客户端如何知道地址变更了 ?
连接池建立新连接时,会去查询主库地址,然后跟内存中的主库地址进行比对,如果变更了,就断开所有连接,重新使用新地址建立新连接。如果是旧的主库挂掉了,那么所有正在使用的连接都会被关闭,然后在重连时就会用上新地址。
问题:如果是 sentinel 主动进行主从切换,主库并没有挂掉,而之前的主库连接已经建立了在使用了,没有新连接需要建立,那这个连接是不是一致切换不了?
Redis-py 在处理命令的时候捕获了一个特殊的异常 ReadOnlyError ,在这个异常里将所有的旧连接全部关闭了,后续指令就会进行重连。

消息丢失

Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以限制主从延迟过大。
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
第二个参数控制是否正常复制,它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈。
  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值