Redis总结

Redis数据结构

  • String字符串

  • Hash

    • 存储对象
  • List

    • 微博关注列表、消息列表

    • 双向链表,支持反向查找和遍历

    • lrange命令支持从某个元素开始读取多少个元素,实现分页查询,基于Redis实现简单的高性能分页,性能高

  • Set

    • 自动排重,很轻易实现交集、并集、差集的操作。比如共同关注,共同好友等功能
  • Sorted Set

    • 排序的实现:增加了一个权重参数score,使集合中的元素能够按照score进行有序排列。比如直播列表中的礼物排行榜,在线用户列表等

    • 保证增删改查的速度:SkipList 跳跃表

    • 跳跃表数据结构:set集合中每个元素任意生成层数,每次增删改查都从第一层开始查询,查询到两数之间的时候调至下一层继续缩小范围,知道最后查询到具体数据或者最小区间

Redis做异步消息队列

  • Redis中的 list(列表)实现异步消息队列,使用rpush / lpush 操作插入队列消息,使用 lpop 和 rpop 来出队消息。使用场景:MQ任务过多时,把任务存在Redis,进行异步消费

  • 队列空了怎么办?

    • 队列空了,客户端不断pop,由于没有数据会不停地进行pop,这样的空轮询会占用CPU资源,redis的QPS被拉高,Redis慢查询可能会显著很多

    • 解决方案:使用阻塞读:blpop,brpop。阻塞读在队列没有数据的时候会立即进入休眠状态,一旦有数据会立即醒来,消费延迟基本为零

    • 上面的方案中,如果阻塞时间过长,Redis客户端连接就成了闲置连接,客户端闲置过久服务器会主动断开连接,减少闲置资源占用,这时候blpop,brpop就会抛出异常来。所以编写客户端消费者时需要注意捕捉异常,增加重试机制

Redis持久化

写时复制(copy on write)

在执行bgsave或bgrewriteaof命令时,Redis都会需要创建当前服务器的子进程,采用COW的方式来提高子进程使用效率。

原理

fork()出子进程共享父进程的物理空间,kernel将父进程所有内存页设置为read-only,当父进程(或者子进程)有内存写入操作时,read-only内存页发生中断,将触发中断操作的内存页复制一份(linux 单位内存页4KB),父子进程各自持有独立的一份(其他部分还是父子进程共享)

优缺点

  • COW技术减少分配和复制大量资源时带来的瞬间延时

  • COW技术可减少不必要的资源分配。比如fork子进程时,父进程的代码段和只读数据段是不允许被修改的,就不需要复制

RDB和AOF

RDB

  • Redis DataBase 快照

  • Redis默认持久化方式

  • 按照一定的时间将内存的数据以二进制文件的格式,快照的方式保存在硬盘中

  • 配置文件中save参数定义快照周期:

    • save 900 1

    • save 300 10

    • save 60 10000

  • 优点:

    • 只有一个文件dump.rdb方便持久化

    • 容灾性好,保存在磁盘

    • 性能最大化,fork子进程进行快照的写入操作,主进程继续处理命令,主进程不进行任何I/O操作,保证Redis高性能

    • 数据集大时,启动效率比AOF高

  • 缺点:

    • 时效性不强,间隔一段时间进行持久化,容易造成数据丢失

AOF

  • append-only file 追加文件

  • 将Redis执行的每次写命令追加记录到单独的日志文件,重启Redis会重新从持久化的日志文件恢复数据

  • 两种持久化方式同时开启时,Redis优先选择AOF进行数据恢复

  • appendfsyncs设置写频率:

    • appendfsync always 总是写入

    • appendfsync everyseca 每秒写入(默认)

    • appendfsync no

  • 优点:

    • 时效性强,数据安全

    • 通过append模式写文件,数据丢失少

    • AOF机制的rewrite模式。AOF文件没被rewrite之前,可以删除其中的某些命令

  • 缺点:

    • AOF文件比RDB文件大,恢复速度慢

    • 数据集大时,启动效率比RDB低

Redis线程模型

