redis的类型(3)一些特殊类型

Redis特殊类型详解

Streams类型

Streams类型是干啥用的?

stream 就是一个队列(里面装着生产者消费者模型中的临界资源),属于是 List blpop / brpop 的升级版本. 生产者可以向stream类中写入消息,消费者可以从stream类中读出消息
在理解stream之前,我们要先理解事件回调机制,没错,就是我们前面讲的epoll事件回调机制,一个线程同时等多个IO事件,哪个IO事件完成了,就给线程发通知,线程去处理对应的收尾工作(哪个小摊饭做好了,就通知你去拿饭)。
在这里插入图片描述

stream中的一个个消息,其实就是对应一个个的事件,这些事件由生产者生产到stream中,再由消费者将其从stream中读走

stream常用命令

消息添加

XADD 用于向 Stream 添加消息,若 Stream 不存在则自动创建。如 XADD mystream * field1 value1* 的意思是让 Redis 自动生成消息 ID 。

消息读取

可以用XREAD指令 从一个或多个 Stream 读取消息,XREADGROUP 用于消费者组角度读取 。如XREAD COUNT 10 STREAMS mystream >表示从 mystream 读取 10 条消息 。

消费者组管理

XGROUP CREATE 创建消费者组,XGROUP DESTROY 删除组 。例如 XGROUP CREATE mystream mygroup $ ,$ 表示从最新消息开始消费

消息确认

XACK 用于消费者确认已处理消息 ,如XACK mystream mygroup messageId

获取信息

XINFO用于 获取 Stream 或消费者组信息 ,如 XINFO STREAM mystream

Geospatial类型

Redis Geospatial 是 Redis 提供的用于处理地理空间数据的功能模块,核心业务就是实现给定一个位置坐标,查找以这个位置坐标为原点,xx为半径的圆内有哪些元素。比如查找比特公司所在大楼1公里以内有哪些餐馆

GEOADD

用于将一个或多个地理位置添加到指定的键(有序集合 )中。例如:
GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
上述命令将Palermo和Catania两个地点的经纬度信息添加到名为Sicily的键中 。

GEOPOS

获取指定位置元素的经纬度坐标。如:
GEOPOS Sicily Palermo会返回Sicily这个有序集合中Palermo元素的经纬度

GEODIST

GEODIST 是 Redis 中用于计算两个地理空间位置之间距离的命令,其基本语法为:

GEODIST key member1 member2 [unit]
  • key:存储地理空间数据的有序集合键名
  • member1member2:要计算距离的两个位置名称
  • unit(可选):距离单位,支持 m(米,默认)、km(千米)、mi(英里)、ft(英尺)

示例步骤:

  1. 先添加几个地理空间位置

    # 添加城市经纬度(纬度,经度,名称)
    GEOADD cities 116.403874 39.914885 "北京"
    GEOADD cities 121.473701 31.230416 "上海"
    GEOADD cities 113.264385 23.129113 "广州"
    
  2. 计算北京到上海的直线距离(默认米)

    GEODIST cities "北京" "上海"
    # 结果:约 1318783.8543 米
    
  3. 指定单位为千米

    GEODIST cities "北京" "上海" km
    # 结果:约 1318.7839 千米
    
  4. 计算上海到广州的距离(英里)

    GEODIST cities "上海" "广州" mi
    # 结果:约 766.5868 英里
    
  5. 如果位置不存在,返回 nil

    GEODIST cities "北京" "深圳"
    # 结果:(nil) (因为未添加深圳的数据)
    

注意:

  • 计算的是直线距离(球面两点间最短距离),非实际交通路线距离
  • 底层基于有序集合实现,经纬度会被编码为 52 位浮点数存储
  • 精度受地球是球体的近似模型影响,误差通常在 0.5% 以内

这个命令常用于附近地点推荐、距离筛选等 LBS(基于位置的服务)场景。

GEORADIUS

