redis主从复制

当今互联网的“三高”架构:高并发、高性能、高可用。


可用性计算公式:

可用性=1年内服务器的可用时间/1年的总时间

业界的目标就是保证服务的高可用,即可用性能达到99.999%。


单机redis的问题

单机redis的问题很明显,如果当前的redis服务宕机了,则系统的整个缓存系统瘫痪,导致灾难性的后果。另外单机redis也有内存容量瓶颈。


redis主从复制

一个master可以有多个slave,一个slave只能有一个master


5_分布式锁



redis的主从复制的作用

1. 读写分离,提高服务读的负载能力(写的负载能力没有提升)。

2. 提高了整个redis服务的可用性。


搭建redis主从复制

在/root/redis目录下创建三个目录:8001 8002 8003


创建文件:/root/redis/8001/redis.conf、/root/redis/8002/redis.conf、/root/redis/8003/redis.conf

port 8001 | 8002 | 8003

masterauth 123

requirepass 123

bind 0.0.0.0

logfile redis.log


启动三个redis容器:

docker run --name redis8001 -d -p 8001:8001 \

--net host \

-v /root/redis/8001/redis.conf:/redis.conf redis:7 \

redis-server /redis.conf


docker run --name redis8002 -d -p 8002:8002 \

--net host \

-v /root/redis/8002/redis.conf:/redis.conf redis:7 \

redis-server /redis.conf


docker run --name redis8003 -d -p 8003:8003 \

--net host \

-v /root/redis/8003/redis.conf:/redis.conf redis:7 \

redis-server /redis.conf


5_docker_02



进入8001、8002、8003容器。

docker exec -it redis8001 redis-cli -p 8001

docker exec -it redis8002 redis-cli -p 8002

docker exec -it redis8003 redis-cli -p 8003


在8002、8003容器中,键入以下命令,让8002和8003认8001为主:

REPLICAOF 192.168.116.128 8001


此时在8001中,键入role命令,可以看到有两个从机:

5_redis_03



测试主从服务之间的关系

☐ a. 主机的数据会复制给从机

☐ b. 从机无法写入任何数据


测试:

☐ 1. 当master宕机以后,slave会怎么样?

☐ 2. 当slave宕机以后,再次重启会怎么样?


redis主从复制流程

阶段一:连接阶段

阶段二:数据同步阶段

阶段三:命令传播阶段


redis主从复制流程

1. 从机使用replicaof ( slaveof )指令与主机建立连接,建立主从关系 (TCP3握手)

2. slave连接上master,向master发送psync指令,请求同步master中的数据

3. master判断是否为全量复制(第一次连接),如果是全量复制,则进入下一步;否则进行增量复制(第8步)。

4. master启动一个后台进程,执行bgsave生成一份RDB快照文件,同时将bgsave执行的过程中所接收到的写命令缓存到复制缓冲区中。

5. RDB文件生成完毕之后,master会将RDB发送给slave。

6. slave收到RDB文件之后,会先清空自己的旧数据,然后将RDB文件持久化到本地磁盘,再从本地磁盘加载到内存中。slave会给发送同步完数据的消息。

7. master会将内存中缓存的写命令发送给slave,slave也会执行这些命令。

8. 此后master会将自己执行的写命令发送给从机,从机接收到命令后执行。

9. 从节点每秒向主节点发送replcof ack命令。该命令可以将从节点自身的偏移量发给主节点,主节点会与自己的偏移量对比,如果从节点因为网络丢包等原因造成数据缺失,主节点会推送缺失的数据


5_分布式锁_04


我们已经知道了主从复制的流程了,而这些流程被划分为3个阶段。


在命令传播阶段(第三个阶段),有可能会出现,从机与主机断开的情况。

  1. 长期断开(1,2小时), 然后再连接上,那就直接从新走三个阶段
  2. 闪断闪连(5,6秒),然后再连接上:利用复制缓冲区来保证主从数据一致性。



主从复制的三个阶段

1. 连接阶段,由slave去主动连接master,双方互相记住对方的套接字(建立了TCP连接)

2. 数据同步阶段,由master把目前的所有数据一次性同步给slave(使用RDB)

3. 命令传播阶段,master把后期收到的写命令,以命令的形式发送给slave


哨兵模式