Redis线程模型.png
  • 基于Reactor模式开发的文件事件处理器:

    • 多个套接字

    • IO多路复用程序(epoll、线程与kernel内存共享空间mmap)

    • 文件事件分派器

    • 事件处理器

  • 由于文件事件分派器队列消费是单线程,所以Redis被叫做单线程模型

  • 利用I/O多路复用同时监听多个套接字,事件分派器根据套接字目前执行任务为套接字关联不同的事件处理器

  • 由于使用I/O多路复用监听多个套接字,事件处理器既实现了高性能的网络通信模型,又很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,保持Redis内部单线程设计的简单性

Redis的高并发和快速原因

  1. Redis是基于内存的,内存的读写速度非常快,主要时间就消耗在I/O上

  2. Redis是单线程的,省去了很多上下文切换线程的时间

  3. 多路I/O复用模型,“多路”指的是多个网络连接,“复用”指的是复用同一个线程,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,当有事件时才从阻塞态被唤醒处理事件,避免多余操作导致I/O消耗

Redis内存淘汰机制

先说下Redis过期策略:定时删除和被动删除

  • 定期删除:每过100ms,Redis会随机去检查一些设置了过期时间的数据,如果过期就删除,之所以随机访问部分数据的原因是:大部分数据都设置了过期时间,如果每次都遍历去访问,会占用CPU,Redis没法保证高性能

  • 被动删除:访问数据时会检查数据是否过期,如果过期就删除数据

但是如果有很多数据过期了,但是我们又没有去访问它,就会造成即使数据过期了,这些过期数据仍然堆积在内存,导致内存耗尽。这时候就需要Redis内存淘汰机制。

内存淘汰机制包括:

  • volatile-lru:从设置了过期时间的数据集中挑选最近最少使用的数据淘汰

  • volatile-ttl:从设置了过期时间的数据集中挑选即将过期的数据淘汰

  • volatile-random:从设置了过期时间的数据集中随机选择数据淘汰

  • volatile-lfu:从设置了过期时间的数据集中挑选最少使用的数据淘汰

  • allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰

  • allkeys-lfu:从所有数据集中挑选最少使用的数据淘汰

  • allkeys-random:从所有数据集中随机选择数据淘汰

  • no-eviction:禁止驱逐数据,当内存不足以容纳新写入的数据时,新写入操作报错

Redis并发竞争问题

  • Redis本身是单线程模型,不存在并发问题,但是我们使用jedis等客户端对Redis进行并发访问的时候,就存在并发竞争的问题

  • 使用Redis分布式锁。Redis分布锁实现主要是利用redis的SETNX。无论是哪种分布式锁基本原理都是不变的,都是通过数据携带一个标识,根据标识的状态来判断是否获取到锁

  • 消息队列。并发量过大的情况可以使用消息中间件,将Redis.set操作放在队列中一个一个执行

Redis分布式锁

  • 基本特点

    • 使用setnx(不存在则set)来防止并发

    • 通过设置过期时间线程挂了导致redis死锁

    • 通过开启守护线程延长过期时间防止线程业务没执行完但持有的锁已经过期,导致多个线程持有锁

  • 手写实现简易的乐观锁实现(项目里有实现volatile+setIfAbsent,不安全并发高了没有保护机制,延时或者死锁释放,总之就是,手动实现分布式锁特别麻烦)

  • 基于Redisson实现

  • 基于Zookeeper实现

Redisson

Redisson具体执行加锁逻辑都是通过lua脚本完成的,lua脚本能够保证原子性。

RLock lock = redisson.getLock("anyLock");

lock.lock();
lock.unlock();

  • 可重入锁

  • watchDog原理:当线程A拿到锁,线程A锁超时释放,但是线程A的业务还没有执行完;这时候线程B拿到锁,执行自己的业务,这样A,B同时持有锁,分布式锁就没有意义了。Redisson引入watch dog的概念,当线程拿到锁,会有一个后台线程自动延长锁的过期时间,防止业务没执行完而锁过期的情况。


    964670284-5f4377c1980fa_articlex.png