GEORADIUS 能够根据给定的经纬度坐标,以指定半径范围查找附近的位置元素。比如:
GEORADIUS Sicily 15 37 100 km
这条指令就是查询以经度 15、纬度 37 为中心点(大致位于西西里岛附近),半径 100 千米 范围内,存储在 Sicily 键中的所有地理位置成员。

GEORADIUSBYMEMBER

GEORADIUSBYMEMBER 则是根据给定的位置元素名称,以指定半径范围查找附近的位置元素。例如:
GEORADIUSBYMEMBER Sicily Palermo 100 km
作用就是查询在 Sicily 键中,所有与 Palermo(巴勒莫)的直线距离在 100 千米以内的地理位置成员。

HyperLogLog 类型

应用场景只有一个,估算集合中的元素个数.

Set与HyperLogLog 的对比

Set有一个应用场景,统计服务器的 UV(用户访问的次数)。使用 Set 统计UV最大的缺点是什么?

使用 Set 当然可以统计 UV,但是最大的问题在于,如果 UV 数据量非常大,Set 就会消耗很多的内存空间,
假设 Set 存储 userId,每个 userId 按照 8 个字节算~~ 如果要统计1亿UV,那我就需要8亿字节,也就是800MB的内存空间(这好像也不是很大呀,别急,有对比),而同样大小的数据如果使用HyperLogLog 类型存储,我们仅仅需要至多12KB的空间(无论存多少数据,HyperLogLog 类型的存储空间最多都是只有12KB)

为什么HyperLogLog类型存储数据占用空间那么少呢?

之所以,set要消耗这么大的空间,是因为Set 需要存储每个元素的内容
而向HyperLogLog中插入元素时, HyperLogLog并不存储元素的内容,而只是记录 “元素的特征”。在新增元素的时候,HyperLogLog能够通过对比新插入数据的特征和内部已有数据的特征,判断当前插入的元素是否是一个HyperLogLog中已有的元素,如果是,则不会插入,如果不是,则记录这个新元素的特征,并将集合元素总数加1
也就是说,HyperLogLog可以告诉你它里面有多少个元素,也可以判断新插入的元素是否是它里面已有的元素,但它不能告诉你里面每个元素具体是什么,因为它压根就没记这个,它记的是元素的特征(而set就可以告诉你元素具体是什么,因为set存储的是一个完整的元素)

请问HyperLogLog记录 “元素的特征”具体是咋记的呢?可以保证判重是百分百准确吗?

具体咋记,核心思路是位操作,按位与或啥的(数学理论)
并不能保证判重百分百准确,确实是存在一定误差的,标准误差为 0.81%,即100次里面最多错一次,很多场景下该误差是可接受的

常用命令

PFADD

  • 用于向 HyperLogLog 结构中添加元素 。
  • PFADD myhyperloglog element1 element2 ,将 element1 和 element2 添加到名为 myhyperloglog 的 HyperLogLog 中。

PFCOUNT

  • 计算 HyperLogLog 结构中不重复元素的近似数量 。
  • 例如 PFCOUNT myhyperloglog ,返回估算的元素个数。

PFMERGE

  • 将多个 HyperLogLog 合并为一个 。
  • 比如 PFMERGE newhyperloglog hyperloglog1 hyperloglog2 ,把 hyperloglog1 和 hyperloglog2 合并到 newhyperloglog 中。

bitmaps类型

就是我们熟悉的位图
位图本质上,就还是一个 集合. 属于是 Set 类型针对整数的特化版本,引入位图的目的主要还是为了节省空间

HyperLogLog 类型似乎也能节省空间,这俩谁更省空间?这俩有啥区别呢?

HyperLogLog 更省空间

  1. 位图就是能够精确地记录一个元素是否存在(对应比特位是0就不存在,是1就存在),而HyperLogLog只能通过元素特征估计一个元素是否存在于HyperLogLog中
  2. bitmaps既可以存储数字,也可以存储字符串,而HyperLogLog不存储元素内容,只是计数效果
  3. hyperloglog 存储元素的时候,提取特征的过程是不可逆的!!(存储数据的信息量会被丢失一部分,因此我们无法再从hyperloglog 中拿到完整的元素,只能判断这个元素存不存在)给你个猪肉,能把它做成火腿肠,给你个火腿肠,能还原回猪肉嘛