问题描述:当master宕机以后,所有的slave是无法正常工作的,这时需要从所有的slave中选择一个作为新的master,并通知所有的其他slave连接该新的master,这个过程完全可以由人工完成。也可以使用哨兵模式!


哨兵是什么

哨兵也是一个redis服务,只是不提供数据服务(你不能在哨兵服务中存任何键值对)。哨兵用来监控master,如果master宕机了,哨兵会选一个slave成为新的master,并且让其他slave臣服于新master。


哨兵模式搭建

1. 再创建一个/root/redis/9001目录,在其中创建一个sentinel.conf文件,内容编辑如下:

# 哨兵监听的端口

port 9001

# 哨兵存放数据的位置

dir ./

# 下面的数字1表示只要有1个哨兵认为master宕机了,就能确认maser确实宕机了,

# 毕竟现在只有一个哨兵,还没有为哨兵做集群

sentinel monitor mymaster 192.168.116.132 8001 1

# 设定master在多长时间(毫秒)没响应,就认为master宕机了

sentinel down-after-milliseconds mymaster 30000

# 哨兵连接master也需要通过master的认证

sentinel auth-pass mymaster 123


2. 先保证主从复制环境搭建好,最后再启动哨兵

docker run --name sentinel9001 -d -p 9001:9001 \

--net host \

-v /root/redis/9001/:/root/sentinel redis: \

redis-sentinel /root/sentinel/sentinel.conf

5_分布式锁_05


注意,不要将宿主机的sentinel.conf直接映射到哨兵容器中的sentinel.conf,否则容器会出现警告的信息。


3. 启动好哨兵后,使用docker logs -f sentinel9001 持续查看哨兵的日志:

5_redis_06



6.此时关闭master,等待一段时间,会发先哨兵选出了一个新的master,并且通知其他slave归顺新主

5_docker_07



7. 当原来的master再次启动时,会被哨兵发现并且让其归顺于新主

5_分布式锁_08



至此哨兵环境已经搭建成功。


下面搭建哨兵集群的环境

1. 为了搭建哨兵集群,需要再建9002和9003两个目录,现在一共就有3个目录了:9001、9002、9003


2. 9001、9002、9003目录,在其中存入哨兵的配置文件

port 9001 | 9002 | 9003

sentinel monitor mymaster 192.168.188.130 8001 2

sentinel down-after-milliseconds mymaster 30000

sentinel auth-pass mymaster 123


3. 搭建好redis的主从复制环境:一主二从。保证8001是主。


4. 然后启动3个哨兵服务

docker run --name sentinel9001 -d -p 9001:9001 \

--net host \

-v /root/redis/9001/:/root/sentinel redis:7 \

redis-sentinel /root/sentinel/sentinel.conf


docker run --name sentinel9002 -d -p 9002:9002 \

--net host \

-v /root/redis/9002/:/root/sentinel redis:7 \

redis-sentinel /root/sentinel/sentinel.conf


docker run --name sentinel9003 -d -p 9003:9003 \

--net host \

-v /root/redis/9003/:/root/sentinel redis:7 \

redis-sentinel /root/sentinel/sentinel.conf

注意,哨兵之间也是能够相互识别的。

docker logs -f sentinel9001

docker logs -f sentinel9002

docker logs -f sentinel9003


5. 关闭8001 redis服务,过一会,哨兵集群会重新选出一个新主

6. 再次启动8001服务,哨兵集群会让8001归顺于新主

7. 关闭sentinel9001容器

8. 此时再关闭新主,测试剩下的2个哨兵是否会选出新主

9. 会发现,剩下的2个哨兵能继续重新选举一个新主

10. 再让9001哨兵回来,可以看出9001回来后,获得到了最新的信息


哨兵工作总结

1. 监控:同步信息。

2. 通知:保持数据互通

3. 故障转移:

(1) 发现问题

(2) 从哨兵中选出负责人

(3) 负责人选出master

(4) 新master上位,其他slave切换master,原master也臣服于新master


哨兵模式的问题

1. 和单机节点的写并发是相同的,毕竟只有一个节点负责写。

2. 在主从切换的瞬间存在访问瞬断的情况。


至此,redis的哨兵模式就学习完了。


redis集群

问题:

业务发展过程中遇到的峰值瓶颈

1. redis提供的服务OPS可以达到11万/秒,而当前业务的OPS需要达到20万/秒甚至更多

2. 单机内存容量可达256G,而当前业务需要内存达到1T