Zookeeper分布式锁

  • 线程每次在zookeeper中争抢锁,只有一个能获得锁

  • 使用Zookeeper临时节点(session),临时节点当客户端连接中断,存活时间耗完就自动删除。这样可以防止获得锁的客户端出现问题而导致死锁。

  • sequence(临时序列节点) + zk的watch机制实现zk分布式锁:

    • 多个线程竞争时,每个客户端对某个方法加锁时,在zk上与该方法对应的指定节点的目录下,各自生成一个唯一的临时顺序节点。

    • 判断获取锁时,客户端只需要判断自己创建的节点的序号是否是所有节点中序号最小的子节点,如果是,则获取锁;

    • 如果不是,则watch子节点变更信息,当收到子节点变更通知之后,再重复步骤二,直至获取锁

Redis事务

  • Redis事务通过MULTI,WATCH,EXEC,DISCARD四个原语实现

    • WATCH命令是一个乐观锁,可以监控一个或者多个key,一旦其中一个key被修改(或删除),之后的事务不执行,监控一直持续EXEC命令被执行。

    • MULTI用于开启一个事务,总是返回OK。MULTI之后客户端可以向服务器发送任意多条命令放在一个队列中(返回queue),当EXEC命令被调用时所有队列的命令才能执行。

    • EXEC:执行所有事务块内的命令,命令按队列先后顺序排列。

    • DISCARD:清空事务队列,退出事务。

  • 事务隔离,多个线程进入,谁先执行EXEC命令就先执行谁,排他性。

  • Redis事务不支持回滚。有命令执行失败则继续执行之后的命令,保证Redis内部简单且快速(性能为王)。

  • Redis命令有语法错误,则语法检查不通过,所有命令都不执行;命令出现运行错误,继续执行之后的命令,不回滚。

  • Redis满足ACID的一致性和隔离性(事务管理ACID:Atomicity 原子性;Consistency 一致性;Isolation 隔离性; Durability 持久性)

Redis高可用

Redis高可用两种方案:主从;哨兵

Redis主从复制

Redis主从复制,一主多从,读写分离。主节点负责写操作,并将数据复制到其他slave节点,从节点负责读,支撑读高并发。实现缓存最终一致性。

复制模式

  • 全量复制:Master全部同步到Slave

  • 部分复制:Slave数据丢失进行备份

Redis主从复制核心原理

  • 当开启一个slave node时,它会发送一个PSYNC命令给master node

  • 如果是slave node初次连接到master node,则触发一次全量复制(full resynchronization)。此时master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端client新收到的所有写命令缓存在内存中

  • RDB文件生成完毕后,master会将这个RDB文件发送给slave,slave先写入本地磁盘,再从本地磁盘加载到内存中

  • 然后master会将内存中缓存的写命令发送到slave,slave也会同步这些数据

  • slave节点如果和master节点有网络故障,断开连接后会自动重连,重连后master节点只会复制给slave部分缺少的数据(部分复制)

过程原理

主从复制.png
  • 当主从库建立MS关系后,slave会向master发送SYNC命令

  • master库接受到SYNC命令后,就开始在后台保存RDB快照,并将期间接收的写命令缓存起来

  • 当快照完成,master会将快照文件和所有缓存的写命令发送给slave

  • slave接收到后,会载入快照文件并执行收到的缓存的写命令

  • 之后,master每当接受到写命令都会将命令发送给slave,从而保证数据一致

主从配置不一样会有什么问题

  • maxmemory不一致:数据丢失

  • 优化参数不一致:内存不一致

缺点

所有slave节点数据的复制和同步都有master节点处理,master节点压力过大。master节点一旦宕机,整个架构就不可用了。

哨兵模式(高可用)

基于主从方案的缺点还是很明显的,假设master宕机,那么就不能写入数据,那么slave也就失去了作用,整个架构就不可用了,除非你手动切换,主要原因就是因为没有自动故障转移机制。而哨兵(sentinel)的功能比单纯的主从架构全面的多了,它具备自动故障转移、集群监控、消息通知等功能。

哨兵模式用于监控Redis主从节点,实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。哨兵可以同时监视多个主从服务器,并在被监视的master下线时,自动将某个slave节点提升为master节点,由新的master继续接收命令