举个粒子
假设有 1000 个部署在现场的传感器,编号从 0 - 999 ,需要快速判断某个传感器在一小时内是否向服务器发送过信号 。

  • 如果我们用位图来存储信息的话,假如说我现在想知道123号传感器在一小时内是否向服务器发送过信号,只需要找到位图中的第123位,看看是0还是1就行了
  • 而如果我们用HyperLogLog存储信息,假如说我现在想知道123号传感器在一小时内是否向服务器发送过信号,我需要向HyperLogLog中插入123,然后看是否能够插入成功。如果成功,就说明没有发送,否则就是发送了。但是这个判重的操作是有概率失败的,位图基本不会错

bitmap相关命令

  1. SETBIT key offset value

    • 设置位图中指定偏移量offset的位为value(0 或 1 )
    • key为位图键名 。若offset超出当前字符串长度,会自动扩展字符串 。例如 SETBIT user_status 100 1 ,将user_status位图中偏移量为 100 的位设置为 1 。
  2. GETBIT key offset

    • 获取位图中指定偏移量offset的位的值 。
    • 若offset超出字符串长度,默认返回 0 。
    • 如 GETBIT user_status 100 ,获取user_status位图中偏移量为 100 的位的值 。
  3. BITCOUNT key [start end]

    • 统计位图中值为 1 的位的数量 。
    • start和end为可选参数,指定统计的位范围(以字节为单位 ) 。
    • 比如 BITCOUNT user_status 统计user_status位图中所有值为 1 的位的数量;
    • BITCOUNT user_status 0 10 统计前 10 个字节中值为 1 的位的数量 。
  4. BITOP operation destkey key [key ...]

    • 对一个或多个位图执行位运算,结果存储在destkey中 。
    • operation为操作类型,如AND(与 )、OR(或 )、XOR(异或 )、NOT(非 ) 。例如BITOP AND result_key bitmap1 bitmap2,将bitmap1和bitmap2进行与运算,结果存于result_key

Bitfields类型

Bitfields你可能听起来比较陌生,但我如果说C 语言中的位段呢?C语言中的位段可以用来精确控制每个成员的位数,显著减少内存占用,比如

struct Bits {
    unsigned int a : 10;  // 占用前10位
    unsigned int b : 20;  // 占用接下来20位(共30位,仍在1个int内)
    unsigned int c : 5;   // 新int的前5位
};
// sizeof(struct Bits) 通常为 8 字节(2个int)

Redis 中的 bitfield 和 C 中的位域,是非常相似的!!
bitfield 可以理解成一串二进制序列(字节数组)
同时可以把这个字节数组中的某几个位,赋予特定的含义,并且可以进行 读取/修改/算术运算 相关操作(比如把0~8个比特位叫做变量a,9~24位叫做变量b,24~32位叫做变量c)
位域这个东西,相比于之前的 String / hash 来说,目的仍然是节省空间 (因为很明显,存储地更加紧凑了)

bitfields应用举例

