Redis数据类型

2022/7/12 2:22:41

本文主要是介绍Redis数据类型,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Redis提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。

随着Redis版本的更新,后面又支持了四种数据类型:BitMap(2.2版新增)、HyperLogLog(2.8版新增)、GEO(3.2版新增)、Stream(5.0版新增)。

一、Redis核心对象

在Redis中有一个核心的对象叫做redisObject,是用来表示所有的key和value的,用redisObject结构体来表示String、Hash、List、Set、ZSet五种数据类型。

redisObject的源代码在redis.h中,使用c语言写的,表示redisObject的结构如下所示:

在redisObject中type表示属于哪种数据类型,encoding表示该数据的存储方式,也就是底层的实现的该数据类型的数据结构。那么encoding中的存储类型又分别表示什么意思呢?具体数据类型所表示的含义,如下图所示:

这张图只是让你找到每种中数据结构对应的储存类型有哪些,举一个简单的例子,在Redis中设置一个字符串key 234,然后查看这个字符串的存储类型就会看到为int类型,非整数型的使用的是embstr储存类型,具体操作如下图所示:

127.0.0.1:6379> set key 234
OK
127.0.0.1:6379> object encoding key
"int"
127.0.0.1:6379> set k2 3.200
OK
127.0.0.1:6379> object encoding k2
"embstr"

二、String

String是Redis最基本的数据类型,这是最简单的类型,就是普通的set和get,做简单的KV缓存。一个键最大能存储512M。

内部的实现是通过SDS(Simple Dynamic String)来存储的。SDS类似于Java中的ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。Redis是用c语言开发的。但是Redis中的字符串和c语言中的字符串类型却是有明显的区别。

2.1 存储方式

String类型的底层的数据结构实现主要是int和SDS(简单动态字符串)。内部编码有三种int、raw、embstr

int

Redis中规定假如存储的是整数型值,比如set num 123这样的类型,就会使用int的存储方式进行存储,在redisObject的「ptr属性」中就会保存该值。

SDS

存储的字符串对象是一个字符串值就会使用SDS方式进行存储。

SDS称为简单动态字符串,对于SDS中的定义在Redis的源码中有的三个属性int len、int free、char buf[]。len保存了字符串的长度,free表示buf数组中未使用的字节数量,buf数组则是保存字符串的每一个字符元素。

  • 如果存储的字符串值长度大于32个字节,对象的编码(encoding)设置为raw。

  • 如果字符串长度小于等于32个字节就会将对象的编码(encoding)改为embstr来保存字符串。

raw和embstr区别

  • embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,
  • raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS。

embstr优缺点

  • 好处:
  1. embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次;
  2. 释放embstr编码的字符串对象同样只需要调用一次内存释放函数;
  3. 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU缓存提升性能。

缺点:

  1. 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

SDS与c语言字符串对比

Redis使用SDS作为存储字符串的类型肯定是有自己的优势,SDS与c语言的字符串相比,SDS对c语言的字符串做了自己的设计和优化,具体优势有以下几点:

(1)c语言中的字符串并不会记录自己的长度,因此每次获取字符串的长度都会遍历得到,时间的复杂度是O(n),而Redis中获取字符串只要读取len的值就可,时间复杂度变为O(1)。
(2)c语言中两个字符串拼接,若是没有分配足够长度的内存空间就会出现缓冲区溢出的情况;而SDS会先根据len属性判断空间是否满足要求,若是空间不够,就会进行相应的空间扩展,所以不会出现缓冲区溢出的情况
(3)SDS还提供空间预分配惰性空间释放两种策略。在为字符串分配空间时,分配的空间比实际要多,这样就能减少连续的执行字符串增长带来内存重新分配的次数。当字符串被缩短的时候,SDS也不会立即回收不适用的空间,而是通过free属性将不使用的空间记录下来,等后面使用的时候再释放。具体的空间预分配原则是:当修改字符串后的长度len小于1MB,就会预分配和len一样长度的空间,即len=free;若是len大于1MB,free分配的空间大小就为1MB
(4)SDS是二进制安全的,除了可以储存字符串以外还可以储存二进制文件(如图片、音频,视频等文件的二进制数据);而c语言中的字符串是以空字符串作为结束符,一些图片中含有结束符,因此不是二进制安全的。

