redis主从复制
当今互联网的“三高”架构:高并发、高性能、高可用。
可用性计算公式:
可用性=1年内服务器的可用时间/1年的总时间
业界的目标就是保证服务的高可用,即可用性能达到99.999%。
单机redis的问题
单机redis的问题很明显,如果当前的redis服务宕机了,则系统的整个缓存系统瘫痪,导致灾难性的后果。另外单机redis也有内存容量瓶颈。
redis主从复制
一个master可以有多个slave,一个slave只能有一个master
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
进入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命令,可以看到有两个从机:
测试主从服务之间的关系
☐ 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命令。该命令可以将从节点自身的偏移量发给主节点,主节点会与自己的偏移量对比,如果从节点因为网络丢包等原因造成数据缺失,主节点会推送缺失的数据
我们已经知道了主从复制的流程了,而这些流程被划分为3个阶段。
在命令传播阶段(第三个阶段),有可能会出现,从机与主机断开的情况。
- 长期断开(1,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
注意,不要将宿主机的sentinel.conf直接映射到哨兵容器中的sentinel.conf,否则容器会出现警告的信息。
3. 启动好哨兵后,使用docker logs -f sentinel9001 持续查看哨兵的日志:
6.此时关闭master,等待一段时间,会发先哨兵选出了一个新的master,并且通知其他slave归顺新主
7. 当原来的master再次启动时,会被哨兵发现并且让其归顺于新主
至此哨兵环境已经搭建成功。
下面搭建哨兵集群的环境
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个节点。
集群的作用
1. 分散单台服务器的访问压力,实现负载均衡
2. 分散单台服务器的存储压力,实现可扩展性
3. 降低单台服务器宕机所带来的业务灾难(高可用)
redis集群搭建
1. 创建redis-cluster文件夹,在其中创建8001、8002、8003、8004、8005、8006文件夹
2. 在8001、8002、8003、8004、8005、8006文件夹中分别创建data文件,这些data目录将用于挂载到容器中的/data目录
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
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
8. 进入redis8001容器的redis客户端,键入以下命令查看集群信息:1
cluster nodes
9. 测试集群效果:
10. 测试集群的高可用,关闭redis8001容器(8001的从节点时8005)
docker stop redis8001
此时进入其他节点,查看集群信息,发现8005成master了
11. 当再次启动redis8001容器后,8001会成为8005的从节点
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集群原理
注意,集群中一共有16384个桶位(slot),一个桶位可以存放多个key。
水平扩展一个新的redis节点后
redis集群内部通讯机制
redis集群中的每一个节点,都存储了各个节点保存的桶位范围,所以向redis集群中保存一个key,最多转发一次就能找到目标节点。
分布式锁
引入依赖
<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原理
在Redis主从架构下,分布式失效问题
在Redis主从架构中,写入都是写入主Redis实例,主实例会向从实例同步key。
一个业务线程A通过向主Redis实例中写入来实现加分布式锁,加锁后开始执行业务代码。这时如果主Redis实例挂掉了,会选举出一个从Redis实例成为新主,如果刚刚加锁的key还没有来得及同步到从Redis中,那么选举出来的新的主Redis实例中就没有这个key,这个时候业务线程B就能在新主中加锁来获取分布式锁,执行业务代码了,而这个时候A还没有执行结束,所以就会出现并发安全问题,这就是Redis主从架构的分布式锁失效问题。
使用RedLock解决分布式锁失效问题
解决方法就是不使用主从复制架构存锁即可,但是这样就丢失了主从复制所带来的高可用的优势了。那么如何才能既保证高可用的优势,又避免分布式锁失效的问题呢?
首先要有多个(最好是奇数个)对等的(没有主从关系)Redis结点。当进行加锁时(SETNX命令),则这个设置key-value的命令会发给每个Redis结点执行,当且仅当客户端收到超过半数的结点写成功的消息时,才认为加锁成功,才开始执行后面的业务代码。
这样能保证:
☐ 避免分布式锁失效:如果客户端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)
布隆过滤器,话述
- 布隆过滤器,本质上是一个bitmap结构
- 比如针对于商品表的id列,创建了一个布隆过滤器,该布隆过滤器中会填充1:
- setbit foo 商品id 1
- getbit foo 查询条件id
- 如果结果是0,说明该id不存在,就不放行了。
- 如果结果是1,说明该id存在,就放行
- 在前端发起查询请求的时候,会拿着请求参数id,作为偏移量,查询布隆过滤器中对应的位是否为1
- 如果查询的条件是非数字,则计算哈希码,利用哈希码作为偏移量。但是哈希码会有碰撞的情况,就是会误判,所以我们可以多次,从而降低误判率。
问题是,不同内容的字符串,可能得出相同的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作为缓存,可以提高查询速度:
当修改了数据库中的数据时,不要考虑更新缓存的方案
以下就是要用先更新数据库,后更新缓存的方式,试一试,看看到底有什么问题。
测试的大前提:
id | name | |
db | 1 | a |
redis | 1 | a |
结论:单线程没有问题。多线程就有问题。
其实可以直接使用分布式锁保证数据库和缓存的双写一致性,但是这样对系统的性能会有影响。所以下面讨论的是不使用分布式锁的情况。
Cache Aside Pattern是经典的缓存一致性处理模式,就是“先改库,再删缓存”。
为什么不是“先删缓存,再写库” 以下图的前提是:库中有数据, 缓存中没有数据
大前提:
id | name | |
db | 1 | a1 |
redis | 1 | a1 |
w
Cache Aside Pattern
问题是,如果线程2的写缓存操作在线程1的删缓存之后呢?
经典的延迟双删: