进阶之路Redis基础知识一篇就满足

Posted 南橘ryc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶之路Redis基础知识一篇就满足相关的知识,希望对你有一定的参考价值。

导言

大家好,我是练习java两年半时间的南橘,下面是我的微信,需要之前的导图或者想互相交流经验的小伙伴可以一起互相交流哦。

这篇文章的出现,首先要感谢一个人三太子敖丙 ,就是他的文章让我发现,原来Redis的知识如此的多姿多彩。恩恩,他的文章,我是期期都看

这是这篇文章的思维导图,因为用的是免费版的软件,所以有不少水印,需要原版的可以问我要

Redis篇,因为时间和篇幅的原因,并没有一次性写完,于是乎,分成了上下两篇,没有看过上半部分的小伙伴可以去看一下~

一、Redis基础知识

要学习Redis基础知识,首先要知道Redis的五种基础数据结构,我们先从这五种数据结构的使用场景和一些工作中(面试中)容易出现的点来介绍

1、String 字符串类型

是redis中最基本的数据类型,一个key对应一个value

适用情况:

1、缓存:经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。

2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。

3.session:通过redis实现session共享

2 、Hash (哈希)

对于Java中的HashMap,本身是一种KV对结构,如 value={{field1,value1},......fieldN,valueN}},非常好理解

适用情况:

HashMap作为缓存,相比于string更节省空间的维护缓存信息,适合存储如用户信息,视频信息等

底层用字典dict实现

3 、List (链表)

Redis 的链表相当于 Java 语言里面的 LinkedList

适用情况:

1、List在Redis中既可以做队列也可以做栈使用,它的插入和删除操作非常快,时间复杂度为 0(1),但是索引定位很慢,时间 复杂度为 O(n)。

2、可以作为微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。

3、可以实现阻塞队列,左进右出的队列组完成设计

list底层使用quickList(快速链表)实现

在早期的设计中, 当列表对象中元素的长度比较小或者数量比较少的时候,采用ziplist来存储,当列表对象中元素的长度比较大或者数量比较多的时候,则会转而使用双向列表linkedlist来存储。

这两种存储方式都各有优缺点

  • 双向链表linkedlist便于在表的两端进行push和pop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
  • ziplist存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝。

3.2版本更新之后,list的底层实现变成了quickList

quickList是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

4 、Set   集合

Redis 的集合相当于 Java 语言里面的 HashSet ,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL 当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。

适用情况:

1、标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。

2、点赞,或点踩,收藏等,可以放到set中实现

3、可以用来存储在某活动中中奖的用户 ID ,因为有去重功能,可以保证同 一个用户不会中奖两次。

5 、Zset  有序集合

它类似于 Java SortedSet HashMap 的结合体, 方面它是个 set ,保证 了内部 value 的唯性,另方面它可 以给每个 value 赋予一个 score ,代表 这个 value 的排序权重。它的内部实现 用的是一种叫作“跳跃表”的数据 结构。

还是和上期一样,从一位大佬那边借过来一张图片给大家展示,什么是跳跃表

从这张图片,我们可以看出来:跳跃表的底层是一个顺序链表,每隔一个节点有一个上层的指针指向下一个节点,并层层向上递归。这样设计成类似树形的结构,可以使得对于链表的查找可以到达二分查找的时间复杂度。

skiplist他不要求上下两层链表之间个数的严格对应关系,他为每个节点随机出一个层数。比如上图第三个节点的随机出的层数是4,那么就把它插入到四层的空间上,而第四个节点随机出的层数是1,那它就只存在第一层空间上。

  • 当数据较少的时候,zset是由一个 ziplist来实现的,就和list底层之前是一样的

ziplist是由一系列特殊编码的连续内存块组成的顺序存储结构,类似于数组,ziplist在内存中是连续存储的,但是不同于数组,为了节省内存 ziplist的每个元素所占的内存大小可以不同

ziplist将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。

  • 当数据较多的时候,zset是一个由dict 和一个 skiplist来实现的, dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据

除了这五大基础数据结构,Redis还有更加专业的数据结构 HyperLogLog(基数统计的算法)、Geo(地理位置系列)、Pub\Sub(消息队列)、Pipeline(管道)、BloomFiler(布隆过滤器),都在不同的地方有用到,有些我会在下文向大家介绍,还有一些深入的我没有了解这么多,大家可以去敖丙的文章中进一步了解

Pipeline

可以将多次IO往返时间缩减为一次,前提是pipleline执行的指令之间没有因果关系

管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与redis的通信次数来实现降低往返延时时间,而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。

注意:pipeline机制可以优化吞吐量,但无法提供原子性/事务保障