为了方便易懂,做了一个c语言的字符串和SDS进行对比的表格,如下所示:

c语言字符串 SDS
获取长度的时间复杂度为O(n) 获取长度的时间复杂度为O(1)
不是二进制安全的 是二进制安全的
只能保存字符串 还可以保存二进制数据
n次增长字符串必然会带来n次的内存分配 n次增长字符串内存分配的次数<=n

2.2 应用场景

说到这里我相信很多人可以说已经精通Redis的String类型了,但是纯理论的精通,理论还是得应用实践,上面说到String可以用来存储图片,现在就以图片存储作为案例实现。

(1)首先要把上传得图片进行编码,这里写了一个工具类把图片处理成了Base64得编码形式,具体得实现代码如下:

/**
 * 将图片内容处理成Base64编码格式
 * @param file
 * @return
 */
public static String encodeImg(MultipartFile file) {
    byte[] imgBytes = null;
    try {
        imgBytes = file.getBytes();
    } catch (IOException e) {
        e.printStackTrace();
    }
    BASE64Encoder encoder = new BASE64Encoder();
    return imgBytes == null ? null : encoder.encode(imgBytes);
}

(2)第二步就是把处理后的图片字符串格式存储进Redis中,实现的代码如下所示:

/**
 * Redis存储图片
 * @param file
 * @return
 */
public void uploadImageServiceImpl(MultipartFile image) {
    String imgId = UUID.randomUUID().toString();
    String imgStr= ImageUtils.encodeImg(image);
    redisUtils.set(imgId , imgStr);
    // 后续操作可以把imgId存进数据库对应的字段
    // 如果需要从redis中取出,只要获取到这个字段后从redis中取出即可。
}

其他String的实际应用场景有:

  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
  • 分布式锁:SET命令有个NX参数可以实现「key不存在才插入」,可以用它来实现分布式锁。

三、Hash

Hash是一个键值对(key-value)集合,其中value的形式入:value=[{field1,value1},...{fieldN,valueN}]。Hash特别适合用于存储对象。

Hash与String对象的区别如下图所示:

内部实现

Hash对象的实现方式有两种分别是压缩列表ziplist和哈希表hashtable,其中hashtable的存储方式key是String类型的,value也是以键值对(key-value)的形式进行存储。

  • 如果哈希类型元素个数小于512个(默认值,可由hash-max-ziplist-entries配置),所有值小于64字节(默认值,可由hash-max-ziplist-value配置)的话,Redis会使用压缩列表作为Hash类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis会使用哈希表作为Hash类型的底层数据结构。

字典类型的底层就是hashtable实现的,明白了字典的底层实现原理也就是明白了hashtable的实现原理,hashtable的实现原理可以与HashMap的是底层原理相似。

哈希表hashtable

两者在新增时都会通过key计算出数组下标,不同的是计算法方式不同,HashMap中是以hash函数的方式,而hashtable中计算出hash值后,还要通过sizemask属性和哈希值再次得到数组下标。

我们知道hash表最大的问题就是hash冲突,为了解决hash冲突,假如hashtable中不同的key通过计算得到同一个index,就会形成单向链表(链地址法),如下图所示:

rehash

在字典的底层实现中,value对象以每一个dictEntry的对象进行存储,当hash表中的存放的键值对不断的增加或者减少时,需要对hash表进行一个扩展或者收缩。

这里就会和HashMap一样也会就进行rehash操作,进行重新散列排布。从上图中可以看到有ht[0]ht[1]两个对象,先来看看对象中的属性是干嘛用的。

在hash表结构定义中有四个属性分别是dictEntry table、unsigned long size、unsigned long sizemask、unsigned long used,分别表示的含义就是哈希表数组、hash表大小、用于计算索引值,总是等于size-1、hash表中已有的节点数

ht[0]是用来最开始存储数据的,当要进行扩展或者收缩时,ht[0]的大小就决定了ht[1]的大小,ht[0]中的所有的键值对就会重新散列到ht[1]中。

