Redis- 数据结构

一. 数据类型

1. 字符串(strings)

  • string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。其他的几种数据结构都是在string类型的基础上构建的
  • 字符串类型的值实际可以使字符串(json,html…)、数字、二进制(图片、音频、视频),但是string 类型的值最大能存储 512MB。这点要注意!
  • 字符串命令: https://www.runoob.com/redis/redis-strings.html

1.1 string扩展

  • 原子计数
    • 由于INCR等指令本身就具有原子操作的特性,所以我们完全可以利用redis的INCR、INCRBY、DECR、DECRBY等指令来实现原子计数
    • 在遇到数值操作时,redis会将字符串类型转换成数值,很多存储系统和编程语言内部会使用CAS机制实现计数功能,会有一定的CPU开销,但再Redis中不会存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行
  • 字符串操作命令:set key value <[ex seconds] | [px milliseconds] | [nx|xx]>
    • ex seconds:为键设置毫秒级过期值
    • px milliseconds:为键设置毫秒级过期时间
    • nx:键必须不存在,才可以设置成功,用于添加
    • xx:与nx相反,键必须存在才可以设置成功,用于更新
    • setex | setnx 这两种方式和ex、nx的功能一致
    • 尤为注意setnx可以作为分布式锁的一种实现方案,后面会详细展开描述,这个很关键,基本已经很少有人会使用zookeeper来实现分布式锁了,因为redis更加的高效
  • 批量操作命令mget [key ...],批量操作命令可以有效的提高开发效率
    • 没有使用mget这样的命令,执行n次get命令的时间为n*(网络传输时间 + 命令执行时间)
    • 使用mget批量操作命令后时间为n*命令执行时间 + 1*网络传输时间
    • 由于redis服务端本身的处理能力就很强大,所以对于开发人员,网络可能会成为性能的瓶颈,这里尤为注意
    • 当然批量操作也不是毫无弊端,每次批量操作发送获取的key的数量不是无节制的,数量过多也可能是造成Redis阻塞或网络的阻塞

2. 列表(lists)

  • list用来存储多个有序可重复字符串。列表中的每个字符串称为元素,一个列表最多可以存储2^32-1个元素。
  • redis中的lists在底层实现上是链表,也就是说对于一个具有上百万个元素的lists来说,在头部和尾部插入一个新元素,其时间复杂度是常数级别的
  • list可以充当栈,队列的角色,在实际开发上有很多应用场景。
  • 列表命令: http://www.runoob.com/redis/redis-lists.html

3. 集合(sets)

  • Redis的Set是string类型的无序集合,集合内元素的唯一性,第二次插入的元素将被忽略。
  • 最多可以存储 2^31 -1 个元素,支持集合取交集,并集,差集等操作
  • 集合命令: https://www.runoob.com/redis/redis-sets.html
  • 内部编码
    • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512)时,Redis选择intset的方式作为集合内部实现,减少内存使用
    • hashtable(哈希表)

4. 有序集合(sorted sets)

  • 有序集合的成员是唯一的,但分数(score)却可以重复。
  • 在集合类型的基础上有序集合类型为集合中的每个元素都关联一个分数,这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作。
  • 有序集合中的每个元素都关联一个序号(score),这便是排序的依据。
  • 很多时候,都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的,比如zrange、zadd、zrevrange、zrangebyscore等等
  • 有序集合命令: https://www.runoob.com/redis/redis-sorted-sets.html

4.1 列表和有序集合比较

  • 在某些方面有序集合和列表类型有些相似。
    • 二者都是有序的。
    • 二者都可以获得某一范围的元素。
  • 二者有着很大区别:
    • 列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。
    • 有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快。
    • 列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)
    • 有序集合要比列表类型更耗内存。