二、进阶知识

1 、Redis实现分布式锁

随着互联网技术的飞速发展,越来越多的单体架构已经转型成了分布式架构,,分布式架构确实能带来性能和效率上的提升,但是也会带来数据一致性的问题。

分布式锁,就是解决分布式架构中数据一致性的专用武器,分布式锁需要满足一下三个方面方可放心使用:

  • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取

  • 避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)

  • 高可用:获取或释放锁的机制必须高可用且性能佳

目前,我所知道的分布式锁大概有三种主流方式实现,分别是zookpeer,redis,还有本地数据库,今天我就介绍一下如何用redis实现分布式锁。

基于Redis实现的锁机制,主要是依赖redis自身的原子操作

setnx争抢锁,再用expire添加过期时间

你没有看错,就是这么简单,如果害怕不妥,比如争抢锁的时候还没有设置过期时间就突然宕机之类的问题,可以直接用jedis等封装好的RedisTemplate把setnx和expire合成一条指令使用。

但是,这样的分布式锁不是绝对安全的

首先,单点故障的问题不可避免 其次,因为使用锁的客户端,和redis服务器,不在一起啊!时间是有延迟的,我们只能依靠redis的TTL命令来查询锁的剩余时间,然后根据这个剩余时间来判断锁是否超时。然而在通常的计算机系统中,很难获取到一个可靠的时间。

  • 系统可能因于时间服务器同步调整时间,
  • 虚拟机可能调整时间,
  • JVM GC可能导致时间停顿

怎么办?

其实如果我们使用的场景不需要那么强的安全性精确性,这样也足够用了,至少我现在的公司够用了(一个排名前列的第三方支付企业)。但是,精益求精是程序员们的本质,RedLock的出现在一定程度上解决了这个问题。

我不是很了解RedLock,只能稍微给大家介绍一下,流程如下:

  1. 客户端获取当前时间,生成一个随机值作为锁的值(目的是更加精确的获得时间)
  2. 依次尝试在所有5个redis上取得同一个锁(使用类似单机redis锁的方法, 使用同样的key和同一个随机值) 获取锁的操作本身需要设定一个比较小的超时时间(如5-50ms), 防止在一个挂掉的redis上浪费太多时间 如果一个redis不可用,要尽快开始尝试下一个
  3. 客户端计算获取锁一共用了多长时间,通过用当前时间减去第1步得到的时间 如果客户端获取了多数redis上的这个锁(3到五个5),并且这时还没有超过锁的超时时间, 这个锁就算是获取成功了
  4. 如果锁获取成功了,有效时间就按锁超时时间-获取锁花费时间算
  5. 如果失败,尝试在所有redis上解除锁 (解除锁的操作是一段lua script,删除一个key如果key的value是第1步生成的随机值)

当然,它也不能解决问题,但是, redis锁只会在比较极端的情况下出错,如果不是需要特别精确,只需要保证绝大多数可靠的时候,可以放心使用redis集群或者redlock。

2、Redis集群

解决了分布式锁的问题,但是还是没有解决各种天灾人祸的问题,所以,这就需要Redis集群出马了

集群同步机制

Redis中有主从机制,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。先讲一下Redis主从同步的流程:

  • 1.第一次同步时,从服务器向主服务器发送一次SYNC命令,主服务器收到之后做一次bgsave、并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点
  • 2.复制节点接收完成后将RDB镜像加载到内存中,加载完成后,再通知主节点
  • 3.后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog

同时,在2.8版本之后,Redis可以自动判断是需要全量同步还是增量同步,效率比较高,增量同步其实就是在完成全量同步后,开始新复制时向主服务器发送PSYNC( )命令 (runid是上次复制的主服务器id,offset是从服务器的复制偏移量),主服务器会根据这个两个参数来决定做哪种同步,判断服务器id是否和本机相同,复制偏移量是否在缓冲区中。

集群的高可用性

  • Redis Sentinal(哨兵模式)集群着眼于高可用,在master宕机时自动将slave提升为master,继续提供服务
  • Redis Cluster集群着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储

关于这些集群的东西一章内容肯定写不完,我会在以后的文章里向大家介绍

3、异步队列

Redis的本职工作是缓存,但是由于它多才多艺,成为队列也不错,有一些阻塞式的API让其有能力做消息队列;另外,做消息队列的其他特性例如FIFO(先入先出)也很容易实现,只需要一个List对象从头取数据,从尾部塞数据即可

  • 在Redis中, 如果让List结构作为队列、rpush生产消息、lpop消费消息、当lpop没有消息的时候,可以当sleep一会再重试,这就相当于生产者消费模式模式了。同时List有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
  • pub\sub主题订阅者模式、可以实现1对N的消息队列,实现生产一次,消费多次。但是,它也有不足之处,如果让pub\sub主题订阅者模式、消费者下线的情况下,消息会丢失、不如直接用MQ

