04、Redis深入数据结构
一、简单动态字符串SDS
无论是Redis中的key还是value,其基础数据类型都是字符串。如,Hash型value的field与value的类型,List型,Set型,ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了一种字符串结构类型,这种字符串本身的结构比较简单,但是功能强大,称为简单动态字符串 Simple Dynamic String ,简称为SDS。
SDS结构
SDS不同于C中的字符串,C中的字符串是以一个双引号括起来,以空字符'\0'结尾的字符序列。而SDS是重新定义的一个结构体,在redis的安装目录src/sds.h
这个结构体有三个部分组成:
struct sdshdr{// 字节数组,用来保存字符串char buf[];// SDS的长度,在buf[]中使用的字节数量int len;// buf[]中没有使用的字节数量int free;
}
注意:虽然redis当中定义了结构体sds,但是并不是所有的字符串都使用这种方式存储,比如我们执行一个set命令,返回了一个OK,这个OK就是C语言中的字符串。get命令获取的值返回的也是C语言的字符串。C 字符串只会出现在字符串“字面常量”中,并且该字符串不可能发生变更!!
我们看下面的命令我们来了解一下类型与实际内存中的存储结构的区别:
set name zhangsan # 写入key为name的值为zhangsan,这里写入的是zhangsan应该是SDS结构类型的
type name # 这里返回的会是string 表示是字符串
object encoding name # 这里会返回embstr,它就表示是SDS,是内存中的存储类型set age 23 # 写入key为age的值为23
type age # 这里返回的是string,表示是字符串
object encoding age # 这里返回的是int,表示在内存中是以整型存储的
SDS优势
C字符串底层是一个数组,字符串最后以’\0‘来结束。
优势一:防止字符串长度获取性能瓶颈
C字符串的长度获取必须要通过遍历整个字符串(直到\0结束)才可以获得,这样的话对于超长的字符串遍历,还是会产生性能瓶颈的。
SDS结构体中我们可以看到直接存放着字符串长度的数据,所以不管字符串有多长,获取字符串的长度所要消耗的系统性能都是一样的,不会成为redis的性能瓶颈。
优势二:保障二进制安全
C字符串对于字符串中的字符是有要求的,遇到\0就表示字符串结束了,但是我们在redis中会用字符串存储二进制数据(图片,视频,压缩文件...),在这些文件中使用\0在中间作为分隔符的情况是很常见的。所以redis中的SDS不以\0作为字符串结束标志,而是通过len属性来判断字符串是否结束,所以对于程序处理SDS中的字符串数据,不需要对数据做任何的限制、过滤等处理,直接读取即可。写入的是什么读出来的就是什么。
优势三:减少内存再分配
SDS中采用了空间预分配策略与惰性空间释放策略来避免内存再分配的问题。
这两个策略就是以空间换时间的做法!
如果我们要释放SDS的未使用空间,可以通过sdsRemoveFreeSpace()函数来释放。
优势四:兼容C函数
redis中提供了很多的SDS的api,方便用户对redis进行二次开发,为了兼容C函数,SDS的底层数组buf[]中的字符串仍以空字符'\0'结尾。
SDS常用操作函数
- 空间预分配策略,每次SDS进行空间扩展的时候,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,从而减少内存再分配次数,客户分配的未使用空间大小取决于空间扩展后SDS的len属性值。
- 如果len属性值小于1M,那么会分配未使用空间保持与len属性相当
- 如果len属性值大于等于1M,那么会固定分配未使用空间1M
-
惰性空间释放策略,如果SDS字符串长度如果缩短,那么多出的未使用空间会暂时不释放,而是增加到free当中去,从而保证后期扩展SDS时减少再分配的次数
函数 | 功能 |
---|---|
sdsnew() | 使用指定的C字符串创建一个SDS |
sdsempty() | 创建一个不包含任何字符串数据的SDS |
sdsdup() | 创建一个指定SDS的副本 |
sdsfree() | 释放是定的SDS |
sdsclear() | 清空指定SDS的字符串内容 |
sdslen() | 获取指定SDS的已使用空间len值 |
sdsavail() | 获取指定SDS的未使用空间free值 |
sdsMakeRoomFor() | 使指定的SDS的free空间增加指定的大小 |
sdsRemoveFreeSpace() | 释放指定SDS的free空间 |
sdscat() | 把指定的C字符串拼接到指定SDS的字符串末尾 |
sdscatsds() | 把指定的SDS字符串拼接到另一个指定的SDS字符串末尾 |
sdscpy() | 把指定C字符串复制到指定的SDS中,覆盖原SDS字符串内容 |
sdsgrouzero() | 扩展SDS字符串到指定长度,这个扩展使用空字符'\0'填充 |
sdsrange() | 截取指定SDS中指定范围内的字符串 |
sdstrim() | 在指定SDS中删除报有指定C字符串中出现的所有字符 |
sdsemp() | 对比给定的两个SDS字符串是否相同 |
sdstolow() | 把指定SDS字符串中的所有字母变为小写 |
sdstoupper() | 把指定SDS字符串中所有字母变为大写 |
这些函数在需要对redis二次开发时可能会用到,正常使用redis时用不到!
集合底层实现原理
Set集合
对于set,我们看它底层的实现可以如下:
sadd cities bj sh gz sz # 添加一个set集合
type cities # 返回的类型是set
object encoding cities # 返回的是hashtable
从上面可以看到set底层是使用hashtable存储的!
Hash与ZSet
这两种它们的底层实际上有两种:压缩列表zipList;跳跃列表skipList
至于Redis什么时候用zipList,什么时候用skipList对于我们用户来说是透明的,用户写入不同的数据,系统会自动判断使用不同的实现。
只要我们的数据量不超过我们设定的阈值则会使用zipList,一旦超过这个阈值则什么变为skipList。
关于这个阈值的查看,可以使用命令进行查看:
config get zset-*-ziplist-*
# 运行后得到的结果类似如下:
1) "zset-max-ziplist-entries"
2) "128"
3) "zset-max-ziplist-value"
4) "64"
上面的信息表示,zset中包含的元素不超过128,并且每一个元素的大小不超过64字节就会使用ziplist。这两个任意一个不满足就会变为skipList。
同样hash的查询方式也类似,其查询命令如下:
config get hash-*-ziplist-*
# 运行这个命令后的结果如下:
1) "hash-max-ziplist-value"
2) "64"
3) "hash-max-ziplist-entries"
4) "512"
关于zipList的底层结构
首先,什么是zipList?
通常称为压缩列表,是一个经过特殊编码(体现了zip压缩的)的用于存储字符串或整数的双向链表。其底层数据结构由三部分构成:head(基础信息)、entries(元素)、end(结束标记),这三部分在内存上是连续存放的。
zipList的三部分在底层如何存储?
说明 | |
---|---|
head | 它包含总括性信息,由三部分组成: zlbytes:4个字节,用来存放zipList列表整体数据结构所占的字节数,包括zlbytes本身的长度 zltail:4个字节,用于存放zipList中最后一个entry在整个数据结构中的偏移量(字节)这个数据可以快速定位列表的尾entry位置,便于操作。 zllen:两个字节,用于存放列表包含的entry的个数,两个字节是16位,所以zipList最多可以包含有 |
entries | entries是真正的列表,由很多的列表元素entry构成,由于不同的元素类型、数值是不同的,从而导致每个entry的长度不同。每个entry由三部分构成: prevlength:用来记录上一个entry的长度,用以实现逆序遍历,默认长度是1字节,只要上一个entry的长度<254字节,prevlength就会占1字节,否则会扩展为3字节长度。 注意:这里分节点为什么是小于254?不是255?因为zipList中有一个部分是1字节的全为1表示结束,所以不能用255,而254也是一个保留的值用来做为prevlength的自动扩展标志的 encoding:用来标记后面的data具体类型。如果data为整数类型,encoding固定长度是1字节,如果data为字符串类型,则encoding长度可能会是1字节、2字节或5字节。data字符串不同的长度,对应着不同的encoding长度 data:真正存储的数据。数据类型只能是整数类型或字符串类型,不同的数据占用的字节长度不同 |
end | end只包含一部分,称为zlend。其占一个字节,值固定为255,也就是二进制全为1,表示一个zipList列表的结束。 |
zipList的结构图大致如下:
关于listPack底层结构
zipList是在Redis7之前版本中使用的,到了7版本不再使用zipList,而是使用listPack进行了替换。
为什么要把zipList替换为listPack?
在zipList中每一个entry中的prevlength当中会记录前一个entry的长度,如果在中间某个元素前插入了一个元素或修改了元素,那这个元素entry中的prevlength还要跟着去调整(级联更新)。在高并发的写操作场景下会极度地降低Redis的性能,所以为了实现更紧凑、更快的解析,更简单的写操作所以重写了zipList,并把它命名为listPack。
那listPack有是什么呢?
它也是一种经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也是由三部分构成的:head、entries、end并且这三部分在内存上也是连续进行存放的。
listPack与zipList的最大区别在于head和每个entry的结构上,表示列表结束的end两者是相同的占一个字节且全为1。
说明 | |
---|---|
head | 由两部分组成: totalBytes:占4个字节,用来存放listPack列表整体数据结构所占的字节数,也包含totalBytes本身的长度 elemNum:占2个字节,用于存放列表包含的entry个数其意义与zipList中的zllen是相同的 注意:在listPack中没有记录最后一个entry偏移量的zltail |
entries | entries也是listPack中真正的列表,由很多的列表元素entry构成。由于不同的元素类型、数值的不同,从而导致每个entry的长度不同。但是与zipList的entry结构相比,listPack的entry结构发生了较大的变化。其中变化最大的就是没有了记录前一个entry长度的prevlength,而增加了记录当前entry长度的element-total-len。而这一变化仍然可以实现逆序遍历(由新的算法来支撑),但去避免了由于在列表中间修改或插入entry引发的级联更新。 每个entry由三个部分组成: encoding:标记后面data的具体类型,如果是整数类型,其长度可能是1,2,3,4,5,9字节。不同的字节长度,标识位不同。如果是字符串则长度可能是1,2,5字节。data字符串不同的长度,对应着不同的encoding长度 data:真正存储的数据。数据类型只能是整数类型或字符串类型,不同的数据占用的字节长度不同 element-total-len:记录当前entry的长度,用来支持逆序遍历,由于其特殊的记录方式,其本身占用的字节数据中能会是1,2,3,4,5字节 |
end | end只包含一部分其占一个字节,值固定为255,也就是二进制全为1,表示一个zipList列表的结束。 |
listPack的底层结构图大致如下:
注意:encoding是比较复杂的,有些数据中可能在encoding中就包含了数据本身了没有data这部分了
关于skipList底层结构
什么是skipList呢?
skipList指的是跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。简单理解这里的跳表也是链表的一种,只是在链表的基础上添加了跳跃的功能。因为有了这个跳跃的功能,所以在查找元素时,能够提供较高的效率。
skipList是如何跳跃后提高查找元素效率的呢?原理是什么?
我们先看下面这个有序列表
此时如果我们要查找某个数据比如23,则需要从头开始一个一个进行比较,直接找到包含要找数据的节点或者找到第一个比要找数据还大的节点或者找到末尾节点(这后面的两种都表示没有找到),这种方式的查找没有效率是不高的,为了提升效率可以在偶数结点上加上一个指针,让这个指针指向下一个偶数节点,如下图所示:
此时所有的偶数节点连成了一个新的链表(称为高层链表),高层链表的节点数只有原链表的一半。如果这个时候要查找某个数据时,会先沿着高层链表进行查找,当遇到第一个比待查数据大的节点时,立即从这个大节点的前一个节点回到原链表进行查找。比如我们想找23,先在高层节点进行遍历,遍历到31时发现比23大了,此时会从它的前一个节点开始也就是19按原链表顺序进行遍历,这时遍历到23,找到元素。
使用跳表这种方式明显可以减少比较的次数,提高查询的效率,如果链表的元素比较多还想进一步提升效率可以再加一层高层链表。此时就变成了先遍历最高层的链表,再遍历第二高层的链表,依次类推。
上面这种跳表的基础结构可能存在的问题:
这种对链表的分层级的方式看上去确实提升了查找效率,但是实际操作时情况比较多,会产生问题,比如固定序号的元素有固定的层级,那么列表中的元素如果出现增加或删除,这会导致列表整体的层级都需要调整,这会大大降低系统性能。
解决这种序号变化带来的层级变化影响性能的算法优化办法:
为了避免前面的问题,skipList采用了随机分配层级方式。在确认了总层级后,每添加一个新的元素时会自己动为其随机分配一个层级。这种随机性就解决了节点序号与层级之前的固定关系问题。
数据新插入进来不影响其它节点的层级数。只需要修改插入节点的前后的指针,而不需要对很多节点进行调整,这也降低了插入操作的复杂度。
skipList指的就是除了最下面第1层链表外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。在查找数据的时先在高层级链表中进行查找,然后逐层降低,最终可能会降到第 1 层链表来精确地确定数据位置。在这个过程中由于跳过了一些节点所以提升了效率。
关于quickList底层结构
什么是quickList呢?
它是一个快速列表,它本身是一个双向无循环链表,它的每一个节点都是一个zipList。从Redis3.2版本开始对于List底层实现,使用quickList代替了zipList和linkedList。
quickList本质上来讲是zipList和linkedList的混合体,它把linkedList按段切分,每一段使用zipList来紧凑存储若干个真正的数据元素,多个zipList之间使用双向指针串接起来。对于每个zipList中最多可存放多大容量的数据元素,在配置文件中可以通过list-max-ziplist-size属性来指定。
如何对它其中的元素进行检索?
首先我们要清楚的是:对于List元素的检索操作,都是使用索引来操作的。
quickList由一个一个的zipList构成,每个zipList的zllen中记录了当前zipList中包含的entry的个数,根据要检索的index,从quickList的头节点开始,逐个对zipList的zllen做求和操作,直到找到第一个求和后的值大于要检索的index时,那么要检索的元素就在这个zipList当中。
如何对它进行元素的插入呢?
由于zipList是有大小限制的,所以在quickList中插入一个元素则相对就比较复杂了。它需要分几种情况来处理。
假设要插入的元素大小为insertBytes,而查找到的插入位置所在zipList当前的大小为zlBytes,处理情况则如下:
- 情况1:当insertBytes + zlBytes <= list-max-ziplist-size时,直接插入到zipList中的相应位置即可
- 情况2:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于这个zipList的首部位置时,此时需要看当前zipList的前一个zipList的大小prev_zlBytes
- 如果insertBytes + prev_zlBytes <= list-max-ziplist-size时,直接把元素插入到前一个zipList的尾部位置
- 如果insertBytes + prev_zlBytes > list-max-ziplist-size时,就会直接创建一个新的zipList并连接入quickList中
- 情况3:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于这个zipList的尾部位置时,此时需要看当前zipList的后一个zipList的大小next_zlBytes
- 如果insertBytes + next_zlBytes <= list-max-ziplist-size时,直接把元素插入到后一个zipList的首部位置
- 如果insertBytes + next_zlBytes > list-max-ziplist-size时,就会直接创建一个新的zipList并连接入quickList中
- 情况4:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于这个zipList的中间位置时,则会把zipList分割为两个zipList连接入quickList当中然后把元素插入到分隔后的前面zipList的尾部
删除操作如何操作?
对于删除操作,只需要注意一点,在相应的 zipList 中删除元素后,该 zipList 中是否还有元素。如果没有其它元素了,则将该 zipList 删除,将其前后两个 zipList 相连接。
关于Redis中key与value支持的数量
- Redis中最多可以处理
个key,大约42亿。每个 Redis 实例至少可以处理 2.5 亿个 key
- 每个Hash,List,Set,ZSet集合都可以包含
个元素
二、BitMap操作
BitMap是什么?
BitMap是Redis2.2.0版本中引入的一种新的数据类型。这个数据类型本质上就是一个仅包含0和1二进制字符串。而它相关的命令都是对这个字符串二进制位的操作。用于描述这个字符串的属性有三个:key,offset,bitValue
- key:BitMap是Redis的key-value中的一种Value的数据类型,所以这个value一定是会有对应key的
- offset:每个BitMap数据都是一个字符串,字符串中的每个字符都有其对应的索引,这个索引是从0开始计数的。该索引则称为每个字符在BitMap中的偏移量offset, 它的范围是[0,
],所以最大值是42亿多。
- bitValue:每个BitMap数据中都是一个仅包含0和1的二进制字符串,每个offset位上的字符就称这个位的值bitValue,它的值只能是0或者1
相关操作命令
命令 | 说明 | 示例 |
---|---|---|
setbit | 为指定key的BitMap数据的offset位置上设置值为value,它的返回值是offset位置的bitValue 对于原BitMap字符串中不存在的offset进行赋值,字符串会自动伸展以确保它可以把value保存到指定的offset上,当字符串值进行伸展时,空白位置上会以0来填充,对于value值只能是0或者1。 注意:对于较大的offset的setbit操作来说,内存分配过程可以造成Redis服务器被阻塞 | setbit sing 3 1 |
getbit | 对key所存储的BitMap字符串值,获取指定offset偏移量上的位值bitValue 当offset比字符串值的长度大,或者key不存在时,返回0 | getbit sing 3 |
bitcount | 统计给定字符串中被设置为1的bit位的数量。一般情况下统计范围是给定的整个BitMap字符串。也可以通过指定额外的start或end参数,实现仅对指定字节范围内字符串进行统计。 注意:是包含start和end的,且start与end的单位是字节,不是bit,并且从0开始计数,也可以在后带上BIT选项指定是按bit单位指定统计 | bitcount sing 0 1:这个命令用来统计前两个字节,因为0表示第一个字节,1表示第二个字节 bitcount sing 0 8 BIT:这里指统计的是0到8位,我们给定了单位选项是BIT |
bitpos | 返回key指定的BitMap中第一个值为指定值的bit的二进制位的位置,默认情况下,命令会检测整个BitMap,也可以指定start参数与end参数来指定检测的范围。start 和end默认是字节为单位,也可以带上个单位选项 | bitpos sing 1 0 0:这里0 0表示就找第一个字节中值为1的位是哪位 |
bitop | 对一个或多个BitMap字符串key进行二进制位操作,并把结果保存到目标key上,对于操作类型可以是AND,OR,NOT,XOR中的任意一种 AND:表示按位与操作 OR:表示按位或操作 NOT:表示非操作 XOR:表示异或操作 | bitop AND result rel sing:AND表示操作,result表示结果保存的BitMap,rel和sing是两个进行操作的BitMap bitop or result rel sing:按位或 bitop not result result:NOT操作只接收一个进行按位操作BitMap,这个操作表示把result这个BitMap中各位进行取反再写回去 bitop xor result rel sing:按位异或 |
应用场景
由于 offset 的取值范围很大,所以其一般应用于大数据量的二值性统计。
例如平台活跃用户统计(二值:访问或未访问)、支持率统计(二值:支持或不支持)、员工考勤统计(二值:上班或未上班)...
注意:对于数据量较小的二值性统计并不适合 BitMap,可能使用 Set 更为合适。当然,具体多少数据量适合使用 Set,超过多少数据量适合使用 BitMap,这需要根据具体场景进行具体分析
关于一个平台统计日活跃用户数量的方案:
方案一:使用set统计
如果使用 Set 来统计,只需上线一个用户,就将其用户 ID 写入 Set 集合即可,最后只需统计出 Set 集合中的元素个数即可完成统计(scard取集合长度即可)。
方案二:使用BitMap统计
先定义一个BitMap,其占有的bit位至少为注册用户数量,当上线一个用户,则立即使用其中一个bit位置为1,最后只需要使用bitcout来统计整个BitCount中为1的个数即可。
这两个方案使用哪个合适呢?
这个要看数据量,如果用户量大活跃用户比较大则使用BitMap更合适更能节省内存空间。
三、HyperLogLog操用
它是Redis2.8.9版本中引入的一种新的数据类型,其意义是hyperlog log,超级日志记录。这个数据类型可以简单理解为一个set集合,集合的元素为字符串。
实际上HyperLogLog是一种基数计数概率算法,通过这个算法可以利用极小的内存完成独立总数的统计,其所有相关命令都是对这个set集合的操作。
它的命令都是pf开头的,目的是纪念念 Philippe Flajolet 博士(法国人)对组合数学和基数计算算法分析的研究。
相关命令
命令 | 说明 | 示例 |
---|---|---|
pfadd | 把任意数量的元素添加到指定的HyperLogLog集合里,如果内部存储被修改了返回1,否则返回0 | pfadd p1 sz ls ww |
pfcount | 这个命令对于单个key时,给定key的HyperLogLog集合的近似基数,对于多个key时,返回所有key的HyperLogLog集合的并集的近似基数,如果key不存在则返回0 | pfcount p1 p2 p3 |
pfmerge | 把多个HyperLogLog集合合并为一个HyperLogLog集合,并存储到指定的key中,合并后的HyperLogLog的基数接近于所有被合并集合的并集 | pfmerge destpf p1 p3:把p1与p3合并后的集合存在destpf |
应用场景
HyperLogLog 可对数据量超级庞大的日志数据做不精确的去重计数统计。
注意:这个统计不是是精确的!!Redis官方给的误差在0.81%
四、Geospatial操作
Geospatial,地理空间
Redis在3.2版本中引入的数据类型。这个类型本质上仍是一个集合,只不过这个集合元素比较特殊,它是由三个部分构成的数据结构。这种数据结构称为空间元素。
- 经度:longitude。有效值是:[-180,180]。正数表示东经,负数表示西经
- 纬度:latitude。有效纬度是:[-85.05112878,85.05112878]。正的表示北纬,负的表示南纬
- 位置名称:为经纬度所标注的位置命名和名称,也称为Geospatial集合的空间元素名称
可以通过这个类型设置、查询某地理位置的经纬度,查询某范围内的空间元素,计算两个空间元素之间的距离等,它的命令都是以geo开头的。
相关命令
命令 | 说明 | 示例 |
---|---|---|
geoadd | 把一个或多个空间元素添加到指定的空间集合中,如果输入一个超出范围的经纬度时会返回一个错误 | geoadd location 120.58 31.30 sz |
geopos | 从指定的地址空间中返回指定元素(可以指定返回多个元素)的位置(经纬度),返回的值是一个数组 | geopos location sz cd |
geodist | 返回两个给定位置之间的距离,其中unit单位必须是以下单位中的一种(默认是米) m:米 km:千米 mi:英里 ft:英尺 如果两个位置之间其中一个不存在,那么返回空值,计算距离在极端情况下可能会产生最大0.5%的差距 | geodist location cd sz KM |
geohash | 返回一个或多个位置元素的geohash值,这个geohash是一种地址编码方法,它可以把二维空间经纬度数据编码为一个字符串,主要用于底层应用或者调试,实际作用并不大 | geohash location cd |
georadius | 以给定的纬度为中心,返回指定地址空间中包含的所有位置元素中,与中心距离不超过给定半径。返回的时候还可以携带上额外的信息
命令中可以排序位置,可以通过以下两个参数,用户可以指定被返回位置元素的排序方式
注意:默认情况下,这个命令会返回所有匹配的位置元素,虽然可以使用count来指定获取前n个元素,但是命令内部可能会需要对所有匹配的元素进行处理,对一个非常大的区域进行搜索时,使用count取少量的元素也可能非常慢。 | georadius location 111.6 29.046 100 KM |
georadiusbymember | 这个命令与georadius是相同的,都是可以找出位于指定范围内的元素,但是这个命令的中心点是由位置元素给定的而不是单独再输入一个中心点 | georadiusbymember location cd 1000 KM |
其它 | 因为GEO 数据实际上被存储在有序集合(sorted set)中,所以我们一样可以使用Sorted set中的一些命令 | zrem location p1 p2:删除其中指定的元素 zrange location 0 -1:遍历元素 |
应用场景
Geospatial 的意义是地理位置,所以其主要应用地理位置相关的计算。如,微信中的“附近”功能,钉钉中的“距离打卡”...
五、发布/订阅操作
发布/订阅,也就是pub/sub,是一种消息通信模式。发布者也就是消息的生产者,生产和发布消息到存储系统当中,订阅者也就是消息的消费者,从存储系统中接收和消费消息。这个存储系统可以是文件系统FS,消息中间件MQ,数据库管理系统DBMS,也可以是Redis。整个消息发布者、订阅者与存储系统称为消息系统。
消息系统中的订阅者订阅了某类消息后,只要存储系统中存在这类消息,就可以不断地接收并消费这些信息。当没有这类消息后,订阅者的接收、消费会阻塞,当发布者把消息写入到存储系统后,立即会唤醒订阅者。当存储系统放满时,不同的发布者会有不同的处理方式,比如:阻塞发布者的发布,等待可用的存储空间或者是把多余的消息丢失。
不同的消息系统消息的发布/订阅方式也是不同的,如RocketMQ、Kafka等中间件构成的消息系统中,发布/订阅的消息都是以主题Topic分类的,而Redis当中则是以频道Channel分类的。
相关命令
命令 | 说明 | 示例 |
---|---|---|
subscribe | redis客户端通过一个subscribe命令可以同时订阅任意数量的频道,在输出了订阅主题后,命令处理阻塞的状态等待相关频道的消息。 返回订阅的结果报告 | subscribe news sports:订阅news和sports |
publish | redis客户端通过一条publish命令可以发布一个频道的消息,返回值是接收到这个消息的订阅者数量。 | publish news "this is a fun news" |
psubscribe | 订阅一个或多个符合给定模式的频道,这里可以指定模式,这个模式中只能使用*号,如:it* 表示的是可以匹配所有以it开头的频道,如:it.news,it.blog|,it.tech等都可以匹配到 | psubscribe it.* news.* |
unsubscribe | redis客户端退订指定的频道,如果没有指定具体的频道,那么客户端使用subscribe命令都会被退订,这个命令会返回一个信息,告知客户端所有被退订的频道。 | |
punsubscribe | 这个与unsubcribe类似只是说它退订的是带模式的频道。 | |
pubsub | 它是一个查看订阅与发布系统状态的内省命令集,它由数个不同格式的子命令组成
|
六、Redis事务
Redis的事务本质上是一组命令的批处理,这组命令在执行过程中会被顺序地,一次性全部执行完毕,只要没有出现语法错误,这组命令在执行期间是不会被中断的。
Redis事务特性
Redis的事务只保证数据的一致性。
- 不具备原子性,这组命令中的某些命令执行失败不会影响到其它命令的执行
- 没有复杂的隔离级别,这组命令使用乐观锁机制实现简单的隔离性
- 这组命令的执行结果是被写入到内存的,是否持久化取决于Redis的持久化策略,与事务无关
Redis事务实现
三个命令
Redis事务通过三个命令进行控制
muti:开始事务
exec:执行事务
discard:取消事务
基本使用
在multi执行后就会开启事务,在其后每写的一个命令都会放到队列当中,当执行exec的时候才会一个一个执行队列中的命令。
如果我们事务开启了并且命令也放到队列中了,但是我们此时执行了discard则会取消所有命令
exec与discard两者是互斥的。最后要么执行exec要么执行discard
以上的两个命令set age 20 与 incr age是一组,在执行过程中不会被中断
如果我们不执行exec,而是执行discard则会取消队列中的命令执行
Redis事务异常处理
语法错误
当事务中的命令出现语法错误时,整个事务在exec执行时会被取消。
因为在队列中的命令存在错误的语法,这个时候exec会取消执行会事务中的命令
执行过程中异常
如果事务中的命令没有语法错误,但是在执行过程中出现异常,这个异常不会影响到其它命令的执行
这里可以看到事务中的命令都是没有语法错误的,但是在执行的时候第二个命令出现的执行异常,这个时候第一个命令已经执行成功了,第二个不会执行。此时执行命令get score会返回A是因为这个命令执行成功了!
Redis事务隔离机制
为什么要隔离呢?
在并发的场景下可能会出现多个客户端对同一数据进行修改的情况。
示例如下:
假设我们有一个账户,里面有金额100元,A客户端要扣减80元,B客户端要扣减50元。首先他们会去看是否够扣减,两个并发执行查看到的金额是100,这对于两者来说都是够减的,于似乎两者都进行扣减金额的动作,这时候时候会出现账户“超扣”的情况。
为了解决这种情况,Redis事务通过乐观锁机制实现了多线程下的执行隔离。
隔离实现
Redis通过watch命令再配合事务实现了多线程下的执行隔离
我们先watch 一个key,再使用multi开事务
实际上我们watch account的时候,我们不仅看到account的值是100,而且可以看到有一个版本是1,当另一个客户端对这个值进行修改后,account中的版本号已经变为了2,此时在执行命令时会发现watch中的account的版本是1小于实际数据的版本2表示现在的版本已经过时了。执行时就会取消掉。
版本号要大于等于原数据的版本号时才能执行,否则不能执行。
七、benchmark测试工具
在Redis安装安成后会自动安装一个redis-benchmark测试工具,它是一个压力测试工具,用于测试Redis的性能。
Redis安装完成后这个具体在:/usr/local/bin/
通过redis-benchmark --help命令可以查看到其用法。
常用的一些命令选项:
- -c 表示并行连接的数量,默认是50
- -n 表示请求的总数量,默认值是100000
- -h 表示连接的Redis的ip,如查是本机可以省略
- -p 表示Redis的端口号,如果是6379则可以省略
- -d 表示数据的大小,get/set命令时其操作的value的数据长度,单位字节,默认值是3,这个测试其它命令的时候没有用
- -k 表示这些并发连接的状态,如果是1表示一直处于连接状态(keep alive);0表示中间会出现断连然后重连接的情况(reconnect),默认值是1
基本测试
命令:redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 200000
这个测试的命令表示使用100个客户端并发连接,总共发起200000个请求
测试的报告如下(每一个命令都是一样的报告结构,下面是set命令测试报告结果):
上在这个测试会把所有命令都测试一遍
指定命令测试
命令: redis-benchmark -t set,lpush,sadd -c 100 -n 100000 -q
上面这个命令中-n如果是100000表示是默认的请求可以省略不写;
其它的选项说明:
- -q 表示测试结果只给出总述性报告
- -t 指定要测试的命令,多个命令之间使用逗号进行分隔,不能有空格
通过benchmark的测试我们看到,redis的性能非常高,其中还是得益于它对五种值类型全部进行了设计,经过设计后变成了适合于它自己的结构。
相关文章:
04、Redis深入数据结构
一、简单动态字符串SDS 无论是Redis中的key还是value,其基础数据类型都是字符串。如,Hash型value的field与value的类型,List型,Set型,ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…...
zephyr移植到STM32
Zephy如何移植到单片机 1. Window下搭建开发环境1.1 安装Choncolatey1.2 安装相关依赖1.3创建虚拟python环境1.4 安装west1.4.1 使用 pip 安装 west1.4.2 检查 west 安装路径1.4.3 将 Scripts路径添加到环境变量1.4.4 验证安装 1.5 获取zephyr源码和[安装python](https://so.cs…...
Windows使用AutoHotKey解决鼠标键连击现象(解决鼠标连击、单击变双击的故障)
注:罗技鼠标,使用久了之后会出现连击现象,如果刚好过保了,可以考虑使用软件方案解决连击现象: 以下是示例AutoHotKey脚本,实现了调用XButton1用于关闭窗口(以及WinW,XButton2也导向…...
案例研究:UML用例图中的结账系统
在软件工程和系统分析中,统一建模语言(UML)用例图是一种强有力的工具,用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究,详细解释UML用例图的关键概念,并说明其在设计结账系统中的应用。 用…...
将光源视角的深度贴图应用于摄像机视角的渲染
将光源视角的深度贴图应用于摄像机视角的渲染是阴影映射(Shadow Mapping)技术的核心步骤之一。这个过程涉及到将摄像机视角下的片段坐标转换到光源视角下,并使用深度贴图来判断这些片段是否处于阴影中。 1. 生成光源视角的深度贴图 首先&…...
安卓漏洞学习(十八):Android加固基本原理
APP加固技术发展历程 APK加固整体思路 加固整体思路:先解压apk文件,取出dex文件,对dex文件进行加密,然后组合壳中的dex文件(Android类加载机制),结合之前的apk资源(解压apk除dex以外…...
前端数据模拟器 mockjs 和 fakerjs
功能:帮助前端生成随机数据,独立于后端单独开发 一、mockjs 安装:npm install mockjs 优点:官网是中文。 缺点:目前该库已经无人维护,也没人解决github上的bug。 官网 github地址 二、fakerjs 安装…...
Ruby语言的软件开发工具
Ruby语言的软件开发工具概述 引言 Ruby是一种简单且功能强大的编程语言,它以优雅的语法和灵活性而闻名。自1995年首次发布以来,Ruby已经被广泛应用于各种开发领域,特别是Web开发。随着Ruby语言的普及,相关的开发工具也日益丰富。…...
P8772 [蓝桥杯 2022 省 A] 求和
题目描述 给定 𝑛 个整数 𝑎1,𝑎2,⋯ ,𝑎𝑛 求它们两两相乘再相加的和,即 𝑆𝑎1⋅𝑎2𝑎1⋅𝑎3⋯𝑎1⋅𝑎𝑛&…...
(七)Linux库的串口开发
文章目录 基于官方提供的串口测试代码部分解析代码部分1. usage 函数2. opt_parsing_err_handle 函数3. sig_handle 函数4. init_serial 函数5. serial_write 函数6. serial_read 函数7. run_read_mode 函数8. run_write_mode 函数9. run_loopback_test 函数 进行测试第一步编译…...
【git】在服务器使用docker设置了一个gogs服务器,访问和现实都不理想
以下问题应该都可以通过设置custom/conf/app.ini来解决 配置文档参考地址:https://www.bookstack.cn/read/gogs_zh/advanced-configuration_cheat_sheet.md domain显示的事localhost,实际上应该是一个IP地址。 关键字: DOMAIN ROOT_URL 因为是docker…...
ubuntu报错:没有在该文件夹中粘贴文件的权限
1 现象: 近期给ubuntu扩展了硬盘,但是在其中进行文件操作时提示“没有在该文件夹中粘贴文件的权限” 2 原因: 新增硬盘挂载地址为“/home/username/data/” 终端输入 ls -ld /home/username/data/输出 drwxr-xr-x 3 root root 4096 1月…...
JavaWeb开发(六)XML介绍
1. XML介绍 1.1. 什么是XML (1)XML 指可扩展标记语言(EXtensible Markup Language)XML 是一种很像HTML的标记语言。 (2)XML 的设计宗旨是传输数据(目前主要是作为配置文件),而不是显示数据。 (3&a…...
Vue 3 和 Electron 来构建一个桌面端应用
我们将使用 Vue 3 和 Electron 来构建一个桌面端应用,该应用可以通过 Websocket 与服务器进行通信,并实现心跳检测、客户端上线、获取资产信息以及修改资产状态的功能。以下是实现步骤的概述: 项目结构:创建一个 Vue 3 项目&…...
Python中的asyncio:高效的异步编程模型
随着互联网应用的快速发展,程序的响应性和处理效率成为衡量系统性能的重要指标。传统的同步编程模型在面对高并发和IO密集型任务时,常常显得捉襟见肘,难以满足现代应用的需求。Python的asyncio库作为一种高效的异步编程模型,为开发…...
《解锁鸿蒙系统AI能力,开启智能应用开发新时代》
在当今科技飞速发展的时代,鸿蒙系统以其独特的分布式架构和强大的AI能力,为开发者们带来了前所未有的机遇。本文将深入探讨开发者如何利用鸿蒙系统的AI能力开发更智能的应用,开启智能应用开发的新时代。 鸿蒙系统构筑了15系统级的AI能力&…...
安卓OCR使用(Google ML Kit)
OCR是一个很常用的功能,Google ML Kit提供了OCR能力,用起来也很简单,本文介绍一下使用方法。 1. 相关概念 名词概念解释TextBlock块一个段落Line行一行文本Element元素单词;对汉字来说,类似"开头 (分隔符)中间&…...
使用redis的5种常用场景
文章目录 1. 缓存热点数据2. 分布式锁3. 计数器和限流器4. 消息队列5. 会话管理总结 在日常开发工作中,Redis作为一款高性能的内存数据库,凭借其强大的功能特性和卓越的性能表现,已经成为了许多项目中不可或缺的组件。本文将详细介绍Redis在实…...
Extreme670和440的DHCP和vlan划分
1.网关配置 防火墙 USG 添加静态路由,也就是回指路由192.168.0.0 255.255.0.0 192.168.100.2 usg关闭DHCP192.168.100.0段的,usg接口的网关地址是192.168.100.1,防火墙策略启用192.168.100.0段到wan1段的内网和外网的NAT地址转换。 2…...
VTK知识学习(33)-交互问题2
1、前言 主要是针对前面有过实现不了交互的情况进行说明,经过一些尝试和分析调用API,总算实现RenderWindowControl函数回调正常串接,当然这个移动处理事件的效果目前也没有确认。 2、使用 vtkImageReslice reslice vtkImageReslice.New();p…...
c++ thread线程join、detach、joinable方法
(621条消息) 线程中断Thread的interrupt()方法_thread interrupt_萝卜阿咕咕的博客-CSDN博客 C/C编程:std::thread 详解-CSDN博客 #include <iostream> #include <thread>void do_some_work() {std::cout<<"Hello Concurrent World\n"…...
Transformer:深度学习的变革力量
深度学习领域的发展日新月异,在自然语言处理(NLP)、计算机视觉等领域取得了巨大突破。然而,早期的循环神经网络(RNN)在处理长序列时面临着梯度消失、并行计算能力不足等瓶颈。而 Transformer 的横空出世&am…...
【Python】__main__.py、__init__.py
文章目录 1. __init__.py作用:用法:示例:特点 2. __main__.py作用:用法:示例:特点: 3. 综合示例总结: 1. init.py 作用: __init__.py 文件的主要作用是标识一个目录是一…...
springboot集成整合工作流,activiti审批流,整合实际案例,流程图设计,流程自定义,表单配置自定义,代码demo流程
前言 activiti工作流引擎项目,企业erp、oa、hr、crm等企事业办公系统轻松落地,一套完整并且实际运用在多套项目中的案例,满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器,流行的前后端…...
代码随想录算法【Day16】
Day16 513.找二叉树左下角的值 本题使用迭代法更简单,使用迭代法和递归法的区别是什么 递归法 目标就是找深度最大的叶子结点 无论前中后序遍历,都是左节点先被遍历到,所以一旦得到深度最大的节点,就是最后一行最靠左侧的节点…...
从光子到图像——相机如何捕获世界?
引言 你是否想过为何我们按一下相机快门就可以将眼前广袤多彩的世界显示于一个小小的相机屏幕上?本期推文中将带着大家重现从光子转换为电子、电子转换为图像中数字驱动值的整个流程。 ▲人们通过相机捕获眼前的场景 从光子到电子的转换 光线首先通过光学镜头进入相…...
Harmony开发【笔记1】报错解决(字段名写错了。。)
在利用axios从网络接收请求时,发现返回obj的code为“-1”,非常不解,利用console.log测试,更加不解,可知抛出错误是 “ E 其他错误: userName required”。但是我在测试时,它并没有体现为空,…...
Ubuntu 下载安装 elasticsearch7.17.9
参考 https://blog.csdn.net/qq_26039331/article/details/115024218 https://blog.csdn.net/mengo1234/article/details/104989382 过程 来到 Es 的版本发布列表页面:https://www.elastic.co/downloads/past-releases#elasticsearch 根据自己的系统以及要安装的…...
8. LINUX 用户和组
文章目录 8.1 密码文件:/etc/passwd1. 登录名(Login Name)2. 经过加密的密码(Encrypted Password)3. 用户 ID(User ID, UID)4. 组 ID(Group ID, GID)5. 注释(…...
vue监听中的watch监听(详解)
1、watch 选项用于监听数据的变化并执行相应的回调函数。watch 选项提供了两个重要的属性:deep 和 immediate。1.1、深度监听 (deep: true) 当你需要监听一个对象或数组内部的变化时,可以使用 deep: true。 这会使得 watch 监听器递归地监听对象或数组内…...
微信小程序中 隐藏scroll-view 滚动条 网页中隐藏滚动条
在微信小程序中隐藏scroll-view的滚动条可以通过以下几种方法实现: 方法一:使用CSS隐藏滚动条 在小程序的样式文件中(如app.wxss或页面的.wxss文件),添加以下CSS代码来隐藏滚动条: scroll-view ::-webkit…...
K8s Pod OOMKilled,监控却显示内存资源并未打满
1. 问题现象 pod一直重启,通过grafana查看,发现内存使用率并没有100%。 2. 排查过程 2.1 describe查看pod最新一次的状态 可以明显看到,最近一次的重启就是因为内存不足导致的。 2.2 describe 查看node节点状态 找到原因了,原来…...
对话|全年HUD前装将超330万台,疆程技术瞄准人机交互“第一屏”
2024年,在高阶智驾进入快速上车的同时,座舱人机交互也在迎来新的增长点。Chat GPT、AR-HUD、车载投影等新配置都在带来新增量机会。 高工智能汽车研究院监测数据显示,2024年1-10月,中国市场(不含进出口)乘用…...
【HTML+CSS+JS+VUE】web前端教程-10-列表标签之无序列表
无序列表实现 无序列表是一个项目的列表,此列项目使用粗体圆点(典型的小黑圆圈)进行标记 无序列表始于<ul>标签,每个列表项始于<li>标签。<ul><li>苹果...
基于V2X的无人机与特种车辆战地智能通信:技术融合与实战应用
一、引言 1.1 研究背景与意义 在现代战争的复杂环境中,通信系统的高效与可靠已然成为决定胜负的关键因素。随着军事技术的飞速发展,战争形态发生了深刻变革,作战空间不断拓展,从陆地、海洋、天空延伸至电磁、网络、太空等多维领…...
20250109下载JDK17的方法链接
20250109下载JDK17的方法&链接 2025/1/9 16:20 缘起:编译地面站应用程序QGC,需要安装QT和【旧版本的】JDK17。 当时在网上没有找到JDK17,就安装了比较接近的JDK21。反正最后的QT for Android最后就是没有编译通过。 到底是谁的问题&#…...
杭州铭师堂的云原生升级实践
作者:升学e网通研发部基建团队 公司介绍 杭州铭师堂,是一个致力于为人的全面发展而服务的在线教育品牌。杭州铭师堂秉持“用互联网改变教育,让中国人都有好书读”的使命,致力于用“互联网教育”的科技手段让更多的孩子都能享有优…...
chrome浏览器的更新提示弹窗无法更新Chrome解决方法
使用组策略编辑器 此方法适用于 Windows 系统且系统为专业版及以上版本,家庭版系统没有组策略功能。 按下Win R键,打开 “运行” 对话框,输入gpedit.msc并回车,打开组策略编辑器。 在组策略编辑器中,依次展开 “计算机…...
LLM prompt提示构造案例:语音回复内容;o1思维链
1、语音回复内容 目的: 语音聊天助手的prompt,让大模型来引导聊天内容,简短和友好,从而文字转语音时候也比较高效。 ## 角色设定与交互规则 ### 基本角色 你是用户的好朋友. 你的回答将通过逼真的文字转语音技术阅读. ### 回答规则…...
OceanBase 学习计划全攻略:开启分布式数据库探索之旅
《OceanBase 学习计划全攻略:开启分布式数据库探索之旅》 在当今数字化浪潮汹涌澎湃的时代,数据库作为企业信息存储与管理的核心基础设施,其性能、可靠性和扩展性至关重要。OceanBase 作为一款具有卓越分布式特性的国产数据库,正…...
Linux 虚拟机与windows主机之间的文件传输--设置共享文件夹方式
Linux 虚拟机与windows主机之间的文件传输 设置共享文件夹方式 在虚拟机中打开终端查看是否已经新建完成,到文件夹中找到它看一下,这个位置就能存储东西啦...
React Context用法总结
1. 基本概念 1.1 什么是 Context Context 提供了一种在组件树中共享数据的方式,而不必通过 props 显式地逐层传递。它主要用于共享那些对于组件树中许多组件来说是"全局"的数据。 1.2 基本用法 // 1. 创建 Context const ThemeContext React.createC…...
Linux好用软件
力荐软件 apt-fast:更快速的软件管理安装过程会进入一个图形界面,配置线程数等信息,全部默认即可 sudo add-apt-repository ppa:apt-fast/stable sudo apt-get update sudo apt-get -y install apt-fast 以后安装应用,把apt-get直接替换成apt-fast即可,例如安装vlc sudo…...
【MYSQL】
文章目录 1.DDL 1.DDL --添加字段 ALTER TABLE table_name add COLUMN embed_model VARCHAR(32) NOT NULL COMMENT 名称备注 COLLATE utf8mb4_bin AFTER config_code;--修改字段 ALTER TABLE table_name CHANGE COLUMN column_a column_b VARCHAR(500) NOT NULL COMMENT 配置信…...
webrtc之rtc::ArrayView<const uint8_t>
rtc::ArrayView<const uint8_t> 是 WebRTC(或其他基于 rtc 命名空间的库)中常见的一个类型,它通常用于表示一块 只读的内存区域,该内存区域由一系列 uint8_t 类型(无符号 8 位整数)元素组成。 1. rt…...
深入理解 MySQL 的 EXPLAIN 工具
1. 什么是 EXPLAIN 工具? EXPLAIN 是 MySQL 中用来分析 SQL 查询执行计划的命令,它能够显示查询在执行时会如何访问表、使用哪些索引、扫描多少行等信息。通过 EXPLAIN 工具,开发者可以直观地了解查询的执行过程,从而进行针对性的…...
谷歌Google、紫鸟浏览器插件开发
对于跨境电商行业的IT部门来说,经常需要获取各种店铺相关数据,但是仅靠官方提供的接口来获取数据远远不够,这个时候我们就需要插件或者RPA的方式来获取数据。 以下是关于自研紫鸟插件的简单demo,紫鸟浏览器使用的是火狐和谷歌的插…...
HTML 显示器纯色亮点检测工具
HTML 显示器纯色亮点检测工具 相关资源文件已经打包成html等文件,可双击直接运行程序,且文章末尾已附上相关源码,以供大家学习交流,博主主页还有更多Html相关程序案例,秉着开源精神的想法,望大家喜欢&#…...
Win32汇编学习笔记09.SEH和反调试
Win32汇编学习笔记09.SEH和反调试-C/C基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net SEH - structed exception handler 结构化异常处理 跟筛选一样都是用来处理异常的,但不同的是 筛选器是整个进程最终处理异常的函数,但无法做到比较精细的去处理异常(例如处理…...
计算机组成原理(九):乘法器
乘法器原理 乘法器的工作原理可以用二进制乘法来说明。二进制乘法和十进制乘法类似,通过部分积的累加得到结果。 部分积的生成 在二进制乘法中,每一位的乘积是两个二进制数位的 与运算(0 0 0,1 0 0,0 1 0&…...