扩展操作:ht[1]扩展的大小是比当前ht[0].used值的二倍大的第一个2的整数幂;收缩操作:ht[0].used的第一个大于等于的2的整数幂。

当ht[0]上的所有的键值对都rehash到ht[1]中,会重新计算所有的数组下标值,当数据迁移完后ht[0]就会被释放,然后将ht[1]改为ht[0],并新创建ht[1],为下一次的扩展和收缩做准备。

渐进式rehash

假如在rehash的过程中数据量非常大,Redis不是一次性把全部数据rehash成功,这样会导致Redis对外服务停止,Redis内部为了处理这种情况采用渐进式的rehash。Redis将所有的rehash的操作分成多步进行,直到都rehash完成,具体的实现与对象中的rehashindex属性相关,若是rehashindex表示为-1表示没有rehash操作。当rehash操作开始时会将该值改成0,在渐进式rehash的过程更新、删除、查询会在ht[0]和ht[1]中都进行,比如更新一个值先更新ht[0],然后再更新ht[1]。而新增操作直接就新增到ht[1]表中,ht[0]不会新增任何的数据,这样保证ht[0]只减不增,直到最后的某一个时刻变成空表,这样rehash操作完成。上面就是字典的底层hashtable的实现原理,说完了hashtable的实现原理,我们再来看看Hash数据结构的两一种存储方式ziplist(压缩列表)

压缩列表

ziplist(压缩列表)是一组连续内存块组成的顺序的数据结构,压缩列表能够节省空间,压缩列表中使用多个节点来存储数据。压缩列表是列表键和哈希键底层实现的原理之一,压缩列表并不是以某种压缩算法进行压缩存储数据,而是它表示一组连续的内存空间的使用,节省空间,压缩列表的内存结构图如下:

压缩列表中每一个节点表示的含义如下所示:

  1. zlbytes:4个字节的大小,记录压缩列表占用内存的字节数。
  2. zltail:4个字节大小,记录表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。
  3. zllen:2个字节的大小,记录压缩列表中的节点数。
  4. entry:表示列表中的每一个节点。
  5. zlend:表示压缩列表的特殊结束符号'0xFF'

再压缩列表中每一个entry节点又有三部分组成,包括previous_entry_ength、encoding、content

  1. previous_entry_ength表示前一个节点entry的长度,可用于计算前一个节点的其实地址,因为他们的地址是连续的。
  2. encoding:这里保存的是content的内容类型和长度。
  3. content:content保存的是每一个节点的内容。

在Redis 7.0中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了。

应用场景

哈希表相对于String类型存储信息更加直观,存储更加方便,经常会用来做用户数据的管理,存储用户的信息。hash也可以用作高并发场景下使用Redis生成唯一的id。下面我们就以这两种场景用作案例编码实现。

存储用户数据

第一个场景比如我们要储存用户信息,一般使用用户的ID作为key值,保持唯一性,用户的其他信息(地址、年龄、生日、电话号码等)作为value值存储。

若是传统的实现就是将用户的信息封装成为一个对象,通过序列化存储数据,当需要获取用户信息的时候,就会通过反序列化得到用户信息。

但是这样必然会造成序列化和反序列化的性能的开销,并且若是只修改其中的一个属性值,就需要把整个对象序列化出来,操作的动作太大,造成不必要的性能开销。

若是使用Redis的hash来存储用户数据,就会将原来的value值又看成了一个kv形式的存储容器,这样就不会带来序列化的性能开销的问题。

分布式生成唯一ID

第二个场景就是生成分布式的唯一ID,这个场景下就是把redis封装成了一个工具类进行实现,实现的代码如下:

// offset表示的是id的递增梯度值
public Long getId(String key,String hashKey,Long offset) throws BusinessException {
    try {
        if (null == offset) {
            offset=1L;
        }
        // 生成唯一id
        return redisUtil.increment(key, hashKey, offset);
    } catch (Exception e) {
        //若是出现异常就是用uuid来生成唯一的id值
        int randNo=UUID.randomUUID().toString().hashCode();
        if (randNo < 0) {
            randNo=-randNo;
        }
        return Long.valueOf(String.format("%16d", randNo));
    }
}

购物车

以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素。

四、List

List列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向List列表添加元素。