解决方案

使用redis集群


集群架构

所谓集群,就是通过多台计算机完成同一个工作,达到更高的效率,整个集群对外表现的就像是一个单机系统。




redis集群

reids集群是一个由多个主从节点群组成的分布式服务器群,它具高可用和分片的特点。redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。redis集群没有中心节点,可水平扩展,据官方文档称可以线性扩展到1000个节点。

5_docker_09




集群的作用

1. 分散单台服务器的访问压力,实现负载均衡

2. 分散单台服务器的存储压力,实现可扩展性

3. 降低单台服务器宕机所带来的业务灾难(高可用)


redis集群搭建

1. 创建redis-cluster文件夹,在其中创建8001、8002、8003、8004、8005、8006文件夹

5_redis_10



2. 在8001、8002、8003、8004、8005、8006文件夹中分别创建data文件,这些data目录将用于挂载到容器中的/data目录

5_docker_11



3. 在8001目录下创建redis.conf文件,内容如下:

port 8001

# 允许所有ip访问

bind 0.0.0.0

# 指定数据文件存放位置

dir /data

# 启动集群模式

cluster-enabled yes

# 指定集群配置文件

cluster-config-file nodes-8001.conf

# 节点超时时间(毫秒)

cluster-node-timeout 5000

appendonly yes


4. 将8001目录下的redis.conf文件复制到8002、8003、8004、8005、8006目录下:

cp 8001/redis.conf 8002/

cp 8001/redis.conf 8003/

cp 8001/redis.conf 8004/

cp 8001/redis.conf 8005/

cp 8001/redis.conf 8006/


5. 针对于不同目录下的配置文件要修改配置中的端口

sed -i s/8001/8002/g 8002/redis.conf

sed -i s/8001/8003/g 8003/redis.conf

sed -i s/8001/8004/g 8004/redis.conf

sed -i s/8001/8005/g 8005/redis.conf

sed -i s/8001/8006/g 8006/redis.conf


6. 启动redis容器:

docker run -p 8001:8001 \

-v /root/redis-cluster/8001/redis.conf:/redis.conf \

--name redis8001 \

--net host \

-d redis:7 redis-server /redis.conf


docker run -p 8002:8002 \

-v /root/redis-cluster/8002/redis.conf:/redis.conf \

--name redis8002 \

--net host \

-d redis:7 redis-server /redis.conf


docker run -p 8003:8003 \

-v /root/redis-cluster/8003/redis.conf:/redis.conf \

--name redis8003 \

--net host \

-d redis:7 redis-server /redis.conf


docker run -p 8004:8004 \

-v /root/redis-cluster/8004/redis.conf:/redis.conf \

--name redis8004 \

--net host \

-d redis:7 redis-server /redis.conf


docker run -p 8005:8005 \

-v /root/redis-cluster/8005/redis.conf:/redis.conf \

--name redis8005 \

--net host \

-d redis:7 redis-server /redis.conf


docker run -p 8006:8006 \

-v /root/redis-cluster/8006/redis.conf:/redis.conf \

--name redis8006 \

--net host \

-d redis:7 redis-server /redis.conf

5_分布式锁_12



7. 此时我们已经创建好了所有节点,但各个节点还是相互独立的,我们需要将他们整合成集群,执行以下命令建立集群

docker exec -it redis8003 redis-cli --cluster create \

172.29.27.193:8001 \

172.29.27.193:8002 \

172.29.27.193:8003 \

172.29.27.193:8004 \

172.29.27.193:8005 \

172.29.27.193:8006 \

--cluster-replicas 1


5_redis_13



8. 进入redis8001容器的redis客户端,键入以下命令查看集群信息:1

cluster nodes

5_redis_14



9. 测试集群效果:

5_分布式锁_15



10. 测试集群的高可用,关闭redis8001容器(8001的从节点时8005)

docker stop redis8001


此时进入其他节点,查看集群信息,发现8005成master了

5_redis_16



11. 当再次启动redis8001容器后,8001会成为8005的从节点

5_分布式锁_17



12. SpringBoot连接redis集群,只有配置变化了,其他代码完全不用变

server:

port: 8080


spring:

datasource:

driver-class-name: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimeznotallow=UTC%2b8

username: root

password: 123

redis:

cluster:

nodes: 192.168.188.129:8001,

192.168.188.129:8002,

192.168.188.129:8003,

