Redis

Redis

Redis 概述:

  • Redis是一种NoSQL非关系型内存数据库,数据没有预定义模式,都是KV值,没有声明性查询语言,基于乐观锁的松散事务控制。
  • 支持多种数据结构,内置复制、LUA脚本、LRU驱动时间、事务、和不同级别的磁盘持久化,并通过哨兵和自动分区提供高可用。

应用场景:

  1. 缓存:将数据库中的热点数据存储到Redis缓存服务器中,查询请求会先在Redis中查找所需数据,查到直接返回,可以减缓数据库的压力。
  2. 数据临时存储位置:通常采用token令牌作为用户登录系统时的身份标识,而token就可以在Redis中临时存储。
  3. 解决Session不一致:通过Spring提供的SpringSession来解决分布式环境中Session不一致的问题,而Redis可以为SpringSession提供存储空间。
  4. 流式数据去重:可以用Redis中的Set数据类进行流式数据的存储达到去重的目的。
说明:
  1. 由于Redis是基于内存的数据库,通常用来作为内存的缓存,可以提高查询速度。
    • 没有Redis的情况下:客户端查询请求->服务端->业务处理->DB-磁盘。
    • 存在Redis的情况下:客户端查询请求->服务端->业务处理->Redis-内存(若查到数据直接从内存返回)。
  2. Session通常用来维护用户的状态,在分布式系统中存在Session不一致问题,用户在其中一个节点登录成功后进行一系列操作时,这些操作可能由不同的节点处理,其他节点并不认为当前用户时登录状态,会导致用户的Session不一致。
  3. 短信验证码的临时存储也可以通过Redis存储,规定时间内会与Redis中的数据进行匹配,超时会销毁Redis中的验证数据则认为验证失效。

Value的常用5种常用数据结构:

  • String:Redis中最基本的数据类型,是key对应的单一值,且二进制安全不存在在编码导致数据的改变为题,可以包含任何数据,比如图片、序列化对象,每个字符串最大容量512M。
  • List:Redis列表是简单的字符串列表,底层基于双向链表,头尾操作效率最高,中间效率最低,维护了两套索引。0 、 -1
  • Set:Redis中的Set也是无序不可重复的,底层基于哈希表实现。
  • Hash:Redis中的hash本身就是一几个键值对集合。类似一个map。
  • Zset:Redis的Zset和Set一样都是不允许重复的,但是Zset是有序的,因为底层还维护了一个socre用来进行排序,在Zset中的值是唯一的,但score是可重复的。

说明:

1.Redis中的数据本身都是一键值对的形式存在的,Key都是String类型,而Value通常为上面的五种类型。

2.Redis中默认有16个数据库,16384 个slot。

Redis使用时需要考虑的问题:

  1. 需要存储什么数据。
  2. 用什么结构存储。
  3. RowKey如何设计。

缓存穿透、缓存雪崩、缓存击穿:

  • 击穿:单个key失效,某个热点key扛着大量并发,Key失效瞬间,持续大量的并发击穿缓存,直接请求数据库,增加数据库的压力。

    解决方案:该key不设置过期时间。

  • 穿透:单个Key失效,查询不存在的key,会导致该请求每次回访问数据库,造成缓存穿透。

    解决方案:

    1.将空对象进行缓存,并设置一个较短的过期时间,减少数据库的请求。

    2.采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询一个不存在的key会被bitmap拦截,减小底层存储系统的查询压力。

  • 雪崩:大量key短时间内同时失效,发生大量的缓存穿透,所有查询都落在数据库上,造成缓存雪崩。

    解决方案:针对key设置不同的过期时间,让过期时间不分布再同一个时间点。

说明:

1.Redis的热点数据设置过期时间只能针对redis自身的key设置过期时间, 如果设置了过期时间且使用的是Hash结构,会导致雪崩。2.Redis是根据redis自身key的hash值进行判断数据分配到那个节点,使用Hash结构会使整张表的数据存储在同一个节点。