在在线游戏中,需记录每个玩家的两个关键指标:金币总数和击杀怪物数量 。因游戏玩家众多,这些计数器至少需 32 位来存储 。使用BITFIELD为每个玩家记录这些指标 。

  1. 初始化金币数量:新玩家开始教程时拥有 1000 金币,对应偏移量 0 。使用命令BITFIELD player:1:stats SET u32 #0 1000

    • player:1:stats是存储玩家 1 统计信息的键名
    • SET表示设置值
    • u32表示无符号 32 位整数 ,
    • #0 1000 表示在player:1:stats这个键对应的二进制序列的第 0 个位置开始,设置一个值为 1000 的无符号 32 位整数
  2. 更新金币数量和击杀计数:玩家杀死囚禁王子的哥布林后,获得 50 金币并增加击杀计数(偏移量 1 ) 。使用命令BITFIELD player:1:stats INCRBY u32 #0 50 INCRBY u32 #1 1

    • #0 和 #1 是偏移量的索引,#0 代表第一个 32 位无符号整数所在的位置,#1 代表第二个 32 位无符号整数所在的位置。
    • 可以把 player:1:stats 这个位图看作是由多个 32 位无符号整数依次排列组成的,#0 对应第一个,#1 对应第二个。
    • INCRBY表示按指定值增加 ,第一个INCRBY u32 #0 50将偏移量 0(金币数量 )的值增加 50 ,第二个INCRBY u32 #1 1将偏移量 1(击杀怪物数量 )的值增加 1 。
    • 命令执行后分别返回更新后的金币数量 1050 和击杀怪物数量 1 。
  3. 玩家要向铁匠支付 999 金币,购买一把传说中的生锈匕首 :使用命令BITFIELD player:1:stats INCRBY u32 #0 -999

    • player:1:stats是记录玩家 1 游戏统计信息的键
    • INCRBY表示按指定数值增减
    • u32表示无符号 32 位整数
    • #0代表偏移量为 0 ,即对应金币数量的存储位
    • -999表示减少 999 金币
    • 命令执行后返回当前金币数量为 51 ,说明在扣除 999 金币后,玩家剩余 51 金币 。

渐进式遍历

什么叫做渐进式遍历?

回顾keys * 操作:一次性的把整个 redis 中所有的 key 都获取到。这个操作比较危险.可能会一下子得到太多的 key, 阻塞 redis 服务器!
而如果我们通过渐进式遍历的命令替代keys * , 我们就可以做到, 既能够获取到所有的 key, 同时又不会卡死服务器

渐进式遍历不是一个命令把所有的 key 都拿到,而是每执行一次命令, 只获取到其中的一小部分, 这样的话保证当前这一次操作不会太卡,要想得到所有的 key 就需要多次遍历了

多次执行渐进式遍历命令——化整为零的思想

如何进行渐进式遍历?

在这里插入图片描述
在这里插入图片描述
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

  • cursor——迭代游标

    • 什么是迭代游标?
      你渐进式遍历不是一次遍历一部分吗,那下次遍历的时候,不就是要接着上次遍历结束的位置开始遍历吗。迭代游标就是你此次遍历的起点元素在整个队列中的光标(注意,是光标不是下标)
    • 为什么你不能将迭代游标看成是下标?
      • 比如在下图中,如果cursor是一串连续递增的整数,那么第二个scan命令应该返回的cursor就是5,但实际上却是7
      • 你不能将cursor理解成“下标”,因为cursor不是一串连续递增的整数!!仅仅就是一个“字符串”。光标这个概念,程序猿/客户端是不认识的,redis服务器则知道这个光标对应的元素位置
    • 注意:cursor这个参数可不敢乱传,必须传入上一次scan命令返回的游标(首次调用时传入0)当返回的游标为0时,迭代结束。
  • MATCH pattern(可选):

    • 用于指定键的匹配模式,和KEYS命令的模式匹配是一样一样的
    • 比如SCAN 0 MATCH user:*,就是查找所有以 “user:” 开头的键
  • COUNT count(可选):
    你渐进式遍历不是一次遍历一部分吗,那这部分遍历到底是遍历多少个元素呢?这个就由count来指定
    count用于指定每次迭代要返回的键的数量,不过这只是一个参考值,实际返回的键的数量可能会有所不同(可能会少一些,比如遍历到尾部了)

介绍完命令,下面我们就来实践一下

  1. 首先我们先搭个环境
    在这里插入图片描述

  2. 直接scan 0,遍历了10个元素
    在这里插入图片描述

  3. 用上一次scan返回的光标11作为新的迭代游标,再次进行渐进式遍历,由于上一次渐进式遍历其实就已经遍历完了所有的元素,所以这次已经没有元素让我遍历了,我直接迭代游标返回0,表示结束遍历
    在这里插入图片描述4. count 指定渐进式遍历中一次遍历的元素个数
    在这里插入图片描述