哨兵监控过程

  • 初始化sentinel,将普通Redis代码转换为sentinel专用代码
  • 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
  • 创建和master的两个连接,命令连接和订阅连接,并订阅sentinel:hello频道
  • 每隔10s向master发送info命令,获取master和它下面的所有salve的当前信息
  • 当发现有新的salve加入,sentinel和新的salve同样建立两个连接,同时每个10s发送info命令,更新master信息

节点下线

  • 主观下线:Sentinel集群每一个节点会定时对Redis集群的所有节点发送心跳包检测节点是否正常,如果节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该Sentinel节点就会认为这个Redis节点下线

  • 客观下线:所有Sentinel节点对Redis节点失败要达成共识,即超过quorum数量的Sentinel都认为该Redis节点主观下线,Redis才真正被认为下线

Leader选举

  • 选举一个Sentinel作为Leader:集群中至少要有三个Sentinel节点,但只有Leader Sentinel 节点才能完成故障转移

  • 选举流程

    • 每个主观下线的Sentinel节点向其他Sentinel节点发送命令,要求设置自己为领导者

    • 收到命令的Sentinel节点如果没有同意其他Sentinel节点发送的命令,则同意该请求,否则拒绝

    • 如果该Sentinel节点发现自己的票数已经超过Sentinel集合半数且超过quorum,则它成为领导者

    • 如果在这个过程中有多个Sentinel节点成为领导者,则等待一段时间再重新选举

故障转移

  • 选举出的Leader Sentinel进行故障转移;Sentinel集群运作过程中故障转移完成,所有Sentinel又会恢复平等。Leader仅仅是故障转移操作出现的角色

  • 转移流程

    • Sentinel选出一个合适的Slave节点作为新的Master

    • 向其余Slave发出通知,让它们成为新Master的Slave

    • 等待旧Master复活,并使之成为新Master的Slave

    • 向客户端通知Master变化

  • 选择新Master节点的规则:

    • 选择slave-priority最高的节点

    • 选择复制偏移量最大的节点(同步数据最多)

    • 选择runId最小的节点

  • 定时任务

    • 每1s每个Sentinel对其他Sentinel和Redis节点执行ping,进行心跳检测

    • 每2s每个Sentinel通过Master的Channel交换信息(pub-sub)

    • 每10s每个Sentinel对Master和Slave执行info,目的是发现Slave节点,确定主从关系

  • 缺点

    • 主从服务器的数据要经常进行主从复制,性能受影响

    • 当主服务器宕机,Slave节点切换成Master节点那段时间,服务不可用

Redis集群(高并发+大数据量)

如果说依靠哨兵可以实现redis的高可用,如果还想在支持高并发同时容纳海量的数据,那就需要redis集群。redis集群是redis提供的分布式数据存储方案,集群通过数据分片sharding来进行数据的共享,同时提供复制和故障转移的功能。

节点

一个redis集群由多个节点node组成,而多个node之间通过cluster meet命令来进行连接,节点的握手过程:

  1. 节点A收到客户端的cluster meet命令

  2. A根据收到的IP地址和端口号,向B发送一条meet消息

  3. 节点B收到meet消息返回pong

  4. A知道B收到了meet消息,返回一条ping消息,握手成功

  5. 最后,节点A将会通过gossip协议把节点B的信息传播给集群中的其他节点,其他节点也将和B进行握手

节点.png

一致性Hash

  • 如果直接使用Hash算法 hash(key) % length,当缓存服务器变化时(宕机或新增节点),length字段发送变化,就会导致所有缓存数据都要重新进行Hash运算,原来的数据就访问不到了,大量数据请求容易导致服务器雪崩,因此,引入一致性Hash

  • 一致性哈希:通过对2^32取模来计算Hash值,保证增删缓存服务器时数据不变。

  • 将2^32想象成一个环,一致性哈希算法通过将缓存服务器和被缓存对象都映射到Hash环上以后, 从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器就是当前对象将被缓存的服务器,由于被缓存对象和服务器Hash后的值固定,所以在服务器不变的情况下,一个对象必定缓存在一个固定的服务器上。

  • Hash环偏斜问题:

    将现有的物理节点通过虚拟的方法复制出来,而复制出来的节点被称为"虚拟节点",让服务器尽量多的,均匀的出现在Hash环(注意Hash环是一个虚拟出来的模型,方便理解,不是真实的)

  • 一致性Hash解决容灾问题:

    如果其中一个节点宕机了,其他服务器的对象仍然能被命中,因为对象的映射到服务器的位置已经固定了,不会因为服务器宕机而导致独享找不到,宕机的服务器上的对象会在下次容灾分配时,重新分配到就近的服务器上

