分布式与微服务☞web组件☞Redis

159 阅读16分钟

基本介绍

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。 Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

数据结构

别人写得好,就抄 超详细Redis数据结构底层实现原理介绍 - 掘金 (juejin.cn)

架构

主从架构

主从复制,是指将一台 Redis 服务器的数据,复制到其他 Redis 服务器,我们将前者称为主节点 master,将后者称为从节点 slave(replica)。在这个过程中,数据的复制是单向的,即只能从主节点到从节点。并且从节点只能读数据,不能写数据,实现读写分离。

image-20210523232751628

  • 一个主节点可以有多个从节点,一个从节点只能有一个主节点。所有的服务器默认都是主节点。
  • 从节点下面还可以有从节点,形成一个图的结构

主从复制的优点

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,写数据时应用连接主节点,读数据时应用连接从节点,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  4. 高可用基础:主从复制是哨兵模式和集群能够实施的基础。

主从复制的实现可以分为三个阶段:建立连接、数据同步、命令传播

image.png

哨兵架构

Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例组成的Sentinel 系统可以监视任意多个主服务,以及这些主服务器属下的所有从服务,并在被监视的主服务进入下线(不可服务)状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

总结一下哨兵的作用:

  • 集群监控 不断的检查master和slave是否正常运行(master存活检测、master与slave运行情况检测)
  • 消息通知 当被监控的服务器出现问题时,向其他哨兵、客户端发送通知
  • 自动故障转移 断开故障master与slave的连接,选取一个slave作为新master,将其他slave连接到新的master并告知客户端新的服务器地址。 注意:哨兵也是一台Redis服务器,只是不提供数据服务;通常哨兵配置的数量为单数。 image.png

集群架构

在集群模式下都是通过全量数据冗余来保证数据的一致性与可用性,在空间上造成了巨大的浪费。这一节我们将介绍Redis的分布式集群存储方式RedisCluster,它的单个节点上不在是全量数据,而只含有整个集群的一份数据。这样既改善了存储空间浪费的问题,同时也增横向增加了Redis服务整体的吞吐性。RedisCluster将所有数据存储区域划分为16384个slots(槽位,过多的槽位会导致心跳包过大,1000个节点以下这么多的槽位足够了),每个节点负责一部分槽位,槽位的信息存储于每个节点中。当客户端请求进来时候会拉去一份槽位信息列表缓存在本地,RedisCluster的每个节点会将集群的配置信息持久化到自己的配置文件中,所以需要引入一套可维护的配置文件管理方案,尽量做到自动化。

  • 槽位算法:RedisCluster 默认会根据key使用crc16算法进行hash得到一个整数,然后用这个整数对16384取模定位key所在的槽位。它还运行用户在key字符串里面嵌入tag将key强制写入指定的槽位。
  • 迁移:当有新的节点加入或者断开节点时,就会触发Redis槽位迁移。当一个槽位正在迁移时候在原节点的状态为migrating,在目标节点的状态为importing。原节点的单个key执行dump指令得到序列化内容,再向目标节点发送restore携带序列化内容作为参数的指令,目标节点接收到内容后反序列化复制到内存中,响应给原节点成功。原节点收到成功响应后把当前节点的key删掉就完成了节点数据迁移。这个过程是一个同步的操作,在复制完成之前原节点时处于阻塞状态的,不会进入新的数据,直到原节点的key被删除完成。如果key内容过大就会导致迁移阻塞时间过长,出现卡顿现象,所以再次强调大key的危害。上面说完了在迁移过程中服务端的变化,现在我们来说一下槽位迁移对于客户端的变化:这时候新旧节点会同时存在部分key,客户端访问到旧节点,如果旧节点存在就正常处理返回。如果客户端访问的数据不在旧节点,它会向客户端发生一个重定向指令(-ASK targetNodeAddr),客户端收到重定向后,先去目标节点执行一个不带参数的asking指令,然后在目标节点执行操作。因为在没有完全迁移完槽位目标节点还不归新节点管理,如果只适合直接发生操作指令,目标节点会返回给客户端一个-MOVED重定向指令,让它去原节点执行,这样就出现了重定向循环。不带参的asking指令目的就是打开目标节点选项让它当做自己的槽位的请求来处理。通过上面的过程得知在迁移过程中,平时的一个指令需要三个ttl才能完成。
  • 跳转:当RedisCluster发生槽位变化的数据迁移时,这时候客户端保存的槽位信息就和RedisCluster的槽位信息不一致,当客户端访问到错误的槽位时候,当前槽位会相应给客户端一个可能包含此数据的槽位信息,当客户端访问成功后更新本地槽位信息。
  • 容错:RedisCluster为每个主节点设置了若干从节点,主节点故障时,集群会主动提升某个从节点作为主节点,当无主节点时Redis整个不可用。也可以通过 cluster-require-full-coverage参数设置允许部分节点故障,其他节点依然可以对完提供服务。实际在异常无处不在生成环境中突然部分节点变得不可用,间隔一会又突然好了是很常见的时候,为了解决这问题我们也通过设置容忍最大离线时间(cluster-node-timeout)来避免,当超过这个最大超时时间则认为节点不可用。还有一个作为被乘数系数来放大超时时间的参数:cluster-slave-validity-factor,当值为0的时候是不能容忍异常短暂离线,系数越大相对越宽松。RedisCluster作为一个去中心化的中间件,一个节点认为某个节点离线,叫可能离线,当所有或者当大多数节点认为某节点离线才叫真正离线,此时集群会剔除此节点或者触发主从切换。它是用Gossip协议来广播自己的状态以及对整个集群变化的感知。比如一个节点发现某节点离线,它会将这个信息向整个集群广播,其他节点也会受到这个信息,如果收到信息的节点发现目标节点状态正常则不更新这条信息,并发送目标节点正常的消息给整个集群,就这样一个个节点的传播消息。当整个集群半数以上节点都持有某目标节点已离线的的信息时候才认为,某目标节点离线的这一事实,否则不予处理。

