1. Redis集群
1.1 主从复制
1.1.1 简介
主从复制即将master中的数据即时,有效的复制到slave中。一个master可以拥有多个slave,一个slave只对应一个master。
职责
- master:
写数据
执行写操作时,将出现变化的数据自动同步到slave
读数据(可忽略)
- slave:
读数据
写数据(禁止)
单机redis的风险
- 服务器宕机,可能会造成数据丢失,对业务造成灾难性打击。
- 容量瓶颈,单台服务器的容量有限,不易扩容。
解决方案
为了避免单点redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上,链接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份。
1.1.2 多台服务器链接方案
1.1.3 主从复制的作用
- 读写分离:master写,slave读,提高服务器的读写负载能力
- 负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量
- 故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复
- 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式
- 高可用基石:基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案
1.1.4 主从复制的工作流程
总述
主从复制过程大体可以分为3个阶段
- 建立连接阶段(即准备阶段)
- 数据同步阶段
- 命令传播阶段
1.1.4.1 建立连接阶段
建立slave到master的链接,使master能够识别slave,并保存slave端口号。
- 设置master的地址和端口,保存master信息
- 建立socket链接
- 发送ping命令(定时器任务)
- 身份验证
- slave发送端口信息,master进行保存
至此,主从链接成功!
状态:
slave:
-
保存master的地址和端口
master:
-
保存slave的端口
流程如下图:
具体实现步骤(slave链接master):
建立连接的方式有多种,比如slave客户端发送连接命令,或者启动服务的时候指定连接参数,但是最常用的还是在配置文件中进行配置,这里也只描述配置文件的方式。
slaveof
授权访问
requirepass
masterauth
1.1.4.2 数据同步阶段
在slave初次链接master后,复制master中的所有数据到slave,将slave的数据库状态更新成master当前数据库状态。
- slave请求同步数据
- master创建RDB同步数据
- slave接收并恢复RDB同步数据
- slave请求部分同步数据(AOF)
- slave在接收RDB数据的时候,可能又会有新的数据进入master,这部分新的数据的指令会被放在master的复制缓冲区,恢复完RDB数据后就会请求这部分数据的指令。
- slave接收并恢复部分同步数据
注意:从slave开始请求同步数据到slave恢复完成RDB同步数据,这一阶段叫做 全量复制。而slave请求部分数据到恢复完成(AOF阶段),叫做部分复制或增量复制,详细过程如下图:
状态:
slave:
-
具有master端全部数据,包含RDB过程接收的数据
master:
-
保存slave在master复制缓冲区同步的位置(下次同步从该位置开始)
1.1.4.3 命令传播阶段
当master数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为命令传播。master将接受到的数据变更命令发送给slave,slave接受命令后执行命令。
总结:命令传播阶段的主要作用就是实时保持中从服务器的数据一致。
命令传播阶段出现的断网现象
- 网络闪断闪连 - 忽略
- 短时间网络中断 - 部分复制
- 长时间网络中断 - 全量复制
1. 命令传播阶段的部分复制
部分复制的三个核心要素
- 服务器的运行id (run id)
- 主服务器的复制积压缓冲区
- 主从服务器的复制偏移量
服务器的运行id (run id)
- 概念
服务器的运行Id是一个随机的十六进制字符,由40位字符组成。每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行Id
- 实现方式
服务器启动时,自动生成的。master首次连接slave时,会将自己的运行Id发送给slave,而slave会保存master的runid,通过info Server命令可以查看节点的runid。
2. 复制缓冲区
- 概念
- 复制缓冲区,又名复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令,每次传播命令,master会将传播的命令记录下来,并存储在复制缓冲区
- 复制缓冲区默认存储空间大小是1M,由于存储空间大小是固定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列
- 由来
每台服务器启动时,如果开启有AOF或被连接成为master节点,就会创建复制缓冲区。
- 组成
- 数据来源
当master接收到主客户端的指令时,除了执行指令,还会将该指令储存到缓冲区。
- 工作原理
- 通过offset区分不同的slave当前数据传播的差异
- master记录已发送的信息对应的offset
- slave记录已接收的信息对应的offser
3. 复制偏移量
- 概念
一个数字,描述复制缓冲区中的指令字节位置
- 分类
- master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个)
- slave复制偏移量:记录slave接受master发送过来的指令字节对应的位置(一个)
- 数据来源
master端:发送一次记录一次
slave端:接收一次记录一次
- 作用
同步信息,对比master与slave的差异,当slave断线后,恢复数据使用
1.1.4.4 数据同步和命令传播阶段详细流程图
1.1.4.5 心跳机制
进入命令传播阶段后,master与slave间需要进行信息交换,使用心跳机制进行维护,实现双方连接保持在线。
- master心跳
- 指令:PING
- 周期:由repl-ping-slave-period决定,默认10秒
- 作用:判断slave是否在线
- 查询: info命令,获取slave最后一次链接时间间隔,lag项维持在0或1视为正常
- slave心跳指令
- 指令:REPLCONF ACK{offset}
- 周期:1秒
- 作用1:汇报slave自己的复制偏移量,获取最新的数据变更指令
- 作用2:判断master是否在线
心跳阶段注意事项
- 当slave多数掉线,或延迟过高时,master为保障数据稳定性,将拒绝所有信息同步操作
min-slave-to-write 2
min-slave-max-lag 10
slave数量少于2个或者多有slave延迟都大于等于10秒时,强制关闭master写功能,停止数据同步
- slave数量和延迟是由slave发送REPLCONF ACK命令做确认
完整流程图
1.2 哨兵模式
哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
1.2.1 哨兵的作用
- 监控
不断地检查master和slave是否正常运行。
master存活检测、master与slave运行情况检测。
- 通知(提醒)
当被监控的服务器出现问题时,向其他(哨兵,客户端)发送通知
- 自动故障转移
断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址。
注意:哨兵也是一台redis服务器,只是不提供数据服务,通常哨兵配置数量为单数
1.2.2 哨兵的工作原理
1). 主从切换
哨兵在主从切换中经历三个阶段:
1.1) 监控阶段
- 同步各个节点的状态信息
- 获取各个sentinel的状态(是否在线)- ping
- 获取master的状态 - info
- 获取所有slave的状态(根据master中的slave信息)- info
- slave属性
- runid
- role:slave
- master_host、master_port
- offset
- …
1.2) 通知阶段
当被监控的服务器出现问题时,向其他(哨兵,客户端)发送通知,保证每个哨兵信息的对等。
1.3) 故障转移阶段
太复杂了,记不住。想了解的同学,去百度吧。
1.3 集群
1.3.1 简介
现状问题
业务发展过程中遇到的峰值瓶颈
- redis提供的服务OPS可以达到10万/秒,当前业务OPS已经达到20万/秒
- 内存单机容量达到256G,当前业务需求内存容量1T
集群架构
集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果。
集群的作用
- 分散单台服务器的访问压力,实现负载均衡
- 分散单台服务器的存储压力,实现可扩展性
- 降低单台服务器宕机带来的业务灾难
1.3.2 集群的搭建
集群具体怎么搭建的,其实也不难,百度一下就可以了,这里就不写具体步骤了,只描述下注意事项。
- 搭建集群至少需要六台服务器(至少3主,每个主服务器至少1个从服务器)
- 启动集群命令
redis-cli --cluster create --cluster-replicas 1 172.16.88.168:6379 172.16.88.168:6380 172.16.88.168:6381 172.16.88.168:6389 172.16.88.168:6390 172.16.88.168:6391
- cluster-replicas 1:代表每台主服务器都要有1台从服务器
- 这6台redis服务器前3个会设置成主,后3个设置成从;如果是8台的话,前4台主,后4台从。
1.3.3 进群中数据是如何存储的?
由于主服务器有多台,当添加新数据的时候,具体会保存到哪台主服务器呢?
- 一个 Redis 集群包含 16384 个插槽
- 每个主服务器会分配一部分插槽
- 新添加一条数据时,会根据key计算出保存到哪个插槽。
录入或读取数据
在 redis-cli 每次录入、查询键值,redis 都会计算出该 key 应该送往的插槽,如果不是该客户端对应服务器的插槽,redis 会报错,并告知应前往的 redis 实例地址和端口。
- 录入/读取 数据的时候可以通过 redis-cli -c 命令实现重定向,如果录入/读取 的key计算出的插槽不在该服务器会自动重定向到正确的服务器。
1.3.4 主从切换/故障恢复
在一个集群中,如果一台主服务器挂了,redis是怎么处理的?如果主服务器挂了超过一定时间从服务器会晋升为主服务器,原来的主服务器再次上线的话会变成从服务器。配置如下:
- 设置节点失联事件,超过该时间(ms),集群自动进行主从切换
cluster-node-timeout 15000
2. 企业级解决方案
2.1 缓存雪崩(多个key过期)
1. 出现原因
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
2. 解决方案
- 构建多级缓存架构
nginx缓存+redis缓存+ehcache缓存
- 使用队列或锁
高并发情况下不适用
- 让缓存失效时间分散开
比如在原有的失效时间上加个1-5分钟的随机值。
- 热点数据特殊处理
如果是临时热点数据,比如秒杀,就让秒杀结束后再失效。
如果一直都是热点数据,就设置永久key。
3. 总结
解决方案各种各样,只要能保证不要让key短时间内集中失效就可以,特别是热点数据。
2.2 缓存击穿(单个key过期)
1. 出现原因
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,缓存中读不到数据就去数据库读取,引起数据库压力瞬间增大,造成过大压力。
2. 解决方案
- 预先设置热点数据
在一些热点数据高峰访问之前,提前加入redis缓存,延长过期时间。
- 使用锁
如果请求到redis中的数据为空就加锁,获取到锁再去判断下redis中有没有数据,多个请求进来的只要一个获取到锁,这样就只有这一个去请求数据库,请求到数据库后放入缓存,然后释放锁。其他线程获取到锁后回再去判断redis,这时候redis已经有值了,就不会去请求数据库了。只不过用锁回降低效率,要慎用。
3. 总结
在涉及架构的时候,提前考虑到这些问题,尽量避免问题的发送。一旦发送了缓存击穿,根据情况去解决,解决方案也不是一成不变的,能解决问题就好。
2.3 缓存穿透
出现原因
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,导致数据库频繁请求不存在得数据,数据库压力增大。这种情况很可能是黑客攻击。
解决方案
- 对空值进行缓存
对数据库查询位null得数据也进行缓存,value就位null,然后设置个短暂的过期时间。
- 白名单策略
使用bitmaps类型定义一个可访问的白名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面就进行拦截。
2.4 分布式锁
分布式锁常见的三种实现方式有:redis、zeekooper、数据库。这里只分析Redis实现分布式锁的方式。
Redis实现分布式锁
通过setnx命令设置锁,通过del命令删除锁。
问题1:锁一直不释放怎么办?
比如加锁之后代码出现异常,导致后面的del命令没有执行,锁一直不会释放怎么办?
解决方案
给锁设置过期时间,指定时间没有释放就自动释放。
expire key second
问题2:过期时间设置失败怎么办?
比如加完锁了,代码刚运行到设置过期时间还未执行,这是时候服务器宕机了,导致设置过期时间失败。
解决方案
通过一条命令,在设置锁的同时设置过期时间。
#nx:key不存在时才生效;ex:过期时间
set key value nx ex second
问题3:释放了别人的锁怎么办(1)?
假如A线程获取到了锁也设置了过期时间了,在执行的过程中卡住了,时间到了就自动释放锁了。这时B线程抢到了锁,正在执行,A线程反应过来了继续往下执行,执行完了要手动释放锁,这时锁在B线程呢,那么A就会把B线程的锁释放掉,这种问题怎么办?
解决方案
设置锁的时候将value设置为UUID,这样没过线程获取到的锁的值都是不一样的,手动释放锁的时候先判断下释放的锁的value和自己拥有锁的value是否相同,相同才释放。
代码效果如下:
问题4:释放了别人的锁怎么办(2)?
在问题3中,手动释放锁的时候需要通过UUID进行比较防止误删。但是,假如A线程刚比较完,自动释放时间就到了,自动释放了,然后B线程快速获取到了锁,而A线程比较完就执行手动操作了,这时A线程就会把B线程的锁给释放掉?
解决方案
要保证比较UUID和手动释放锁的原子性,通过lua(撸啊)脚本可以保证他们的原子性。
#这句话的意思时判断redis中所得value跟我们传过来的是否相同,相同就调用删除key的方法,不相同就返回0
if redis.call(‘get’,KEYS[1])==ARGV[1] then return redis.call(‘del’,KEYS[1]) else return 0 end
如下图:
其他说明
通过Redisson的看门狗机制也可以实现分布式锁,当一个锁快过期的时候,看门狗会自动延长有效期。
总结
为了确保分布式锁可用,实现分布式锁的时候至少要确保满足以下4个条件:
- 互斥性。在任意时刻,只有一个客户端能拥有锁。
- 不会发生死锁。即使一个客户端持有锁的期间崩溃没有手动释放锁,也要保证后续其他客户端依然可以获取锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端不能把别人的锁给释放了。
- 加锁和解锁必须具有原子性。加完锁,在解锁之前,其他客户端不能干扰。