Redis持久化机制:

  • 由于Redis是基于内存的数据存储系统,内存中的数据存在安全问题,Redis通常采用持久化机制来保证数据的安全性。
  1. RDB机制:每隔一段时间会将内存中的数据作为一个快照存储到磁盘中。Redis默认开启。

    • 触发时机:
      1. 自动触发: 900 1 / 300 10 / 60 10000 秒/更改Key次数, 更改次数越多存储越频繁。
      2. 手动存储: Save同步方式/Bgsave异步方式。
      3. 关闭服务存储 :SHUTDOWN命令让Redis正常退出,Redis就会执行一次持久化保存。
    • 优势:大规模数据恢复,速度快。
    • 缺点:RDB快照无法保证拥有全部的数据,快照非实时触发,而是根据触发条件进行快照,可能刚快照完还没有进行下一次快照这之间有大量操作且释放内存会丢失最后一次快照后的所有修改,不能绝对保证数据的高度一致性和完整性,且Fork时会克隆内存中的数据造成2倍的膨胀性。
  2. AOF机制:根据配置文件的指定策略,只保存生产数据的指令到磁盘。Aof可以通过配置文件中设置的存储频率对内存中的数据进行存储,频率很高。Redis的AOF持久化机制都是通过追加的方式向磁盘中写入数据,而追加写效率非常高。如:HDFS、Edits、Kafka、Wal、AOF等。

    • AOF重写:AOF重写要解决的问题是将之前删除的操作以及删除掉的数据从aof文件中去除,没必要在恢复时重写加载一些已经丢弃的数据,并且还会将一些多次操作完成而产生数据结果的中间过程去掉,只保留最终的结果。

    • AOF文件损坏修复:Redis读取了损坏的持久化文件会导致启动失败,此时就需要执行Redis命令修复持久化文件。

      说明:所谓修复持久化文件仅仅是把损坏的的文件中无法识别的部分丢弃,会造成损坏的数据会丢失而没法把受损的数据找回,修复后不能保证数据的完整性,所以文件虽坏以后通常不建议check-Aof修复,如果损坏文件不多可以自己进行修改。

    • 优势:通过AOF机制的 'appendfsync-always'方式理论上可以做到数据的完全一致,但由于频繁的刷写会导致性能降低,且文件具有可读性,能够分析Redis工作情况。

    • 缺点:1.由于AOF的持久化文件需要维护自身的数据结构,会导致添加相同的数据比RDB机制占用的磁盘更多;

      2.恢复速度比RDB慢,效率在同步写入时低于RDB,不同步写入时与RDB相同。

    说明:RDB和AOF的持久化文件默认使用相同的存储路径dir。

  3. Redis为'appendonly.aof'的持久化提供了三种配置方式:

    • 'no':只将数据写到OSbuffer,由操作系统决定何时将数据写到磁盘,这种方式速度最快。
    • 'always':每次在appendonly.aof 中追加内容都调用fsync()将数据写入磁盘,这种方式最慢但是最安全。
    • 'everysec':默认配置,表示每秒调用一次fsync()将数据写入磁盘,是一种折中的方式。
  4. AOF与RDB共存:

    • Redis启动时默认会优先加载AOF持久化文件来恢复原始数据,两者同时使用时Redis启动只会找AOF持久化文件,因为RDB不实时,AOF通常情况下比RDB保存的数据完整, 但是通常两者一起使用,因为AOF在不断变化不好备份,而RDB更适合用于备份数据库、快速重启,而且AOF可能存在潜在的BUG,RDB可以留作备份以防万一。

    • 使用建议:

      如果Redis仅仅作为缓存可以不使用任何持久化方式。其他应用方式综合考虑性能和完整性,以及一致性要求。
      RDB文件只用作后备用途,通常只在Slave上持久化RDB文件,只需15分钟备份一次,只保留save 900 1。
      如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件即可。代价一是带来持续的IO,二是AOF中rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎不可避免的。只要硬盘许可,应该尽量减少AOF中rewrite的频率,而且AOF重写的基础大小默认值64M太小,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。 如果不开启AOF,仅靠Master-Slave Replication 实现高可用性能也不错。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时Down掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,会选择载入较新的那个。

Redis事务:

  • Redis事务是基于乐观锁的松散事务,与传统的关系型数据库的事务不同,Redis的事务没有回滚,所以事务失败时不进行回滚而是继续执行下一条命令。所以队列中的操作只要没有语法错误都会执行,当对错误类型进行了INCR操作导致执错误的不执行,会继续执行下一条命令。而在组队的时候出现的语法错误,整个队伍队列中的操作都不会执行。

  • 悲观锁:认为当前环境非常容易发生碰撞其他人会同时想要来修改当前正在操作的,所以执行操作前需要把数据锁定,当操作的数据加上悲观锁锁以后其他的操作会等待锁释放后再操作,不会放弃操作。

    说明:悲观锁锁上的数据其他用户连看的权力都没有,不放弃操作。

  • 乐观锁:实质上是没有锁的,认为当前环境不容易发生碰撞,所以执行操作前不锁定数据,如果发生碰撞,那么放弃自己的操作。所有操作根据版本号进行,先看版本号,修改之前再看版本号,相同可以修改,不同不能修改,修改过后同时修改版本号。

    说明:乐观锁会放弃版本号不一样的失败操作,所有人都可以看乐观锁中的数据。

Redis事务基于乐观锁,所以在继续事务的组队操作前需要Watch看一下版本号,修改数据时会自动去对比该版本号。