4、延时队列

在Redis中,可以利用 sorted-set 来做延时队列

zadd key score1 value1 score2 value2

  • socre为执行时间,key为队列名,value为数据
  • 消费队列循环从sorted-set根据score获取(zrangebyscore)小于等于当前时间的且score最小的一条数据轮询处理
  • 如果没有取到数据,睡一会再去获取

但是,Redis的延时队列无法返回ACK,所以需要自己实现

5、持久化

Redis有两种持久化的方式,分别是RDB和AOF

RDB做镜像全量持久化、AOF做增量持久化,因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量有效数据丢失,所以需要AOF来配合使用,在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

RDB机制

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。RDB提供了三种机制来触发持久化

  • 1、save触发方式---客户端发起save请求

    执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止

  • 2、bgsave触发方式--客户端发起bgsave请求

    执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求

  • 3、自动触发

    自动触发是由我们的配置文件来完成的,在redis.conf文件中配置,大家可以去了解一下,这里就不写那么多东西了

AOF机制

全量备份总是耗时的(随机的传说总是好的???),有时候我们提供一种更加高效的方式AOF,Redis会将每一个收到的写命令都通过write函数追加到文件中,就是日志记录。

和RDB一样,AOF也有三种同步机制:

  • 1、always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

  • 2、everysec:每秒同步,异步操作,每秒记录 如果一秒内宕机,有数据丢失

  • 3、no:从不同步

Redis本身的机制是AOF持久化开启存在AOF文件时,优先加载AOF文件。AOF文件不存在时候,加载RDB文件。加载AOF\RDB文件后,Redis启动成功。AOF\RDB文件存在错误时,Redis启动失败并打印错误信息。

不要问AOF和RDB用哪个,我的经验就是,全都用。RDB同步快,但是要损失最多五分钟的内容,AOF同步慢,但是每秒同步的情况下最多损失1s的内容,损失的内容也可以通过日志去找回。

机器断电对数据丢失的影响

AOF日志中可以进行sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据,但在高性能要求下每次都sync是不现实的,一般都使用定时sync,比如1s一次,这个时候就最多丢失1s的数据。

  • 1、定时删除
  • 2、惰性删除——查询后如果过期就不返回、过期则删除。这种情况容易出现很多冗余数据导致占用大量空间

1、定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作,默认100ms就随机抽一些key判断是否过期,过期的话就删除,用处理器性能换取存储空间(拿时间换空间)

  • 优点: 节约内存,到时就删除,快速释放掉不必要的内存占用
  • 缺点: CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量

2、惰性删除

当Redis中的数据到了过期时间,我们先不做处理。等下次访问该数据时进行一次判断,如果未过期,就正常返回,如果发现数据已过期,立刻删除,然后返回不存在,用存储空间换取处理器性能(拿空间换时间)

  • 优点: 节约CPU性能,发现必须删除的时候才删除
  • 缺点: 内存压力很大,出现长期占用内存的数据

不知道大家不知道发现了没有,大部分的算法,不是时间换空间,就是空间换时间。刚刚发现这个秘密的我简直惊呆了,这就是人类的终极奥秘之一了,只要我们知道这个诀窍,就能解决大部分的问题。

3、淘汰机制

在Redis的redis.config文件中还可通过搜索maxmemory-policy来设置淘汰机制

noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
allkeys-lru:在主键空间中,优先移除最近未使用的key。
volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random:在主键空间中,随机移除某个key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。


结语


Redis篇写完了,感觉Redis真的是有好多内容啊,之前觉得自己貌似全都掌握了一样,回过头来,发现还是有很多不懂~~~~~很开心能在这里给大家分享我的收获,我知道我的技术栈比起各位响当当的大佬还是有差距,但是人不努力怎么知道自己不可以?希望大家能喜欢我的文章,也希望这篇文章能帮到大家。同时需要思维导图的话,可以联系我,毕竟知识越分享越香!


以上是关于进阶之路Redis基础知识一篇就满足的主要内容,如果未能解决你的问题,请参考以下文章

Redux从入门到进阶,看这一篇就够了!

Redux从入门到进阶,看这一篇就够了!

网络安全工程师从零基础到进阶,看这一篇就够了

js进阶全面理解Event Loop这一篇就够了

超硬核Python避坑学习方案奉上!入门到就业一篇就搞定!

超硬核Python避坑学习方案奉上!入门到就业一篇就搞定!