列表的最大长度为2^32 - 1,也即每个列表支持超过40亿个元素。

底层实现

Redis中的列表在3.2之前的版本是使用ziplistlinkedlist进行实现的。

  • 如果列表的元素个数小于512个(默认值,可由list-max-ziplist-entries配置),列表每个元素的值都小于64字节(默认值,可由list-max-ziplist-value配置),Redis会使用压缩列表作为List类型的底层数据结构;

  • 如果列表的元素不满足上面的条件,Redis会使用双向链表作为List类型的底层数据结构;

在3.2之后的版本就是引入了quicklist。linkedlist是一个双向链表,普通的链表一样都是由指向前后节点的指针。插入、修改、更新的时间复杂度尾O(1),但是查询的时间复杂度确实O(n)。linkedlist和quicklist的底层实现是采用链表进行实现,在c语言中并没有内置的链表这种数据结构,Redis实现了自己的链表结构。

Redis中链表的特性:

  1. 每一个节点都有指向前一个节点和后一个节点的指针。
  2. 头节点和尾节点的prev和next指针指向为null,所以链表是无环的。
  3. 链表有自己长度的信息,获取长度的时间复杂度为O(1)。

应用场景

  • 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
  • 文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。

五、Set

Redis中列表和集合都可以用来存储字符串,但是Set是不可重复的集合,而List列表可以存储相同的字符串,Set集合是无序的这个和后面讲的ZSet有序集合相对。

Set类型和List类型的区别如下:

  • List可以存储重复元素,Set只能存储非重复元素;
  • List是按照元素的先后顺序存储元素的,而Set则是无序方式存储元素的。

底层实现

Set的底层实现是ht(哈希表)和intset(整数集合)

  • 如果集合中的元素都是整数且元素个数小于512(默认值,set-maxintset-entries配置)个,Redis会使用整数集合作为Set类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则Redis使用哈希表作为Set类型的底层数据结构。

ht前面已经详细了解过,下面我们来看看inset类型的存储结构。

inset也叫做整数集合,用于保存整数值的数据结构类型,它可以保存int16_tint32_t或者int64_t的整数值。在整数集合中,有三个属性值encoding、length、contents[],分别表示编码方式、整数集合的长度、以及元素内容,length就是记录contents里面的大小。

在整数集合新增元素的时候,若是超出了原集合的长度大小,就会对集合进行升级,具体的升级过程如下:

  1. 首先扩展底层数组的大小,并且数组的类型为新元素的类型。
  2. 然后将原来的数组中的元素转为新元素的类型,并放到扩展后数组对应的位置。
  3. 整数集合升级后就不会再降级,编码会一直保持升级后的状态。

应用场景

Set集合的应用场景可以用来去重、点赞、抽奖、共同好友、二度好友等业务类型。接下来模拟一个添加好友的案例实现:

@RequestMapping(value = "/addFriend", method = RequestMethod.POST)
public Long addFriend(User user, String friend) {
    String currentKey = null;
    // 判断是否是当前用户的好友
    if (AppContext.getCurrentUser().getId().equals(user.getId)) {
        currentKey = user.getId.toString();
    }
    //若是返回0则表示不是该用户好友
    return currentKey==null?0l:setOperations.add(currentKey, friend);
}

假如两个用户A和B都是用上上面的这个接口添加了很多的自己的好友,那么有一个需求就是要实现获取A和B的共同好友,那么可以进行如下操作:

public Set intersectFriend(User userA, User userB) {
    return setOperations.intersect(userA.getId.toString(), userB.getId.toString());
}

举一反三,还可以实现A用户自己的好友,或者B用户自己的好友等,都可以进行实现。

六、ZSet

Zset(Sorted set)是有序集合类型,相比于Set类型多了一个排序属性score(分值),对于有序集合ZSet来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。

有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

底层实现

Zset的底层实现是ziplist(压缩列表)和skiplist(跳表)实现的。

  • 如果有序集合的元素个数小于128个,并且每个元素的值小于64字节时,Redis会使用压缩列表作为Zset类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis会使用跳表作为Zset类型的底层数据结构;