Redis主从复制机制:

  • 通常一个Redis的Master会配备两个Slave。
  • 作用:
    1. 实现读写分离:从机不能主动接收数据,Master负责所有的写操作,而slave只负责读,并且需要去同步master的数据。
    2. 故障转移:当master故障以后其中一台slave可以转换成master。
  • 好处:1.Master只负责数据写,Slaver只负责数据的读,可以更好的适应不同的模式工作,提高读写效率;
    2.由于Master故障后其中的某个Slave会切换成master,避免了单点故障问题。

Redis集群:

  • Redis集群并非中心化集群,而是多个节点共存,节点之间可以相互进行转发,每个节点都有一个主机两个从机,三个节点共同管理16384个slot插槽,插入key时会进行为运算获取slot位置,每个主从集群负责一个区域的slot,该节点集群完全挂掉时负责区域的插槽slot就用不了了。

    说明:单独的主从机并不是真实的集群,而是一个Redis的一个主从机制。

Redis哨兵机制:

  • 在Master故障以后,须有手动将Slave切换为Master,并且将其他的Slave设置为新的Master的Slave,非常不方便,所以采用哨兵机制。
  1. Master故障,Slaver什么都不做,会认为Master只是临时离开过一会儿还会回来,手动切换Slave为新的Master以后,之前故障的Master重启后还是Master,需要手动的将其设置为新Master的Slave。

  2. Slave故障,会被认为已经移除队伍,但Slave重启后不在是之前队伍的一员,需要重新设置为Master的Slave。

  • 哨兵的作用:通过哨兵服务器监控 Master/ Slave 实现主从复制集群的自动管理,当Master故障后,哨兵通过客观下线然后选出新的Master,并且将其他的Slave设置为新的Master的Slave,当原来的Master重启后会被列入到新Master的Slave队伍中;而Slave故障以后哨兵会认为他离线,当Slave重启后会重新归队。
  • 主观下线: 单个哨兵检测到某个节点服务下线。
  • 客观下线: 检测到某个节点服务器下线的哨兵数量达到指定数量,后认为该节点服务下线。可以在配置文件中设置Sentinel同意下线的哨兵数量。
  • 主观下线的问题:单个哨兵检测到节点服务下线可能时网络延时或其他原因造成的故障,只有通过客观下线才能重新选除新的Master。
  • 新Master的选择:会先选择Slave优先级高的(越小越高),如果一样会选offset小的(越小同步的数据越完整),如果还一样听天由命选择RunID小的(这玩意随机发的每次都不一样)。

Redis发布订阅:

  • Redis也可以作为消息中间件,需要通过发布订阅。

说明:Redis消息的订阅发布中,必须有订阅者订阅才可以发布消息,不然没法发布。

思考:

Redis 持久化期间如何对外提供服务?

  • RDB持久化时,手动存储时调用bgsave,以异步的方式写入磁盘,bgsave会调用Linux的fork()函数来创建一个子进程,让子进程来生成快照,期间redis依然可以对外提供服务。而save命令是以同步的方式写入磁盘,Redis进程会被阻塞,直到快照生成完成,期间redis不能对外提供服务。

在数据进行快照的过程中进行了数据的修改,最后持久化的时什么时候的数据?

  • 对于save的方式来说,生成快照期间redis不能对外提供服务,所以在快照生成期间不存在数据修改。但是对于bgsave方式来说,生成快照期间redis依然可以对外提供服务,所以极有可能有些数据被修改。这时子进程是根据快照开始时刻的数据来生成快照的。在快照生成期间修改的数据只能在下一次生成快照时处理。但是在这期间被修改的值,对外部调用方来说是可以实时访问的。也就是说redis不仅要存储快照生成点时刻的所有值,还要存储变量的最新值。这样redis中6G的数据,在生成快照的时候会瞬间变成12G。

  • Redis使用了Copy On Write 机制来解决生成快照过程中修改数据而导致最终生成快照时6G数据变成12G。

    CopyOnWrite机制:如果有多个调用者同时请求相同的资源,他们会获得相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本给该调用者,而其他的调用者所见到的最初资源仍然保持不变,这个过程对其他调用者时透明的。Redis中的fork()函数实现了CopyOnWrite机制,当redis调用bgsave之后bgsave调用fork函数,此时内存中的数据并不会为了两个进程而复制成两份,而是两个进程中的指针都指向同一个内存地址,如果在快照生成期间Redis中的数据被修改了,操作系统只会把修改的数据复制一份进行修改,而不会将所有的数据进行复制,此时子进程中获取到的数据还是原来的数据,而redis对外提供的服务也能访问最新的数据。

    *说明:CopyOnWrite机制只能用于生成快照的过程持续的时间较短,期间只有少量的数据发生了变化的情况,如果所有的数据都发生了变化就真的将6G数据变成12G。

