• Redis数据结构


    Redis基础数据结构

    Redis有五种基础数据结构。分别为

    1、string(字符串)

      Redis的字符串是简单动态字符串SDS(Simple Dynamic String),是可以修改的字符串,内部结构的实现类似于Java的ArrayList,是一个带长度信息的字节数组,采用预分配冗余空间的方式来减少内存的频繁分配;

      实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1MB时,扩容都是加倍现有的空间。如果字符串的长度超过1MB,扩容时一次

      只会多扩1MB的空间。需要注意的是字符串的最大长度为512MB

      计数:如果value是一个整数,还可以对它进行自增操作。不过自增是有范围的,它的范围在signed long的最大值和最小值之间(0-2^63-1),超过这个范

      围,Redis会报错

    2、list(列表)

      Redis中的列表相当于Java语言中的LinkedList,注意它是链表而不是数组。这意味着list的插入和删除很快,时间复杂度为 O(1),但是查询很慢,时间复杂度

      为O(n),但是深入了解的话,Redis底层存储的并不是一个简单的LinkedList,而是称为一个快速链表的结构。在列表元素较少的情况下,会使用一块连续的内

      存存储,这个结构是ziplist,即压缩列表,当数据量比较多的时候才会改成quicklist,因为普通的链表需要的附加指针空间太大,会浪费空间,还会加重内存的

      碎片化,Redis将多个ziplist使用双向指针串起来使用组成了quicklist,既满足了快速的插入删除性能,又不会出现太大的空间冗余。

      Redis的列表常用来做异步队列使用。将需要延后的任务结构体序列化字符串,塞进Redis中的列表,另一个线程从这个列表中轮询数据进行处理。

     

    3、hash(字典)

      Redis的字典相当于Java语言中的HashMap,它是无序字典,内部存储了很多键值对。实际结构和Java中的HashMap也是一样的,都是数组+链表的结构,第一

      维hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。但是Redis的字典的值只能是字符串,另外它们rehash的方式也不同,Java需要一次性rehash,

      Redis为了追求性能,采用了渐进式rehash策略:在rehash时会保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作

      指令中,循序渐进的将旧的hash内容一点点的迁移到新的hash结构中,当全部迁移完成了就会使用的新的hash完全替代。

      hash也有缺点,hash结构的存储消耗要高于单个字符串。hash结构中的单个子key也可以单独进行计数,对应的指令是hincrby,和incr基本一样

    4、set(集合)

      Redis的集合相当于Java语言中的HashSet,它内部的键值对是无序,唯一的,它的内部实现相当于一个特殊的字典,字典中所有的Value都是一个值NULL。

      当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。set结构可以用来存储在某活动中中奖用户的ID,因为有去重功能,可以保证一个

      用户不会中奖两次。

    5、zset(有序列表)

      它类似与Java中的SortedSet和HashMap的结合体,一方面它是一个set,保证了内部value的唯一性,另一方面可以为每个value赋予一个score,代表这个

      value的排序权重。它的内部实现的数据结构是跳跃列表。

      zset可以存储学生的成绩,value是学生的ID,score是他的考试成绩,我们对成绩按分数进行排序就能得到名次。

      跳跃列表

      zset内部的排序功能是通过跳跃列表来实现的。因为zset要支持随机的插入和删除,所以不宜使用数组来表示。

      跳表可以理解为一个加了多级索引的链表。对一个单链表来讲,即使链表中存储的数据是有序的,如果我们要想在

      其中查找某个数据,也只能从头到尾遍历链表,效率很低,时间复杂度是 O(n),如何提高查询效率呢,我们可以

      通过给链表加索引的方式。如:

      

       如果要查找元素16,现在索引层遍历,找到第二索引层中13的结点时,向下到第一层索引,发现其下一个元素是17,大于16,因此通过索引层结点的down

       指针,下降到原始链表这一层,继续遍历,这个时候只需要再遍历2个结点,就可以找到16这个结点了。这样有了索引层之后原本需要遍历10个结点,现在

       只需要遍历6个结点了。链表元素越多,索引层越多查询优势越加明显。所以在跳表中查询任意数据的时间复杂度就是 O(logn),和二分查找的时间复杂度是

         一样的。但是相比于单链表,跳表需要存储多级索引,因此要消耗更多的存储空间。如一个长度为n的链表,第一级索引大约有n/2个结点,第二级大约有n/4

              个结点,每上升 一级结点就减少一半,直到剩下2个结点。因此共有约2+4+...+n/4+n/2是一个等比数列和为n-2(公式a1-an*q/1-q),因此跳表的空间复杂度为O(n).

       要想降低跳表的空间复杂度,可以考虑3个或更多的结点建立一个索引。

       跳表的索引动态更新:当不断的往跳表中插入数据时,如果不更新索引就会出现某2个索引结点之间数据非常多的情况,极端条件下甚至会退化成单链表。因此

          当往跳表中插入数据时,应该选择性的将这个数据插入到部分索引层中,通过随机函数的方式生成值k,将结点添加到第k级索引层中。

          Redis为什么用跳表实现有序列表而不是红黑树?

          Redis中有序集合支持的核心操作主要有下面几个:

      插入数据,删除数据,查找数据,按照区间查找数据(比如查找score在[100-250]的数据),迭代输出有序序列等

      其中,插入,删除,查找以及迭代输出有序序列这几个操作,红黑树也能实现,时间复杂度也是O(logn),但是按照区间来查找数据这个操作,红黑树的效率没有

      跳表高。对于按照区间查找数据这个操作,跳表可以做到O(logn)的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,效率很高。还有一个

      原因就是跳表更容易代码实现。

      容器型数据结构的通用规则

      list、set、hash、zset这四种数据结构是容器型数据结构,有下面两个通用规则

      1、create if not exists:如果容器不存在就创建一个,再进行操作

      2、drop if no elements:如果容器里的元素没有了,那么立即删除容器,释放内存

      过期时间

      Redis所有的数据结构都可以设置过期时间,时间到了之后会执行Redis的删除策略,但是有一个需要特别注意的地方,如果一个字符串已经设置了过期时间,

      然后调用set命令修改了它,那么它的过期时间会消失。

    Redis分布式锁

      分布式应用在逻辑 处理中经常会遇到并发问题。如一个操作要修改用户的状态,需要先读出用户的状态,再在内存中进行修改,改完了再还回去。但是如果有

      多个这样的操作同时进行,就会出现并发问题,,因为读取和修改这两个操作不是原子操作(原子操作是指不会被线程调度机制打断的操作,原子操作一旦开

      始,就会一直运行结束,中间不会有任何线程切换。)

     分布式锁的奥义:

      分布式锁本质上就是在Redis里面占一个坑,当别的线程也要来占坑时,发现已经被占了,只好放弃或者稍后再试。占坑一般使用setnx(set if not exists)指令,

      只允许一个客户端占坑,先来先占,完成操作再调用del命令释放坑。为防止逻辑执行中出现异常,del指令没有被调用,导致锁不能释放,需在拿到锁时设置

      过期时间,这样即使出现异常也能保证在到期之后释放锁。

      redis1.x版本需要用两个指令来获取锁和设置过期时间,分别是setnx和expire,但是如果在setnx和expire之间服务器挂掉了也会导致expire得不到执行,也会

      造成死锁。解决这个问题需要使用lua脚本来使这两个指令变成一个原子操作。

      Redis2.x版本新增了指令 SETEX key seconds value 可以让setnx和expire一起执行。

     超时问题:

      如果在加锁后的逻辑处理执行时间太长,以至于超过了锁的超时机制,就会出现问题,因为这个时候,A线程持有的锁过期了,但A线程的逻辑还未处理

      完,这时候B线程获得了锁,仍然存在并发问题。如果这时A线程执行完成了任务,然后去释放锁,这时释放的就是B线程创建和持有的锁。

      为了避免这个问题:

      1、Redis分布式锁不要用来执行较长时间的任务

      2、加锁的value是个特殊值(如uuid),只有持有锁的线程知道,释放锁前先对比value是否相同,相同的话再释放锁。

        为了防止对比时,释放锁前当前锁超时,其他线程再创建新的锁,需要使获取锁value和释放锁是一个原子操作,用lua脚本来解决。

    消息队列

      Redis可以实现轻量级的消息队列。对于那些只有一组消费者的消息队列,使用Redis就可以轻松搞定。但是Redis不是专业的消息队列,没有ack保证,因此

      对消息可靠性有极高要求的话就不适合使用。

    1、异步消息队列

    Redis的list(列表)数据结构常用来作为异步消息队列使用,使用rpush(右入队)和lpush(左入队)操作入队列,用lpop(左出队)和rpop(右出队)操作出队。

    它支持多个生产者和消费这并发进出消息,每个消费者拿到的消息都是不同的列表元素。

    如果队列空了:pop获取数据的话会不停的pop空轮询,会导致拉高客户端的CPU消耗和QPS导致慢查询增多。因此遇到空队列可让当前线程先sleep 1s。

    阻塞读:让线程休眠1s可导致消息队列的延迟增大,可使用blpop/brpop替代lpop/rpop,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则

        立即醒过来,消息的延迟几乎为0.

    空闲连接:如果线程阻塞时间太长,Redis的客户端就变成了闲置连接,闲置过久,服务器一般会断开连接,减少闲置资源占用,这个时候brpop/blpop会抛出异常,

        所以在编写消费者时要扑获异常,然后重试。

    2、延时队列

      延时队列可以使用Redis的zset(有序列表)来实现。将消息序列化成一个字符串作为zset的value,这个消息的到期处理时间作为score,然后利用多个线程轮询

      zset获取到期的任务进行处理。

     Redis高级数据结构

    位图

      bitmap

      平时的开发中,有些布尔型的数据需要存取,如用户一年的签到记录,签了是1,不签是0.要记录365天,当用户量很大的时候,需要的存储空间是很大的。为了

      解决这个问题,Redis提供了位图数据结构,这样每条数据只占据一个位,如365条记录只占据365个位,46字节。大大节省了存储空间。位图的最小单位是bit。

      每个bit的取值只能是0或者1。位图其实就是byte数组。可以使用get/set直接获取和设置整个位图的内容,也可以使用位图操作getbit/setbit来把byte数组当成位

      数组来处理。

    HyperLogLog

      HyperLogLog用来解决去重的统计问题,提供了不精确的计数方案,标准误差为0.81%。例如统计页面的UV,用set的话如果有千万级的UV,set集合会非常大

      非常浪费空间。这种情况就适合用HyperLogLog。

      HyperLogLog提供了两个指令pfadd和pfcount,pfadd用来增加计数,pfcount用来获取计数。另一个指令pfmerge,用于将多个pf计数值累加在一起形成一个新

      的pf值。

    布隆过滤器

      HyperLogLog虽然能去重,但是不能判断是否包含一个值的情况。这就需要布隆过滤器来判断是否包含某个值了。

      Bloom Filter,专门用来解决大数据量的去重问题。而且比set要节省90%以上的空间,不过稍微有点不精确,有一定的误判概率。当布隆过滤器说某个值存在

      时,可能不存在,当说不存在时就一定不存在。

    用法:两个基本指令,bf.add和bf.exists 添加一个元素和判断一个元素是否存在。 bf.madd和bf.mexists添加多个元素和查询多个元素是否存在。

      通过在add之前bf.reserve指令显式创建,在bf.reserve指令的三个参数中自定义布隆过滤器。这三个参数分别是 key、error_rate(错误率)和initial_size

      error_rate越低,需要的空间越大

      initial_size表示预计放入元素的数量,当实际数量超出这个数值时,误判率会上升,所以需要提前设置一个较大的数值避免超出导致误判率升高。但也不能太

      大,会浪费内存空间。所以在使用之前要尽可能的精确估计元素数量,还要加上一定的冗余空间避免实际的元素比预估高出很多。

    原理:布隆过滤器的数据结构是一个大型的位数组和几个不一样的无偏hash函数。所谓无偏就是能够把元素的hash值算的比较均匀,让元素被hash映射到位数组

      的位置比较随机。  过程:当向布隆过滤器中添加key时,会使用多个hash函数对key进行hash,算得一个整数索引值,然后对位数组长度进行取模运算得到

      一个位置,每个hash函数都会得到一个位置。再把位位置的这几个位置都置为1,完成add操作。 查找key时,用多个hash函数对key进行hash,再根据长度算

      得对应的几个位置,看这几个位置对应的值是否都是1,有一个位置为0,则说明key不存在。都为1也可能是其他的key哈希冲突导致,也可能不存在。所以如

      果数组比较稀疏,则判断正确的概率就很大。

    应用:避免缓存穿透将所有有可能存在的key放进布隆过滤器中,查询时过滤掉一个一定不存在数据的查询,减轻数据库的压力。

    Redis实现限流

    漏斗限流:模块Redis-Cell,用法

    cl.throttle key capacity times seconds 1

    参数:key 是用户+行为

      capacity:漏斗的初始容量,不受限流控制的次数

      times :多少次

      seconds:多少时间内,单位秒

      1:默认值1,表示剩余空间的最小单位

    结果为5个Integer值,从上到下依次表示

      1、0表示允许,1表示禁止

      2、漏斗容量capacity

      3、漏斗剩余空间

      4、如果被拒绝了需要多长时间进行重试,单位秒,没决绝的话值为-1

      5、多长时间后漏斗完全空出来

    地理位置Geo模块

      可以用来查找附近的单车,餐馆类似功能。地理元素的位置使用二维的经纬度表示,京都范围 [-180,180],纬度范围 [-90,90],纬度正负以赤道为界,北正南负。

      经度正负以本初子午线(英国格林尼治天文台)为界,东正西负。

      业界比较通用的地理位置距离排序算法是GeoHash算法,Redis也使用GeoHash算法。GeoHash算法将二维的经纬度数据映射到一维的整数,这样所有的元素

      都挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。如我们要计算附近的人时,首先将目标位置映射到这条线上,然后在这个一

      维的 线上获取附近的点就行了。

    scan

      Redis提供了一个简单粗暴的keys指令用来列出所有满足特定正则字符串规则的key。使用时只需要再keys 后面提供一个简单的正则匹配字符串即可。但是有两

      个致命缺点:

      1、没有offset、limit参数,会一次性列出所有符合条件的key,即使有成千上万个key,也会一一列出

      2、keys算法是遍历算法,时间复杂度为O(n),如果符合条件的key有千万级别以上就会导致Redis服务卡顿,由于Redis是单线程操作,所有读写Redis的其他

        指令都会延后甚至会超时报错。

      scan指令:2.8版本提供

      1、复杂度为O(n),但它是通过游标分步进行的,不会阻塞线程

      2、提供limit参数,可以控制每次返回结果的最大条数,limit只是一个hint,返回的结果可多可少

      3、同keys一样,也提供模式匹配功能

      4、服务器不需要为游标保持状态,游标唯一的状态就是scan返回给客户端的游标整数

      5、返回的结果可能会有重复,需要客户端去重

      6、遍历的过程如果有数据修改,改动后的数据能不能遍历是不确定的

      7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为0

      在Redis中所有的key都存储在一个很大的字典中,这个字典和Java中的HashMap一样。

  • 相关阅读:
    Linux cat命令详解
    Linux终端中文显示乱码
    Linux命令对应的英文全称【转载】
    Linux常用命令学习
    链接Linux工具(SecureCRT)
    Linux下四款常见远程工具比较
    怎么让mysql的id从0开始
    substr 字符串截取
    C#学习笔记(三)——流程控制
    C#学习笔记(二)——变量和表达式
  • 原文地址:https://www.cnblogs.com/yangyongjie/p/10821054.html
Copyright © 2020-2023  润新知