skiplist(跳跃表)是一种有序的数据结构,它通过每一个节点维持多个指向其它节点的指针,从而达到快速访问的目的。

skiplist有如下几个特点:

  1. 有很多层组成,由上到下节点数逐渐密集,最上层的节点最稀疏,跨度也最大。
  2. 每一层都是一个有序链表,至少包含两个节点,头节点和尾节点。
  3. 每一层的每一个每一个节点都含有指向同一层下一个节点和下一层同一个位置节点的指针。
  4. 如果一个节点在某一层出现,那么该以下的所有链表同一个位置都会出现该节点。

具体实现的结构图如下所示

在跳跃表的结构中有head和tail表示指向头节点和尾节点的指针,能快速的实现定位。level表示层数,len表示跳跃表的长度,BW表示后退指针,在从尾向前遍历的时候使用。BW下面还有两个值分别表示分值(score)和成员对象(各个节点保存的成员对象)。跳跃表的实现中,除了最底层的一层保存的是原始链表的完整数据,上层的节点数会越来越少,并且跨度会越来越大。跳跃表的上面层就相当于索引层,都是为了找到最后的数据而服务的,数据量越大,条表所体现的查询的效率就越高,和平衡树的查询效率相差无几。

应用场景

zset的使用场景与set类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

  • 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  • ZSets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

*微博热搜榜,就是有个后面的热度值,前面就是名称。因为ZSet是有序的集合,因此ZSet在实现排序类型的业务是比较常见的,比如在首页推荐10个最热门的帖子,也就是阅读量由高到低,排行榜的实现等业务。下面就选用获取排行榜前前10名的选手作为案例实现,实现的代码如下所示:

/**
 * 获取前10排名
 * @return
 */
public static List<levelVO > getZset(String key, long baseNum, LevelService levelService){
    ZSetOperations<Serializable, Object> operations = redisTemplate.opsForZSet();
    // 根据score分数值获取前10名的数据
    Set<ZSetOperations.TypedTuple<Object>> set = operations.reverseRangeWithScores(key,0,9);
    List<LevelVO> list= new ArrayList<LevelVO>();
    int i=1;
    for (ZSetOperations.TypedTuple<Object> o:set){
        int uid = (int) o.getValue();
        LevelCache levelCache = levelService.getLevelCache(uid);
        LevelVO levelVO = levelCache.getLevelVO();
        long score = (o.getScore().longValue() - baseNum + levelVO .getCtime())/CommonUtil.multiplier;
        levelVO .setScore(score);
        levelVO .setRank(i);
        list.add( levelVO );
        i++;
    }
    return list;
}

以上的代码实现大致逻辑就是根据score分数值获取前10名的数据,然后封装成lawyerVO对象的列表进行返回。

七、Bitmap

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

由于bit是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。

内部实现

Bitmap本身是用String类型作为底层数据结构实现的一种统计二值状态的数据类型。

String类型是会保存为二进制的字节数组,所以,Redis就把字节数组的每个bit位利用起来,用来表示一个元素的二值状态,你可以把Bitmap看作是一个bit数组。

应用场景

Bitmap类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有0和1两种,在记录海量数据时,Bitmap能够有效地节省内存空间。

签到统计

在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。

签到统计时,每个用户一天的签到用1个bit位就能表示,一个月(假设是31天)的签到情况用31个bit位就可以,而一年的签到也只需要用365个bit位,根本不用太复杂的集合类型。

判断用户登陆态

Bitmap提供了GETBIT、SETBIT操作,通过一个偏移值offset对bit数组的offset位置的bit位进行读写操作,需要注意的是offset从0开始。

只需要一个key=login_status表示存储用户登陆状态集合数据,将用户ID作为offset,在线就设置为1,下线设置0。通过GETBIT判断对应的用户是否在线。50000万用户只需要6MB的空间。

连续签到用户总数

把每天的日期作为Bitmap的key,userId作为offset,若是打卡则将offset位置的bit设置成1。

key对应的集合的每个bit位的数据则是一个用户在该日期的打卡记录。

一共有7个这样的Bitmap,如果我们能对这7个Bitmap的对应的bit位做『与』运算。同样的UserID offset都是一样的,当一个userID在7个Bitmap对应对应的offset位置的bit=1就说明该用户7天连续打卡。