一致性Hash.png

缓存策略

更新策略

allkeys-lru、volatile-lru 最近最少

缓存最终一致性

Redis主从模式就可以满足缓存的最终一致性。如果不用主从模式,则:

  • 读请求:先读缓存,缓存没有就读数据库,取出数据放入缓存,同时返回响应

  • 写请求:先更新数据库,然后再删除缓存(直接删除缓存的原因:1.是为了避免大量写而不经常读的情况,造成冷数据过多,导致缓存频繁更新 2.两个线程同时进行更新操作,线程A先更新了数据库,线程B后更新数据库,但是由于一些原因导致线程B比线程A先更新了缓存,导致了脏数据。所以直接删除缓存是最好的)

  • https://blog.csdn.net/koli6678/article/details/88202245

  • https://www.jianshu.com/p/fbe6a7928229?utm_source=oschina-app

不通过主从模式实现缓存最终一致性.png
*   应用直接写数据到数据库中
    
*   数据库更新binlog日志
    
*   利用Canal中间件读取binlog,并借助限流组件按频率将数据发到MQ
    
*   应用监控MQ通道,将MQ的数据更新到Redis缓存中

缓存击穿

  • 一个热门Key一直扛着大并发量的访问,当key从Redis失效时,访问直接落到数据库,并发量大时而导致数据库压力过大

  • 解决方案1:将缓存设置为永不过期,这样只有在改变key值时才需要清除缓存重新缓存

  • 解决方案2:回归起点,造成问题的是并发量压垮数据库,增加分布式锁,setNX获取到锁的才去DB查询,没有获取到的线程,sleep一段时间,再争抢锁

缓存穿透

  • 当客户端发起大量缓存和数据库都不存在的数据请求(代码bug,恶意攻击),无法利用缓存,查询数据库还会造成查询错误,流量过大而导致数据库压力过大

  • 首先一定要做好的是参数鉴权,可以防止大量非法请求走到查询这步(已经可以防止大部分缓存穿透的情况了)

  • 解决方案1:缓存空值,对数据库没有查询到的key赋予空值缓存在Redis(这种方式个人认为不好)

  • 解决方案2:采用布隆过滤器,redis继承了一个bloomFilter

  • 过滤原理:

    • 准备一个bitmap数组,用于存放Hash值

    • 准备n个映射函数(输入:redis的key值和salt;输出:int类型的hash值)

    • 每次有新数据进入,都用这n个映射函数对key计算hash值,计算出来的值对应bit数组上的位置设置为1

    • 当有新的查询时,将需要查询的key通过这n个映射函数求取hash值,如果这些hash值对应bit数组的位数都为1,则说明这个key"可能"在缓存中,让它先走缓存查找,查询不到再走持久层查找;如果有一个hash值对应位数为0,则说明这个key一定不在查询范围中,直接返回不允查询

布隆过滤器.png

缓存雪崩

  • 一时间大量缓存失效或者缓存崩溃,请求直接落在数据库上,很可能由于无法承受大量的并发请求导致数据库崩溃

  • 应对方案1:

    • 过期时间随机设置(不知道好不好)
  • 应对方案2

    • 事前:Redis主从复制+哨兵模式,Redis Cluster,保证高可用避免崩盘

    • 事中:增加本地ehcache缓存+hystrix限流&降级,避免数据库承受太多压力

    • 事后:Redis持久化(RDB,AOF),一旦重启,自动从磁盘上加载数据,快速回复缓存数据

  • 请求过程

    • 用户请求的时候先访问本地缓存,没有命中本地缓存再访问Redis,如果还有没有命中Redis缓存,再查询数据库,并把数据添加到Redis和本地缓存中

    • 设置了限流,一段时间内超出的请求走降级处理(hystrix支持自己定义降级方法,返回默认值或者给出友情提示就好了)

你可能感兴趣的:(Redis总结)