缓存+数据库

实现Redis缓存和数据库的数据一致性的方法

这张图,大多数人的很多业务操作都是根据这个图来做缓存的。但是一旦设计到双写或者数据库和缓存更新等操作,就很容易出现数据一致性的问题。无论是先写数据库,在删除缓存,还是先删除缓存,在写入数据库,都会出现数据一致性的问题。列举两个小例子。

  1. 先删除了redis缓存,但是因为其他什么原因还没来得及写入数据库,另外一个线程就来读取,发现缓存为空,则去数据库读取到之前的数据并写入缓存,此时缓存中为脏数据。
  2. 如果先写入了数据库,但是在缓存被删除前,写入数据库的线程因为其他原因被中断了,没有删除掉缓存,就也会出现数据不一致的情况。 总的来说,写和读在多数情况下都是并发的,不能绝对保证先后顺序,就会很容易出现缓存和数据库数据不一致的情况,还怎么解决呢?
  • 方案一:采用延时双删策略 基本思路: 在写库前后都进行删除缓存操作,并且设置合理的超时时间
    基本步骤: 先删除缓存–再写数据库—休眠一段时间—再次删除缓存
    注:休眠的时间是根据自己的项目的读数据业务逻辑的耗时来确定的。这样做主要是为了保证在写请求之前确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
    该方案的弊端: 集合双删策略+缓存超时策略设置,这样最差的结果就是在超时时间内数据存在不一致,又增加了写请求的耗时。

  • 方案二:一步更新缓存(基于订阅Binlog的同步机制) 基本思路: mysql Binlog增强订阅消费+消息队列+增量数据更新到redis—读redis:热数据基本上都在redis—写mysql:增删改都是操作mysql—更新redis数据:mysql的数据操作Binlog,来更新redis

应用注意事项

持久化