结果保存到一个新Bitmap中,我们再通过BITCOUNT统计bit=1的个数便得到了连续打卡3天的用户总数了。

八、HyperLogLog

HyperLogLog是Redis 2.8.9版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog是统计规则是基于概率完成的,不是非常准确,标准误算率是0.81%。

所以,简单来说HyperLogLog提供不精确的去重计数。

HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数,和元素越多就越耗费内存的Set和Hash类型相比,HyperLogLog就非常节省空间。

这什么概念?举个例子给大家对比一下。

用Java语言来说,一般long类型占用8字节,而1字节有8位,即:1byte = 8bit,即long数据类型最大可以表示的数是:263-1。对应上面的264个数,假设此时有263-1这么多个数,从0~263-1,按照long以及1k=1024字节的规则来计算内存总数,就是:((2^63-1) * 8/1024)K,这是很庞大的一个数,存储空间远远超过12K,而HyperLogLog却可以用12K就能统计完。

应用场景

百万级网页UV计数:HyperLogLog优势在于只需要花费12KB内存,就可以计算接近2^64个元素的基数,和元素越多就越耗费内存的Set和Hash类型相比,HyperLogLog就非常节省空间。

所以,非常适合统计百万级以上的网页UV的场景。

九、Geo

GEO是Redis 3.2版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO就非常适合应用在LBS服务的场景中。

底层实现

GEO本身并没有设计新的底层数据结构,而是直接使用了SortedSet集合类型。

GEO类型使用GeoHash编码方法实现了经纬度到Sorted Set中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为Sorted Set元素的权重分数。

这样一来,我们就可以把经纬度保存到Sorted Set中,利用Sorted Set提供的“按权重进行有序范围查找”的特性,实现LBS服务中频繁使用的“搜索附近”的需求。

应用场景

这里以滴滴叫车的场景为例,介绍下具体如何使用GEO命令:GEOADD和GEORADIUS这两个命令。

假设车辆ID是33,经纬度位置是(116.034579,39.030452),我们可以用一个GEO集合保存所有车辆的经纬度,集合key是cars:locations。

当用户想要寻找自己附近的网约车时,LBS应用就可以使用GEORADIUS命令。

例如,LBS应用执行下面的命令时,Redis会根据输入的用户的经纬度信息(116.054579,39.030452),查找以这个经纬度为中心的5公里内的车辆信息,并返回给LBS应用。

可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?这三个其实也可以算作一种数据结构,不知道还有多少朋友记得,我在梦开始的地方,Redis基础中提到过,你如果只知道五种基础类型那只能拿60分,如果你能讲出高级用法,那就觉得你有点东西

十、Stream

Stream是Redis 5.0版本新增加的数据类型,Redis专门为消息队列设计的数据类型。

在Redis 5.0 Stream没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • List实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一ID。

基于以上问题,Redis 5.0便推出了Stream类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

应用场景

消息队列

生产者通过XADD命令插入一条消息,消费者通过XREAD命令从消息队列中读取消息时,可以指定一个消息ID,并从这个消息ID的下一条消息开始进行读取

总结

Redis常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sorted set:有序集合)

这五种数据类型都由多种数据结构实现的,主要是出于时间和空间的考虑,当数据量小的时候使用更简单的数据结构,有利于节省内存,提高性能。

Redis五种数据类型的应用场景:

  • String类型的应用场景:缓存对象、常规计数、分布式锁、共享session信息等。
  • List类型的应用场景:消息队列(有两个问题:1.生产者需要自行实现全局唯一ID;2.不能以消费组形式消费数据)等。
  • Hash类型:缓存对象、购物车等。
  • Set类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset类型:排序场景,比如排行榜、电话和姓名排序等。

Redis后续版本又支持四种数据类型,它们的应用场景如下:

  • BitMap(2.2版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • HyperLogLog(2.8版新增):海量数据基数统计的场景,比如百万级网页UV计数等;
  • GEO(3.2版新增):存储地理位置信息的场景,比如滴滴叫车;
  • Stream(5.0版新增):消息队列,相比于基于List类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。


这篇关于Redis数据类型的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程