Redis高可用
Redis分布式锁
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
实现分布式锁的主要步骤:
- 指定一个 key 作为锁标记,存入 Redis 中,指定一个唯一的标识作为 value。
- 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
- 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
- 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足解铃还须系铃人** 。
Redisson提供了看门狗,每获得一个锁时,只设置一个很短的过期时间,同时起一个线程在每次快要到超时时间时去刷新锁的过期时间。在释放锁的同时结束这个线程。
RedLock算法
Redisson分布式锁,在某些极端情况下仍然是有缺陷的
客户端长时间内阻塞导致锁失效
客户端 1 得到了锁,因为网络问题或者 GC 等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端 2 也能正常拿到锁,可能会导致线程安全问题。
Redis 服务器时钟漂移
如果 Redis 服务器的机器时间发生了向前跳跃,就会导致这个 key 过早超时失效,比如说客户端 1 拿到锁后,key 还没有到过期时间,但是 Redis 服务器的时间比客户端快了 2 分钟,导致 key 提前就失效了,这时候,如果客户端 1 还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的情况,同样会造成线程安全的问题。
单点实例安全问题
如果 Redis 是单机模式挂了的话,那所有的客户端都获取不到锁了,假设你是主从模式,但 Redis 的主从同步是异步进行的,如果 Redis 主宕机了,这个时候从机并没有同步到这一把锁,那么机器 B 再次申请的时候就会再次申请到这把锁。
引入红锁算法:
客户端在多个 Redis 实例上申请加锁,必须保证大多数节点加锁成功,默认为N/2+1个
解决容错性问题,部分实例异常,剩下的还能加锁成功
大多数节点加锁的总耗时,要小于锁设置的过期时间。
多实例操作,可能存在网络延迟、丢包、超时等问题,所以就算是大多数节点加锁成功,如果加锁的累积耗时超过了锁的过期时间,那有些节点上的锁可能也已经失效了,还是没有意义的。客户端会记录每次加锁消耗的时间并求和,加锁总耗时小于锁失效时间,锁才算获取成功。
释放锁,要向全部节点发起释放锁请求
如果部分节点加锁成功,但最后由于异常导致大部分节点没加锁成功,就要释放掉所有redis实例,各节点要保持一致
Redis主从模式
主从复制,或者叫主从同步,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。
前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 单向 的,只能由主节点到从节点。
Redis 主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。
主从复制的目的:
- 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
- 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。
redis2.7及以前版本
主从复制依赖于sync
当主服务器接收到sync命令后,会执行bgsave命令
主服务器主进程fork的子进程会生成一个RDB文件,同时将RDB快照产生后的所有写操作记录在缓冲区中
bgsave命令执行完成后,主服务器将生成的RDB文件发送给从服务器
从服务器接收到RDB文件后,首先会清除本身的全部数据,然后载入RDB文件,将自己的数据状态更新成主服务器的RDB文件的数据状态
主服务器将缓冲区的写命令发送给从服务器,从服务器接收命令,并执行,完成主从复制
主从同步依赖于命令传播
当主服务器接收命令导致数据发生变化时,为了维护主从状态一致,主服务器会将导致自己数据状态发生改变的命令传播到从服务器执行,当从服务器也执行了相同的命令之后,主从服务器之间的数据状态将会保持一致。比如主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
sync命令的缺点:
- 生成RDB快照文件会占用大量的CPU,磁盘IO资源,令主服务器响应能力下降
- 主服务器将生成的RDB文件发送给从服务器,会占用大量网络IO资源
- 从服务器接收RDB文件并载入,会导致从服务器阻塞,无法提供服务
- 当从服务器掉线再重连后,会产生不一致的问题,而sync无法处理这一情况,只能使用全量同步
redis2.8 — redis4.0
尽量减少全量同步的发生,尽可能使用增量同步,在2.8版本之后使用psync命令代替了sync命令来执行同步操作,psync命令同时具备全量同步和增量同步的功能。
增量同步的实现原理:
- 复制偏移量
- 复制积压缓冲区
- 服务器运行 ID
复制偏移量
这是主从服务器都会维护的参数
主服务器向从服务发送数据,传播N个字节的数据,主服务的复制偏移量增加N
从服务器接收主服务器发送的数据,接收N个字节的数据,从服务器的复制偏移量增加N
假设此时A/B正常传播,C从服务器断线
有了复制偏移量之后,从服务器C断线重连后,主服务器只需要发送从服务器缺少的100字节数据即可
复制积压缓冲区
复制积压缓冲区是一个固定长度的队列,默认为1MB大小。
当主服务器数据状态发生改变,主服务器将数据同步给从服务器的同时会另存一份到复制积压缓冲区中
复制积压缓冲区为了能和偏移量进行匹配,它不仅存储了数据内容,还记录了每个字节对应的偏移量:
所以主服务器可以通过这个缓冲区来知道从服务器缺失了哪些数据
当从服务器断线重连后,从服务器通过psync命令将自己的复制偏移量(offset)发送给主服务器,主服务器便可通过这个偏移量来判断进行增量传播还是全量同步。
- 如果偏移量offset+1的数据仍然在复制积压缓冲区中,那么进行增量同步操作
- 反之进行全量同步操作,与sync一致
可以由用户自定义缓冲区大小:尽可能的使用增量同步,但是缓冲区又不会占用过大的内存
服务器运行ID
当主服务器宕机后,某台从服务器被选举成为新的主服务器,就通过比较运行ID来区分谁是主服务器
运行ID(run id)是服务器启动时自动生成的40个随机的十六进制字符串,主服务和从服务器均会生成运行ID
当从服务器首次同步主服务器的数据时,主服务器会发送自己的运行ID给从服务器,从服务器会保存在RDB文件中
当从服务器断线重连后,从服务器会向主服务器发送之前保存的主服务器运行ID,如果服务器运行ID匹配,则证明主服务器未发生更改,可以尝试进行增量同步
如果服务器运行ID不匹配,则进行全量同步
redis4.0之后
psync升级为psync2.0
psync2.0 抛弃了服务器运行ID,采用了replid和replid2来代替,其中replid存储的是当前主服务器的运行ID,replid2保存的是上一个主服务器运行ID
通过replid和replid2我们可以解决主服务器切换时,增量同步的问题:
如果replid等于当前主服务器的运行id,那么判断同步方式增量/全量同步
如果replid不相等,则判断replid2是否相等(是否同属于上一个主服务器的从服务器),如果相等,仍然可以选择增量/全量同步,如果不相等则只能进行全量同步。
Redis哨兵模式
Redis Sentinel(哨兵):由一个或多个Sentinel实例组成的Sentinel系统,它可以监视任意多个主从服务器
当监视的主服务器宕机时,自动下线主服务器,并且择优选取从服务器升级为新的主服务器。
Sentinel本质上就是一个Redis服务器,一个拥有较少命令和部分特殊功能的Redis服务器
哨兵的职能:
- 监控:不断地检查主节点和从节点是否运作正常。周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行,没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;若主库下线,就会开始自动切换主库的流程。
- 通知:当被监控的某个 Redis 服务器出现问题时, 哨兵可以通过 API 向管理员或者其他应用程序发送通知。当推举出新主库时,哨兵会把新主库的连接信息发给其他从库,和新主库建立连接。同时也会将连接信息通知客户端。
- 故障转移:当 主节点 不能正常工作时,哨兵会开始故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供:客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址,当试图连接失效的主服务器时,哨兵集群也会向客户端返回新主服务器的地址
主观下线和客观下线
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。如果检测的是从库,那么哨兵直接标记为“下线”,因为从库的下线影响一般不太大,集群的对外服务不会间断。
但是,如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,因为有可能出现误判。一旦错误启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下。
因为哨兵通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入集群一起来判断,多个哨兵的网络同时不稳定的概率较小,就可以避免单个哨兵误判的情况。
在判断主库是否下线时,只有大多数(一半以上)的哨兵实例都判断主库已经“主观下线”了,主库才会被标记为客观下线
故障转移的步骤
- 在Slave中选择数据最新的作为新的Master
- 向其他Slave发送新的复制指令,让其他从服务器成为新的Master的Slave
- 继续监视旧Master,如果其上线则将旧Master设置为新Master的Slave
Redis 集群模式
集群,即 Redis Cluster,是 Redis 3.0 开始引入的分布式存储方案。集群由多个节点(Node)组成,Redis 的数据分布在这些节点中。
不同于主从复制模式,集群会提供多个master节点提供写服务,每个master节点中存储的数据都不一样,这些数据通过数据分片的方式被自动分割到不同的master节点上。
为了保证集群的高可用,每个master节点下面还需要添加至少1个slave节点,这样当某个master节点发生故障后,可以从它的slave节点中选举一个作为新的master节点继续提供服务。不过当某个master节点和它下面所有的slave节点都发生故障时,就不可用了。
在搭建分布式集群的时候首先要考虑的是如何将数据按照一定的区分规则映射到对应的节点上,每个节点的数据都是总数据的一部分,所有的节点合成一份总的数据,每个节点的数据相互独立,没有重复的数据。
hash分区
hash分区一般有三种方式
hash取余
对键值进行hash产生一个hsah数值,然后根据服务器的数量进行取模操作
当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移
这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分为 512 或 1024 张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中
一致性hash
一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围一般是 0 - 2^32-1,对于每一个数据,根据 key
计算 hash 值,确定数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器
其根本思想就是对所有的数据进行一次hash运算,然后将映射到对应节点上去,映射条件为距离最近的节点,这样就可以保证集群进行伸缩操作的时候影响的数据最小
哈希槽hash
该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用crc16算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
使用哈希槽的好处就在于可以方便的添加或移除节点:
- 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
- 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
Redis的作者认为它的哈希槽+crc算法计算hash的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。
Redis的分区有一个很显著的特点:分区规则是内存的一个逻辑模块,不需要像原来的关系数据库使用外部挂件进行分区,例如mysql分区通常还会通过mycat进行操作。
集群脑裂
在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据,此时这些数据被旧主节点缓存到了缓冲区里,但是这些数据都是无法同步给从节点的。
这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上正常运行),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点
这时集群就有两个主节点了 —— 脑裂出现了。
然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点,然后从节点会向新主节点请求数据同步
因为第一次同步是全量同步的方式,此时的从节点会清空掉自己本地的数据,然后再做全量同步。
所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
总结就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主节点。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
如何解决:设置参数并监听
当主节点发现从节点下线或者通信的总数量小于阈值时,那么认为主节点失去了与从节点的连接,此时禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
- min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是
主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。
这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线,开始进行主从切换。
同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。
这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题
Redis缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中,请求都会压到数据库,从而压垮数据库。
比如用户一个不存在的用户id获取用户信息
在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义。
缓存穿透解决方案:
- 对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
- 设置可访问的白名单:使用bitmaps;类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问
- 采用布隆过滤器:用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果不在布隆器中,则直接返回。
布隆过滤器是一种概率型数据结构,它可以告诉你某种东西一定不存在或者可能存在,当布隆过滤器说,某种东西存在时,这种东西可能不存在;当布隆过滤器说,某种东西不存在时,那么这种东西一定不存在。 常用于解决一个元素是否在某个集合中的业务场景,但是判断某种东西是否存在时,可能会被误判。
向布隆过滤器中添加key时,会使用多个hash函数对key进行hash并取模获得一个位置,每个hash函数都会算得一个不同的位置,把数组的这几个位置都置为1。
向布隆过滤器询问key是否存在时,计算散列值,看看位数组中这几个位置是否都为1,只要有一个位为0,那么说明布隆过滤器中这个key一定不存在。
如果这几个位置都是1,只是极有可能存在,因为这些位被置为1可能是因为其他的key存在所致。如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。
Redis缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据,这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。比如微博热搜的一个突发事件,如果没有把这个词条作为热点词存储到缓存中或者缓存时间到期,那么用户访问这个词时,就会通过缓存,直接访问数据库,引起数据库压力瞬间增大。
它和缓存穿透的区别在于:缓存击穿是指缓存中没有但数据库中有的数据,由于并发用户特别多,同时读缓存没读到数据,同时数据库取数据引起数据库压力瞬间增大,造成过大压力。缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。
缓存击穿解决方案:
- 预先设置热门数据:在redis高峰访问前,把一些热门数据提前存入redis中,监控哪些数据是热门数据,实时调整key的过期时长
- 使用分布式锁:
Redis缓存雪崩
情况一:当数据保存在缓存中,并且设置了过期时间。如果在某一个时刻,大量数据同时过期,此时,再访问这些数据的话,就会发生缓存缺失,应用就会把请求发送给数据库,从数据库中读取数据。如果应用的并发请求量很大,从而导致数据库压力激增
情况一解决方案
- 避免给大量的数据设置相同的过期时间,可以使用一个较小的随机数来作为波动值(随机增加 1~3 分钟),这样一来,不同数据的过期时间有所差别,但差别又不会太大,既避免了大量数据同时过期,同时也保证了这些数据基本在相近的时间失效,仍然能满足业务需求。
- 服务降级:对于优先级不同的数据采用不同的应对措施来降低数据库压力。当业务应用访问非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;当业务应用访问核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
情况二:Redis 缓存实例发生故障宕机,无法处理请求,导致大量请求同时积压到持久层,从而发生缓存雪崩。
情况二解决方案
- 服务熔断:暂停业务应用对缓存系统的接口访问。业务应用调用缓存接口时,缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,等到 Redis 缓存实例重新恢复服务后,再允许应用请求发送到缓存系统。避免引发连锁的数据库雪崩。但是这种方法对业务应用的影响大,因为暂停了访问
- 请求限流:不暂停,而是限制访问量,在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。
- 事前预防:构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。
缓存更新策略和一致性
Cache Aside(旁路缓存)策略是最常用的策略
应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
写策略:先更新数据库中的数据,再删除缓存中的数据。
写策略的两步不能颠倒,不能先删除缓存再更新数据库,读写并发的时候会出现缓存和数据库数据不一致的问题
假设某个用户的年龄是 20,请求A要更新用户年龄为 21,如果先删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21。此时数据库和缓存就出现了数据不一致的情况。
也不能先更新缓存再更新数据库
假设「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。此时,数据库中的数据是 1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象。
读策略:
- 如果读取的数据命中了缓存,则直接返回数据;
- 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,再返回。