当前位置: 首页 > 科技观察

图解 Redis String 底层数据结构 SDS 与计数器实战

时间:2023-03-16 14:16:37 科技观察

RedisString底层数据结构示意图SDS与计数器实战Sortedcollectionsbasedonrangequeries),Bitmap(位图),HyperLogLog,Geospatial(地理空间),andStream(流)等数据类型。接下来重点介绍String数据类型的使用技巧和使用场景,以及String数据类型的底层数据结构原理。数据类型的使用技巧和每种数据类型的底层实现原理是你打好核心基础的必经之路,所以要好好练习。打好基础,练好心态,让你的程序更快,把内存节省到极致。2.1.1String(字符串)1.什么类型的字符串使用最广泛,比如计数器,缓存,分布式锁,用于登录后存储用户信息,key=token,value=Java对象序列化成JSONString。按照以下说明进行操作。SETuser:token:666{"name":"哥码","gender":"M","city":"shenzhen"}接下来带大家了解String类型,底层数据结构和用法场景。?MySQL:“你们都是C语言开发的,C语言已经有字符串了,你吓唬谁。”能把pattern放开一点吗?我没有直接使用C语言的字符串,而是创建了一个SDS结构来表示字符串。SDS的全称是SimpleDynamicString,中文称为“简单动态字符串”。?MySQL:《做SDS的目的是什么?》字符串是应用最广泛的,我需要保证它们能够支持丰富高性能的字符串操作函数,同时保存二进制数据,节省内存占用。实现您的领导经常向您提出的目标,这些目标既必要又重要。首先看一下C语言字符串数组的结构。例如,通过char*s="MageByte"定义一个字符串变量。注意数组的最后一个字符串是“\0”,表示字符串结束。之所以设计SDS,是因为C语言标准库string.h中的字符串存在以下不足。C语言使用char*字符串数组来实现字符串,创建字符串时需要手工检查和分配字符串空间。由于没有length属性来记录字符串的长度,所以要想得到字符串的长度,就得从头遍历到\0,这对于唯快不破的我来说是无法容忍的。无法实现“安全二进制存储”:无法保存图片等二进制数据。不能存储特殊字符\0,因为\0表示C语言字符串的结尾。字符串扩缩容:char数组的长度在创建字符串的时候就确定了。如果要添加数据,需要重新申请一个空间,复制追加的字符串内容,然后释放旧的空间。很耗资源。2.培养心法?MySQL:“说说SDS结构,你是怎么解决这些问题的。”为了存储字符串的实际内容,我需要有一个char类型的数组来存储,用一个inttypelen字段记录char数组使用了多少字节。另外,必须有一个int类型的allocfield记录分配的char数组的总长度,alloc-len等于char类型的buf数组未使用的字节数(Redis7.0去掉了表示数量的free字段未使用的字节数)。SDS也遵循C字符串以空字符开头以“\0”结尾的约定,保存的空字符的大小不计入SDS的len属性。另外,在字符串末尾添加空字符串“\0”等操作,都是由SDS函数自动完成的。O(1)获取字符串长度的时间复杂度。SDS中的len保存了字符串的长度,实现了O(1)的时间复杂度来获取字符串的长度。有没有注意到SDS结构体有一个flags字段,表示SDS的类型。其实SDS一共设计了5种类型,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32和sdshdr64,区别在于数组的len长度以及分配的空间长度alloc。例如,sdshdr8。结构__attribute__((__packed__))sdshdr8{uint8_tlen;uint8_t分配;无符号字符标志;字符缓冲区[];};len和alloc字段都是uint8_t类型,int在Java中是32位,但在C语言中是不同长度的int值,uint8_t是占8位的unsignedint值,可以表示的最大值为2^8-1,所以其buf数组的最大长度为2^8-1。之所以这样设计内存节省,是为了使用不同的SDS类型来保存不同大小的字符串,从而节省内存的使用。?MySQL:"HowbigastringcanSDSstore?"alloc表示当前sds结构允许的最大字符长度。比如uint32_talloc的取值范围是0~2^32=4294967296。理论上一个char数组的最大长度是4294967296。一个char字符占一个字节,可以存储4G,更何况是sdshdr64。这些是理论值。事实上,Redis内部将最大字符串长度限制为512M。编码格式我也使用了三种编码格式来存储String数据,分别是int,embstr,raw,可以通过OBJECTencodingkey查看对象使用的编码类型。编码选择过程如图2-3所示。int编码,8字节长整型,值为数字类型且数字长度小于20embstr,字符串小于等于44字节。字符串大于44个字节。?MySQL:“什么是__attribute__((__packed__))?”这是我为了节省内存空间而采用的一种特殊的编译器优化方法。作用是告诉编译器不要使用字节对齐,而是以紧凑的方式分配内存。默认情况下,编译器将按8字节对齐分配内存,即使此变量的大小小于8字节。__attribute__((__packed__))用于定义结构体,编译器会根据实际占用分配内存空间。二进制安全SDS不仅可以存储String类型数据,还可以存储二进制数据。SDS不使用“\0”判断字符串结束,而是使用len标志结束,所以可以直接存储二进制数据。空间预分配不仅分配所需的空间,还可以在需要扩展SDS空间时分配额外的未使用空间。通过预分配策略,减少了执行字符串增长所需的内存重新分配次数,减少了字符串增长操作带来的性能损失。Lazyspacerelease当缩短SDS时,程序不会回收多余的内存空间。如果后面需要进行append操作,则直接使用buf数组alloc-len中未使用的空间。通过惰性空间释放策略,避免了减少字符串所需的内存重新分配操作,优化了以后的增长操作。3.实战:分布式ID生成器相信大家会经常遇到需要生成唯一ID的场景,比如标识每个请求,生成订单号,创建用户需要创建用户ID。分布式ID生成器需要满足以下属性。要想分治、二分法搜索,就必须实现有序性的单调递增。此外,MySQL是最常用的数据库。B+树为了维护ID的顺序,会频繁的在索引中间插入并移动后续节点的位置,甚至会造成频繁的分页。这对性能的影响是巨大的。全局唯一性,如果ID不唯一,会出现主键冲突。高性能,ID生成属于高频操作,如果性能慢,系统整体性能会受到限制。高可用性,即在给定的时间间隔内,系统的总可用时间所占的比例。存储空间小。使用MySQL的InnoDBB+树,普通索引(非聚集索引)会存储主键值。主键越大,每个Page能存放的数据就越少,访问磁盘I/O的次数就会增加。Redis集群可以保证高可用和高性能。为了节省内存,ID可以是数字的形式,可以增量创建新的ID。为防止重启后数据丢失,还需要开启RedisAOF持久化。?MySQL:“如果开启AOF持久化,为了性能设置everysec策略,可能还是会丢失一秒的数据,所以也可以使用异步机制,将生成的最大ID持久化到一个MySQL中。”好主意,发生成ID后向MQ消息队列发送消息,并将该值持久化到MySQL中。我提供了INCR指令,它可以将存储在密钥中的数字加1,并将其返回给客户端。如果key不存在,则先将key的值初始化为0,然后加1返回给客户端。该指令的值限于64位有符号数。设计思路假设订单ID生成器的key为“counter:order”。当应用服务启动时,首先从数据库中查询最大值M。执行EXISTScounter:order判断key是否存在。Redis中不存在key"counter:order",执行SETcounter:orderM将M的值写入Redis。Redis中有一个key“counter:order”,值为K,然后比较M和K的值,执行SETcounter:ordermax(M,N)将最大值写入Redis,如果是等于,不操作。应用服务启动后,每次需要生成ID时,应用都会向Redis服务器发送INCRcounter:order命令。应用程序将获取到的ID值发送给MQ消息队列,消费者监听队列更新值给MySQL。String类型和底层存储原理的实战到此结束。接下来我会继续介绍其他数据类型的底层存储原理和实战。