数据达到了Redis内存的最大限制,如何处理?

  • 置换策略、LRU淘汰策略、LFU淘汰策略。

    1. 置换策略:不淘汰策略和七种淘汰数据策略(在设置过期时间中淘汰的四种种、全部数据中进行淘汰三种)。

    2. LRU算法:就是最近最少使用原则来进行数据的淘汰算法,将数据放入到一个链表中,当链表中的某个元素被访问时,这个元素就被会提到链表的前面,而其他元素默认向后移动,当这个时候向缓存中新增一个元素时也会被增加到链表的头部,那么尾部的最后一个元素就被淘汰。

      说明:LRU的思想就是刚被访问的数据在接下来的时间段里更容易被再次访问,而一段时间内没有被访问的数据,之后也不会被再次访问,但是要将Redis中的全部数据放入到这样一个列表,当redis中的数据被频繁访问时,需要不停的移动链表的位置,这会降低Redis的性能,所以Redis对此进行了优化,会给每个数据记录一个最近访问的时间戳(记录在RedisObject的lru字段中),当数据需要进行淘汰时,Redis就随机筛选出N个数据放入到候选集合中去,然后比较这N个数据中的lru的值,最小的就会被淘汰。当再次需要淘汰数据时就不在是随机筛选,能进入候选集合的数据的lru字段值必须小于候选集合中最小的lru值,然后再次将最小的lru的值的数据进行淘汰。

    3. LFU算法:最近使用最少的数据将被淘汰,在LRU的基础上增加一个次数统计。其步骤就是根据数据的访问次数进行筛选,淘汰访问次数少的数据,如果访问次数相同再根据访问时间进行比较,淘汰访问时间久远的数据。Redis中的实现方式是将lru字段拆成两个部分,前16位还是用来表示时间戳,后8位用来表示数据的访问次数。

      说明:当LFU策略筛选数据时,Redis会在候选集合中根据数据lru字段的后8bit选择访问次数最少的数据进行淘汰。当访问次数相同时再根据lru字段的前16bit值大小,选择访问时间最久远的数据进行淘汰。但是8个bit位,最大只能记录255的值,而Redis中的数据是成千上万的。 对此redis对计数进行了优化,并不是数据被访问一次counter就会被加1,而是当数据被访问一次时,首先用计数器当前的值乘以配置项lfu_log_factor再加1,再取倒数得到一个p值,然后把这个p值和一个取值范围在(0,1)的一个随机数r,进行比大小,只有p值大于r时counter的值才会被加1。计数器的默认初始值为5并不是为0,这样可以避免数据刚进入缓存,就因为访问次数少而被立即淘汰。

Redis中的String和hash如何选取?

  • String对大量字段的对象中某个数据进行获取,需要进行整体的数据获取,并在客户端完成反序化。而hash可以获取指定字段获取数据。在设置过期时间时,不能使用hash结构,如果用了会出现缓存雪崩。

Zset底层实现原理是什么?

  • 有序集合对象的编码可以是ziplist或者skiplist,当元素数量小于128个,且所有member的长度都小于64字节时使用ziplist编码,否则使用skiplist编码。

  • ZipList: Ziplkist编码的Zset使用紧凑压缩列表节点来保存,第一个节点保存member,第二个保存score。Ziplist内的集合元素按score从小到大排序,其实质是一个双向链表。

    Ziplist内存数据结构由5部分构成:

    1. 'zlbytes': 存储一个无符号整数,固定四个字节,用于存储压缩列表所占用的字节。当重新分配内存的时候使用,不需要遍历整个列表来计算内存大小。

    2. 'zltail': 储一个无符号整数,固定四个字节,表示ziplist表中entry在ziplist中的偏移字节数。可以快速找到最后一项而不用遍历整个ziplist,从而可以在ziplist尾端快速地执行push或pop操作。

    3. 'zllen': 压缩列表包含的节点个数,固定两个字节,表示ziplist中数据项entry的个数,由于zllen字段只有16bit,所以可以表达的最大值为2^16-1。

      说明:如果ziplist中数据项个数超过了16bit能表达的最大值ziplist仍然可以表示,如果小于等于216-2(也就是不等于216-1),那么就表示ziplist中数据项的个数,否则也就是等于16bit全为1的情况,那么就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项才能计数出来。

    4. 'entry':表示真正存放数据的数据项,长度不定。每个entry也有它自己的内部结构。

    5. 'zlend':Ziplist最后1个字节,值固定等于255,其是一个结束标记。

posted @ 2021-06-03 20:42  yuexiuping  阅读(45)  评论(0编辑  收藏  举报