192.168.188.129:8004,

192.168.188.129:8005,

192.168.188.129:8006


pagehelper:

helper-dialect: mysql

reasonable: true

support-methods-arguments: true


logging:

level:

com.gao.dao: debug


redis集群原理

5_docker_18



注意,集群中一共有16384个桶位(slot),一个桶位可以存放多个key。


水平扩展一个新的redis节点后

5_docker_19




redis集群内部通讯机制

redis集群中的每一个节点,都存储了各个节点保存的桶位范围,所以向redis集群中保存一个key,最多转发一次就能找到目标节点。

5_redis_20





分布式锁

引入依赖

<dependency>

<groupId>org.redisson</groupId>

<artifactId>redisson</artifactId>

<version>3.13.4</version>

</dependency>


redisson配置类

@Configuration

public class RedissonConfiguration {

@Bean

public Redisson redisson() {

Config config = new Config();

config.useSingleServer().setAddress("redis://192.168.88.128:6379").setDatabase(0);

config.useSingleServer().setPassword("123");

return (Redisson) Redisson.create(config);

}

}


集群redisson配置

@Bean

public Redisson redisson() {

Config config = new Config();

config.useClusterServers().addNodeAddress("redis://192.168.3.128:8001 redis://192.168.3.128:8002 redis://192.168.3.128:8003 redis://192.168.3.128:8004 redis://192.168.3.128:8005 redis://192.168.3.128:8006".split(" "));

return (Redisson) Redisson.create(config);

}


redisson使用

@Autowired

private Redisson redisson;


RLock lock = null;

lock = redisson.getLock(mylock);

lock.lock();

// ...

lock.unlock();


redisson原理

5_redis_21




在Redis主从架构下,分布式失效问题

在Redis主从架构中,写入都是写入主Redis实例,主实例会向从实例同步key。

一个业务线程A通过向主Redis实例中写入来实现加分布式锁,加锁后开始执行业务代码。这时如果主Redis实例挂掉了,会选举出一个从Redis实例成为新主,如果刚刚加锁的key还没有来得及同步到从Redis中,那么选举出来的新的主Redis实例中就没有这个key,这个时候业务线程B就能在新主中加锁来获取分布式锁,执行业务代码了,而这个时候A还没有执行结束,所以就会出现并发安全问题,这就是Redis主从架构的分布式锁失效问题。


使用RedLock解决分布式锁失效问题

解决方法就是不使用主从复制架构存锁即可,但是这样就丢失了主从复制所带来的高可用的优势了。那么如何才能既保证高可用的优势,又避免分布式锁失效的问题呢?


首先要有多个(最好是奇数个)对等的(没有主从关系)Redis结点。当进行加锁时(SETNX命令),则这个设置key-value的命令会发给每个Redis结点执行,当且仅当客户端收到超过半数的结点写成功的消息时,才认为加锁成功,才开始执行后面的业务代码。

5_redis_22



这样能保证:

☐ 避免分布式锁失效:如果客户端A在获取分布式锁时,在1、2、3结点上加锁成功以后(此时分布式锁已经获取成功),结果1结点挂了(不影响已经获得的分布式锁)。此时客户端B也要获取分布式锁,则客户端B只能在4、5结点上获取到锁(无法获取到5个结点的一半),所以客户端B获取分布式锁是失败的。

☐ 高可用:此时就算其中2个结点挂了,客户端也是能获取到分布式锁的


企业级解决方案


缓存预热

缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,由于缓存中没有任何数据,而并发地全部访问数据库。


缓存雪崩

1. 在一个较短的时间内,缓存中较多的key过期

2. 恰恰就是在较短的时间内,有很多请求访问过期key而未命中,让请求到达数据库

3. 数据库同时接收大量的请求,而无法及时处理,导致数据库崩溃


缓存雪崩解决方案

1. 对key的过期时间进行分类错峰:均匀分布key的过期时间,避免大量key在较短时间内较多集中过期。

2. 超热key永不过期


缓存击穿

1. redis中某一个高热key过期

2. 同时海量请求都在访问这同一个key,均未命中

3. 海量请求呼啸而至,奔向数据库,数据库崩溃


缓存击穿解决方案

1. 对自然流量所推出来的新的高热key,延长其过期时间或者设置其为永久key