这里的渐进式遍历,在遍历过程中,不会在服务器这边存储任何的状态信息。此处的遍历是随时可以终止的~~不会对服务器产生任何的副作用~~(如何理解这句话?)

一般的服务器,一个遍历请求一旦开始,就必须得执行完毕(中间可以停,但是早晚得把所有指定元素遍历一遍),不能说执行着一半就停了,后面该遍历的元素再也不遍历了。而redis确实可以做到,遍历随时可以中断,想停就停
打个比喻的话,一般的遍历如果想中途退出,就是属于上了烤架的烤串,退不掉了,Redis渐进式遍历如果想中途退出,就是属于去超市买东西,我已经把货都装到了小推车了,在我将小推车推向收银台之前,我随时都可以把小推车一扔,自己直接出去

如果我在Redis正在进行scan遍历的过程中对元素内容做了修改,会对遍历的过程产生影响吗?

渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。
不仅仅是redis,遍历其他内容的时候,也是比较忌讳一边遍历,一边修改的,比如在C++ STL中,在遍历中进行 修改/新增/删除操作 也很有可能导致 迭代器失效 (C++里遍历中修改会直接报错崩溃,但你redis遍历中修改不会给你崩溃,就是说操作出现遗漏重复了,你都不知道!C++人家好歹告诉你错了,redis连你错了都不告诉你,你后面出现排障的时候,都不知道排谁的障! )
在这里插入图片描述

注意!!上面这样的代码,就会导致“迭代器失效”!当删除完成之后,it这个迭代器指向哪里,已经不知道了!!此时循环体结束之后再次 ++it,不一定就能指向下一个元素了

补充:为什么c++中喜欢前置++?

大部分编程语言中,++ 都是后置写的。Java 中 ++ 只是针对 数字 ~~ 数字本身无论是前置后置都是足够快。而C++ 中 ++ 可能是针对一个对象。(C++ 有运算符重载)。之所以C++ 更偏好 前置++,是因为在 C++ 里前置 ++ 比后置 ++ 少了一次临时对象的构造,所以性能更高

  • 前置++
    前置++ 的操作顺序是 “先自增,再返回自增后的值” 。它直接对原对象进行自增操作,然后返回自增后原对象的引用。比如对于自定义类型obj ,前置++ 重载函数大概形式为:
    这个过程没有创建临时对象,直接返回自增后的原对象,所以不存在临时对象构造和析构的开销

  • 后置++
    后置++ 的操作顺序是 “先返回自增前的值,再自增” 。由于要返回自增前的值,而自增操作会改变对象本身,所以必须先创建一个临时对象来保存自增前原对象的状态。以自定义类型为例,后置++ 重载函数类似如下形式:
    这里old 就是临时对象,在函数调用结束后,这个临时对象会被析构。创建和销毁这个临时对象会带来额外开销,相比之下,前置++ 就少了这一次临时对象构造及后续析构的过程,所以在性能上更有优势 。

Redis的database

Redis中有database吗?如果有,那它与mysql中的database有什么区别?

Redis中当然也有database,与mysql中的database最大的区别在于,redis 中的 database 是现成的,咱们用户不能创建新的数据库,也不能删除已有的数据库~~而mysql上则是可以随心所欲的创建/删除 数据库

  • redis给咱们提供了16个数据库,编号为0 - 15。这16个数据库中的数据是隔离的(相互之间不会有影响)
  • redis默认情况下使用的数据库就是0号,我们可以使用指令select + 数据库编号来切换数据库。实际使用redis很少会关注到数据库,一般都是默认就用0号就可以了
  • 看下面的实验,在0号数据库中的<key, 111>,在1号数据库中就查不到
    在这里插入图片描述

对Redis的database进行的一些基本操作

如何切换当前使用的数据库?

select + 数据库编号
比如select 0就是将当前使用的数据库切换到0号数据库

如何查看Redis当前数据库中有多少个key?

直接命令行输入指令:DBSIZE

如何删除当前数据库中的所有key?

直接命令行输入指令:flushdb

如果删除所有数据库中的所有key?(如何删库?)

直接命令行输入指令:flushall

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值