Redis之所以能被称之为内存数据库而不单单是内存缓存是因为其有自己的持久化机制,可以持久化数据库到硬盘当中,在宕机之后能及时的恢复,其持久化方式主要有两种,AOF和RDB,下面的内容是对这两种方式的总结。

  • AOF AOF方式也就是Append Only File,其是在执行命令之后,将对应的日志写到日志文件中的,因为是先执行操作,再写日志,因此不会阻塞当前的写操作。 何时将日志刷盘,决定了Redis宕机恢复时丢失数据的多少,Redis提供了三种策略
  1. Always: 同步写回,命令执行完成,立马写回到磁盘
  2. Everysec: 每秒写回,隔一秒就将缓存页数据写回磁盘
  3. No: 依赖操作系统控制的写回,操作系统自己决定刷盘时机。 以上三种策略,对数据的可靠性要求越高的对性能的影响也就越大,因此如何配置是一个取舍问题。 AOF日志写的格式是执行命令的Redis协议内容,Redis协议是文本协议,自然占据空间比较大,而由于其记录的是操作流,对同一个Key的不断修改使得AOF日志中记录了很多历史的Value,为了解决这个问题,Redis引入了AOF重写机制,如何重写,是否影响Redis对外提供的服务是我们关心的内容,下面是关于AOF重写过程的总结,以问题的形式呈现。
  • RDB 上面讲到了AOF来记录Redis操作,当进行数据恢复的时候,AOF需要从头到尾执行命令才能恢复到最新的状态,而如果能够直接将内存快照下来,就会快很多,这就是RDB也就是Redis Data Base 执行RDB操作有两个命令可以直接执行,save和bgsave。其中save是在主进程中执行save操作,会阻塞主进程,而bgsave会创建一个子进程,来负责生成快照。
  • AOF与RDB混合模式 混合模式同时使用了两种方式来生成快照,在配置文件中指定的时间周期生成RDB,最后又将过程中发生的写操作以AOF的形式写到快照文件中。
  • 数据恢复 如果只配置 AOF ,重启时加载 AOF 文件恢复数据;如果同时配置了 RDB 和 AOF ,启动是只加载 AOF 文件恢复数据;如果只配置 RDB,启动是将加载 dump 文件恢复数据

高性能

基于内存+单线程+IO复用+高效的数据结构和内存分配回收机制

缓存击穿

  • 缓存穿透 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
    方案一: 缓存空对象
    当存储层未命中后,即使数据库返回的空对象也对其进行缓存,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
    方案二: 布隆过滤器
    布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
    方案三: 设置可访问的白名单
    使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
    方案四: 进行实时监控
    当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务 \
  • 缓存击穿 key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,但是缓存回设期,这个时候大并发的请求可能会瞬间把后端DB压垮。
    方案一:预先设置热门数据
    在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
    方案二:设置热点数据永不过期
    从缓存层面来说,没有设置过期时间,所以就不会出现热点key过期产生的问题
    方案三:实时调整
    现场监控哪些数据热门,实时调整key的过期时长
    方案四:使用分布式锁
    使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程在没有获取分布式锁的权限时,需要等待。这种方式将高并发的压力转移到了分布式锁,因此对于分布式锁的考验很大。\
  • 缓存雪崩 缓存雪崩,是指在某个时间段,缓存集中过期失效,导致数据库异常的状况。
    key对应的数据存在,但在redis中过期或者是缓存服务直接宕机,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
    方案一:构建集群
    通过搭建Redis集群,保障Redis的高可用
    方案二:搭建多级缓存架构
    Nginx缓存+Redis缓存+其他缓存(ehcache等)
    方案三:限流降级
    在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,从而避免失效时大量的并发请求落到底层存储系统上。比如对某个key只允许一个线程查 询数据和写缓存,其他线程等待。不适用高并发情况。
    方案四:数据预热
    在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
    方案五:设置过期标志更新缓存
    记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
    方案六:将缓存失效时间分散开
    比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。