2. 使用分布式锁,当web应用请求redis时得不到数据,web应有就会去访问数据库中的数据,而在访问数据库的这一步,加上分布式锁,这样同一时刻只有一个线程可以访问到数据库,此时数据库的压力就非常小,当抢到锁的线程查询出数据交给web应用后,web应用再把数据缓存到redis,而让其他没有抢到所的线程自旋,自旋期间不断地访问redis中的数据,当其他线程访问到redis中的数据时,就不用再去数据库中查询数据了,也不用再竞争分布式锁了。


缓存穿透

1. redis中出现大量未命中的请求(redis中没有,数据库中也没有)

2. 出现非正常URL的访问,这些访问由于没有走正常的应用,所以可以故意访问一类不可能存在的key,这些key不在缓存中,数据库中也没有。

3. 后续出现大量以上请求,很可能是懂技术的人在对服务器进行攻击!


缓存穿透解决方案

1. 缓存null:对查询结果为null的数据也进行缓存,设定较短的过期时间(对方可以换不同的参数)

2. ip拉黑或限流(对方可以换不同的ip)

3. 在应用中规定特殊的key前缀,再使用过滤器检测key前缀是否合法

4. 使用布隆过滤器(不是100%命中非法key)


布隆过滤器,话述

  1. 布隆过滤器,本质上是一个bitmap结构
  2. 比如针对于商品表的id列,创建了一个布隆过滤器,该布隆过滤器中会填充1:
  1. setbit foo 商品id 1
  2. getbit foo 查询条件id
  3. 如果结果是0,说明该id不存在,就不放行了。
  4. 如果结果是1,说明该id存在,就放行
  1. 在前端发起查询请求的时候,会拿着请求参数id,作为偏移量,查询布隆过滤器中对应的位是否为1
  2. 如果查询的条件是非数字,则计算哈希码,利用哈希码作为偏移量。但是哈希码会有碰撞的情况,就是会误判,所以我们可以多次,从而降低误判率。


5_分布式锁_23




5_docker_24


问题是,不同内容的字符串,可能得出相同的hash码(hash碰撞)。 所以布隆过滤器会有误判的情况。

我们无法避免误判,但能降低误判的几率?就是把一个字符串,进行多次不同的散列算法,计算出不同的hash值,这些hash值作为

偏移量设置为1. 查询条件字符串,也使用完全相同的散列算法,散列多次,只有全部都是1,才是可能存在。 如果无存在,那一定是不存在。




使用布隆过滤器:

<dependency>

<groupId>com.google.guava</groupId>

<artifactId>guava</artifactId>

<version>19.0</version>

</dependency>


import com.google.common.hash.BloomFilter;

import com.google.common.hash.Funnels;


/**

* @author gao

* @time 2020/07/16 19:54:08

*/

public class App {

预计要插入多少数据

期望的误判率

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);

public static void main(String[] args) {

插入数据

for (int i = 0; i < 1000000; i++) {

bloomFilter.put(i);

}

int count = 0;

for (int i = 1000000; i < 2000000; i++) {

if (bloomFilter.mightContain(i)) {

count++;

误判了~~~");

}

}

总共的误判数:" + count);

}

}



对比缓存击穿和缓存穿透

缓存击穿,访问的是一个存在的高热key,突然过期,导致数据库服务器压力激增

缓存穿透,访问的是一个压根不存在的key,导致数据库服务器压力激增。



数据库、缓存双写一致性

redis作为缓存,可以提高查询速度:

5_docker_25




当修改了数据库中的数据时,不要考虑更新缓存的方案

以下就是要用先更新数据库,后更新缓存的方式,试一试,看看到底有什么问题。


测试的大前提:



id

name

db

1

a

redis

1

a


5_docker_26



结论:单线程没有问题。多线程就有问题。


其实可以直接使用分布式锁保证数据库和缓存的双写一致性,但是这样对系统的性能会有影响。所以下面讨论的是不使用分布式锁的情况。


Cache Aside Pattern是经典的缓存一致性处理模式,就是“先改库,再删缓存”。



为什么不是“先删缓存,再写库” 以下图的前提是:库中有数据, 缓存中没有数据


大前提:


id

name

db

1

a1

redis

1

a1

w

5_分布式锁_27



Cache Aside Pattern

5_docker_28



问题是,如果线程2的写缓存操作在线程1的删缓存之后呢?

5_redis_29



经典的延迟双删:

5_分布式锁_30