4.2 集合比较

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mXhejMD-1604208029701)(http://oowanghan.cn/usr/uploads/redis/redis10.jpg)]

5. 哈希(hashes)

  • 哈希是从redis-2.0.0版本之后才有的数据结构。哈希类型是指键值本身又是一个键值对的结构,如 value={filed1:val1, filed2:val2}
  • Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象
  • 哈希命令: https://www.runoob.com/redis/redis-hashes.html

5.1 哈希(hashes)扩展

  • hgetall使用可能会发生阻塞
    • 当redis中某个hash类型的value存储的哈希元素个数比较多,会存在Redis被阻塞的可能性。
    • 如果只是获取部分数据,推荐使用hmget
    • 如果要获取全部数据,建议使用hscan命令,渐进式遍历hash类型。

6. 扩展数据类型

6.1 bitmap
6.2 HyperLogLog
  • Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
  • Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
  • 因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
  • 相关命令: https://www.runoob.com/redis/redis-hyperloglog.html
  • HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。虽然误差率不算大,但是,如果你需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型。
6.3 GEO数据类型
  • 基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中,我们来看一下它的底层结构。
  • 操作:https://www.runoob.com/redis/redis-geo.html
  • GEO的底层结构是GeoHash,GeoHash用一个字符串表示经度和纬度两个坐标。
  • GeoHash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。
  • GeoHash编码
    • 基本原理就是二分区间,区间编码,当我们要对一组经纬度进行 GeoHash 编码时,我们要先对经度和纬度分别编码,然后再把经纬度各自的编码组合成一个最终编码。
    • 对于一个地理位置信息来说,它的经度范围是[-180,180]。GeoHash 编码会把一个经度值编码成一个 N 位的二进制值,我们来对经度范围[-180,180]做 N 次的二分区操作,其中 N 可以自定义。
    • 在进行第一次二分区时,经度范围[-180,180]会被分成两个子区间:[-180,0) 和[0,180](我称之为左、右分区)。此时,我们可以查看一下要编码的经度值落在了左分区还是右分区。
    • 如果是落在左分区,我们就用 0 表示;如果落在右分区,就用 1 表示。
    • 这样一来,每做完一次二分区,我们就可以得到 1 位编码值。
    • 然后,我们再对经度值所属的分区再做一次二分区,同时再次查看经度值落在了二分区后的左分区还是右分区,按照刚才的规则再做 1 位编码。当做完 N 次的二分区后,经度值就可以用一个 N bit 的数来表示了。
  • 举例
    • 假设我们要编码的经度值是 116.37,我们用 5 位编码值(也就是 N=5,做 5 次分区)。
    • 第一次二分区操作:把经度区间[-180,180]分成了左分区[-180,0) 和右分区[0,180],此时,经度值 116.37 是属于右分区[0,180],所以,我们用 1 表示第一次二分区后的编码值。
    • 第二次二分区操作:把经度值 116.37 所属的[0,180]区间,分成[0,90) 和[90, 180]。此时,经度值 116.37 还是属于右分区[90,180],所以,第二次分区后的编码值仍然为 1。
    • 第三次二分区操作:把[90,180]进行二分区,经度值 116.37 落在了分区后的左分区[90, 135) 中,所以,第三次分区后的编码值就是 0。按照这种方法,做完 5 次分区后,我们把经度值 116.37 定位在[112.5, 123.75]这个区间,并且得到了经度值的 5 位编码值,即 11010。这个编码过程如下表所示:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfWbNuml-1604208029704)(http://oowanghan.cn/usr/uploads/redis/redis11.jpg)]
    • 对纬度的编码方式,和对经度的一样,只是纬度的范围是[-90,90],下面这张表显示了对纬度值 39.86 的编码过程。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQNjyMOs-1604208029705)(http://oowanghan.cn/usr/uploads/redis/redis12.jpg)]
    • 当一组经纬度值都编完码后,我们再把它们的各自编码值组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。
    • 刚刚计算的经纬度(116.37,39.86)的各自编码值是 11010 和 10111,组合之后就能得到最终编码值 1110011101,如下图所示:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZqINhIDw-1604208029707)(http://oowanghan.cn/usr/uploads/redis/redis13.jpg)]
  • GeoHash原理
    • 用了 GeoHash 编码后,原来无法用一个权重分数表示的一组经纬度(116.37,39.86)就可以用 1110011101 这一个值来表示,就可以保存为 Sorted Set 的权重分数了。当然,使用 GeoHash 编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了 GeoHash 中的一个分区。
    • 为何要如此编码,可以将经度[-180,180]和纬度[-90,90]都做一次二分区,得到如下4个分区
      • 分区一:[-180,0) 和 [-90,0),编码 00
      • 分区二:[-180,0) 和 [0,90],编码 01
      • 分区三:[0,180] 和 [-90,0),编码 10
      • 分区四:[0,180] 和 [0,90],编码 11
    • 这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的 GeoHash 编码值基本也是接近的,如下图所示:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CvpAkTUJ-1604208029709)(http://oowanghan.cn/usr/uploads/redis/redis14.jpg)]
    • 当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。
    • 这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crZxFiv0-1604208029710)(http://oowanghan.cn/usr/uploads/redis/redis15.jpg)]
    • 为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。注意是在一维上面查找周围的4个或8个,这样就可以规避Peano空间的突变性所带来的误差
6.4 自定义数据类型
  • RedisObject 结构借助*ptr指针,就可以指向不同的数据类型,例如,ptr指向一个 SDS 或一个跳表,就表示键值对中的值是 String 类型或 Sorted Set 类型。所以,我们在定义了新的数据类型后,也只要在 RedisObject 中设置好新类型的 type 和 encoding,再用ptr指向新类型的实现,就行了。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTfLbg7Z-1604208029711)(http://oowanghan.cn/usr/uploads/redis/redis16.jpg)]
  • 开发一个新的数据类型步骤(NewTypeObject)
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8ueUt9P-1604208029711)(http://oowanghan.cn/usr/uploads/redis/redis17.jpg)]
    • 第一步:定义新数据类型的底层结构
      • 用 newtype.h 文件来保存这个新类型的定义,具体定义的代码如下所示:
        struct NewTypeObject {
            struct NewTypeNode *head; 
            size_t len; 
        }NewTypeObject;
        
      • 其中,NewTypeNode 结构就是我们自定义的新类型的底层结构。我们为底层结构设计两个成员变量:一个是 Long 类型的 value 值,用来保存实际数据;一个是*next指针,指向下一个 NewTypeNode 结构。
        struct NewTypeNode {
            long value;
            struct NewTypeNode *next;
        };
        
    • 第二步:在 RedisObject 的 type 属性中,增加这个新类型的定义
      • 这个定义是在 Redis 的 server.h 文件中。比如,我们增加一个叫作 OBJ_NEWTYPE 的宏定义,用来在代码中指代 NewTypeObject 这个新类型。
        #define OBJ_STRING 0    /* String object. */
        #define OBJ_LIST 1      /* List object. */
        #define OBJ_SET 2       /* Set object. */
        #define OBJ_ZSET 3      /* Sorted set object. */
        …
        #define OBJ_NEWTYPE 7
        
    • 第三步:开发新类型的创建和释放函数
      • Redis 把数据类型的创建和释放函数都定义在了 object.c 文件中。所以,我们可以在这个文件中增加 NewTypeObject 的创建函数 createNewTypeObject,如下所示:
        robj *createNewTypeObject(void){
            NewTypeObject *h = newtypeNew(); 
            robj *o = createObject(OBJ_NEWTYPE,h);
            return o;
        }
        
      • createNewTypeObject 分别调用了 newtypeNewcreateObject 两个函数。
      • newtypeNew 函数。它是用来为新数据类型初始化内存结构的。这个初始化过程主要是用 zmalloc 做底层结构分配空间,以便写入数据。newtypeNew 函数涉及到新数据类型的具体创建,而 Redis 默认会为每个数据类型定义一个单独文件,实现这个类型的创建和命令操作,例如,t_string.c 和 t_list.c 分别对应 String 和 List 类型。按照 Redis 的惯例,我们就把 newtypeNew 函数定义在名为 t_newtype.c 的文件中。
        NewTypeObject *newtypeNew(void){
            NewTypeObject *n = zmalloc(sizeof(*n));
            n->head = NULL;
            n->len = 0;
            return n;
        }
        
      • createObject 是 Redis 本身提供的 RedisObject 创建函数,它的参数是数据类型的 type 和指向数据类型实现的指针*ptr。createObject 函数中需要传入了两个参数,分别是新类型的 type 值 OBJ_NEWTYPE,以及指向一个初始化过的 NewTypeObjec 的指针。这样一来,创建的 RedisObject 就能指向我们自定义的新数据类型了。
        robj *createObject(int type, void *ptr) {
            robj *o = zmalloc(sizeof(*o));
            o->type = type;
            o->ptr = ptr;
            ...
            return o;
        }
        
      • 对于释放函数来说,它是创建函数的反过程,是用 zfree 命令把新结构的内存空间释放掉。
    • 第四步:开发新类型的命令操作
      • 增加相应的命令操作的过程可以分成三小步:
      1. 在 t_newtype.c 文件中增加命令操作的实现。比如说,我们定义 ntinsertCommand 函数,由它实现对 NewTypeObject 单向链表的插入操作:
        void ntinsertCommand(client *c){
        //基于客户端传递的参数,实现在NewTypeObject链表头插入元素
        }
        
      2. 在 server.h 文件中,声明我们已经实现的命令,以便在 server.c 文件引用这个命令,例如:
        void ntinsertCommand(client *c)
        
      3. 在 server.c 文件中的 redisCommandTable 里面,把新增命令和实现函数关联起来。例如,新增的 ntinsert 命令由 ntinsertCommand 函数实现,我们就可以用 ntinsert 命令给 NewTypeObject 数据类型插入元素了。
        struct redisCommand redisCommandTable[] = { 
        ...
        {"ntinsert",ntinsertCommand,2,"m",...}
        }
        
    • 这样就完成了一个自定义的 NewTypeObject 数据类型,可以实现基本的命令操作了。当然,如果还希望新的数据类型能被持久化保存,还需要在 Redis 的 RDB 和 AOF 模块中增加对新数据类型进行持久化保存的代码

6.5. 消息队列

二. 应用场景

1. 字符串(strings)

  • 缓存功能
    • Redis作为缓存层,MySQL作为存储层,绝大多数请求数据都是从Redis中获取,降低MySQL压力
    • 缓存层的设计key也是需要注意的,不要使用过大的key值,造成内存浪费。同时key的命名规范也要注意,推荐使用:业务名:对象名:id:[属性]作为键名
    • redis在使用缓存的时候也要注意缓存一致性的问题,还有缓存击穿,穿透,雪崩等情况,后续会详细讲解这方面,这里只是暂时做一下记录
  • 计数
    • 快速计数,原子性操作。。。
  • 共享session
    • 分布式web服务(例如微服务架构)会将用户的登录信息使用redis做集中式管理。来保证每个服务节点都能使用用户的登录信息。这种情况只要保证redis的高可用和扩展性即可,也就是redis的集群分片,副本机制以及哨兵机制
  • 限速
    • 比如一个网站限制一个IP地址不能一秒钟之内访问超过n次可以使用redis的计数功能来实现
  • string还有很多应用场景,在不同的业务场景下应用丰富,主要是理解字符串使用的思路与方式

2. 字符串列表(lists)

  • 消息队列
    • Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个客户端通过brpop命令获取数据
  • 文章列表
    • 列表不但有序,同时支持按照索引范围获取数据
    • 每篇文章使用hash存储结构,还有一个用户列表的缓存保存用户的文章列表。可以使用title,timestamp,content三个属性
    • 如果在获取文章列表时数据量过大,可以使用Redis3.2提供的quicklist,结合了ziplist, linkedlist,所以获取数据时也比较高效

3. 字符串集合(sets)

  • 用户标签
    • 以用户id为key,标签作为set集合,这样也可以通过交集等操作查找出用户的共同点,等等
  • 抽奖,生成随机数
    • spop可以随机弹出一个元素,但是会删除该元素
    • srandmember可以随机返回一个元素,但是不会删除该元素
  • 用户增量,留存统计
    • 集合的交集,并集,差集等操作来实现上述业务统计
    • sinter(交集),sunion(并集), sdiff(差集)
    • Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了

4. 有序字符串集合(sorted sets)

  • 展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,优先考虑使用 Sorted Set

5. 哈希(hashes)

  • 主要适用于一些对象的存储,例如用户信息存储
    • 哈希类型是稀疏的,在一些范围查询,排序查询等复杂查询比较麻烦

6. bitmap(字节操作)

  • 二值状态统计,签到,打卡方面的统计
  • 二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录签到(1)或未签到(0)

7. hyperloglog

  • 统计网页的 UV
  • 网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中,Set 类型默认支持去重,如果 page 非常火爆,UV 达到了千万,这个时候,一个 Set 就要记录千万个用户 ID。对于一个搞大促的电商网站而言,这样的页面可能有成千上万个,如果每个页面都用这样的一个 Set,就会消耗很大的内存空间。

8. 统计应用场景注意事项

  • 如果是在集群模式使用多个key聚合计算的命令,一定要注意,因为这些key可能分布在不同的实例上,多个实例之间是无法做聚合运算的,这样操作可能会直接报错或者得到的结果是错误的!
  • 当数据量非常大时,使用这些统计命令,因为复杂度较高,可能会有阻塞Redis的风险,建议把这些统计数据与在线业务数据拆分开,实例单独部署,防止在做统计操作时影响到在线业务。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值