【redis基础】-redis的基本数据类型以及一些内部编码优化

redis作为一个内存数据库,优化存储、减少内存使用空间显得尤为重要,首先,作为redis的使用者,我们可以对键值人工优化,比如对于键的起名,可以使用缩略词进行标注,这样既可以节省空间又易懂,再比如,redis提供了四个命令可以直接操作二进制位,位操作命令可以非常紧凑的存储布尔值,当一个网站需要存储100万个用户的性别的时候,我们就可以使用位操作记录,这样只需要占用100KB多的空间!

同时,redis自身也作出了存储优化,那就是内部编码优化。首先,redis为每种数据类型都提供了两种编码方式(redis一共五种数据类型),可以使用命令OBJECT ENCODING k 来查看编码方式;

redis中,每一个键值都式使用一个redisObject结构体存储(redis是使用c写的),其结构如图所示:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第1张图片

其中:

1. type:用来标明键值的数据类型,它有如下几种取值:

              REDIS_STRING 0

              REDIS_LIST 1

              REDIS_SET 2

              REDIS_ZSET 3

              REDIS_HASH 4

    也就是对应这redis中的五种数据类型;

2. encoding:表示的是键值再redis内部的编码方式,编码方式一共有九中:

              REDIS_ENCODING_RAW  0   (字符串)

              REDIS_ENCODING_INT  1   (字符串)

              REDIS_ENCODING_HT  2    (散列、集合)

              REDIS_ENCODING_ZIPMAP  3   

              REDIS_ENCODING_LINKDELIST  4   (列表)

              REDIS_ENCODING_ZIPLIST  5   (散列、列表、有序集合)

              REDIS_ENCODING_INTSET  6   (集合)

              REDIS_ENCODING_SKIPLIST  7   (有序集合)

              REDIS_ENCODING_EMBSTR  8   (字符串)

    括号中的是可以采用该种编码的数据类型。

3. ptr:一个指针,指向的是一个变量,这个变量是sdshdr类型,用来存储字符串,这个sdshdr变量定义如下:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第2张图片

    len表示的是字符串的长度,free表示的是剩余空间,buf是字符串本身的内容(redis中字符串就是存储再这);

4. refcount:记录了键值被引用的数量

下面我们就开始分析分析redis内部对数据编码的优化策略吧!

1. 首先,是字符串类型,字符串类型是redis中最基本的数据类型,它可以存储任何形式的字符串,包括二进制数据,它可以用来存储图片、用户邮箱、JSON化的对象,其最大容量为512MB,还有一点需要大家了解,那就是字符串类型是其他四种类型的基础,可以说,其他四种数据类型就是以不同的形式组织字符串,其常用命令有:

        SET K V :赋值操作

        GET K :根据key获取value

        APPEND K V :向value追加数据

    当存储的是整形的数据时,可以使用:

        INCR K:value增1(i++操作)

        DECR K:value减1

        INCRBY K n:增加指定数值

       DECRBY K n:减少指定数值

    还有四个命令进行位操作:

        GETBIT K offset:获取一个字符串指定位置的二进制位的值(超出索引返回0)

        SETBIT K offset value:设置指定位置的二进制位的值(返回的是原来的值,若位置不存在,返回0)

        BITCOUNT K [start] [end]:获取字符串中1的个数

        BITOP operation destkey K [K1...]:对多个字符串数据进行运算(支持的运算有OR  AND  XOR  NOT)

介绍了字符串基本的操作命令,那么我们就来看看其内部的工作原理吧,redis对字符串主要有两种优化。

    首先,Redis中是使用上文提到的 sdshdr类型的变量存储字符串数据,而ptr指针指向的就是这个变量,我们举个例子,比如现在有一个字符串str,那么如果按照embstr编码方式,它所占用的内存就是redisObjecy+sdshdr+strlen(str),但是,当value的内容可以用一个64位整数表示的时候,此时redis就会对字符串进行int编码优化,它会将字符串转换成long类型存储再ptr指针中,此时占用内存空间就是redisObjetc,做个测试,我现在向redis中存储两条数据:  name:jason   age:24,然后看看他们的编码方式分别是什么:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第3张图片

    另外,再Redis启动的时候,它会预先建立10000个分别存储0~99这些数字的redisObject类型变量作为共享变量,如果你存储的一个字符串数据再它的范围之内,那么它将引用共享对象而不会创建新的redisObject,也就是说,现在redis只需要存储键名和一个共享对象引用就可以了,键值占用的空间是0。所以,碎玉使用字符串来存储id这种小数字是非常节省空间的。

    注意:1. 通过配置文件参数maxmemmory设置了redis的最大空间大小后,redis将不会自动创建共享变量。

                2. 例子中的embstr编码方式是redis3.0新加入的,它与raw编码方式极其相似,不同的是,embstr编码方式会将redisObjetc结构体与sdshdr变量分配在连续的内存空间,这样做的好处是:由于分配的内存块是连续的,使得不论是内存的分配还是释放,所需要的操作都由原来的两次减少到一次,同时,连续的 内存可以让操作系统的缓存更好的发挥作用。当数据大小不超过39字节的时候,会使用embstr编码方式,当embstr编码方式的数据进行疼和修改操作的时候,数据都会自动变成raw编码方式。

2. 散列类型,散列类型就是java中的HshMap类型,它是一种字典结构,它存储的是字段与字段值的映射,需要注意的是,散列类型不能前台其他数据类型,也就是说,字段值只能是字符串类型(redis中的所有数据类型都不支持嵌套)。下面我们来简单介绍一下散列数据类型:

    散列数据类型相对于传统的关系型数据库,看他有很大的优势,比如,我现在要存储几个book对象,那么再关系型数据库中我就要建立一张book表,包括booknam、bookprice、bookauthor桑格字段,现在想为id是1的book对象增加一个出版日期,那么对于其他book对象,这个日期字段就是一种空间浪费,再后来,数据越来越多,不同的对象需要不同的属性的时候,表的字段会越来越多,难于维护,同时,在修改表的结构的时候,必须中断服务。而在redis的散列中,对于book对象+三个属性的结构,redis不会强行要求每条记录都依据这种结构,我们可以完全自由地增减字段而不影响其他记录。如图所示,我存储了两个book对象,第一个有三个字段,第二个有四个字段:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第4张图片

【redis基础】-redis的基本数据类型以及一些内部编码优化_第5张图片

    散列类型主要有一下集中常用命令:

         HSET K field value:存储一个字段和字段值

         HGET K field:获取一个字段内容

         HMSET K field value [field value ...]:存储多个字段和字段值

         HMGET K field [field ...]:获取多个字段值

         HGETALL K:获取所有的字段与对应字段值

         HEXISTS K field :判断字段是否存在(存在返回1,否则返回0)

         HSETNX K field value:当字段不存在的时候赋值(相当于先判断再赋值,不过这条命令是原子操作,没有竞争问题存在)

         HDEL K field [field...]:删除多条字段

         HKEYS K:只获取k的所有字段名

         HVALS K:只获取k的所有字段值

    散列类型的数据有两种编码方式:分别是:REDIS_ENCODING_HT(ht)和 REDIS_ENCODING_ZIPLIST(ziplist),两种编码方式是根绝配置文件设定的规则进行切换,打开.conf文件,找到如图所示的地方:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第6张图片

可以看到两条数据,分别是hash-max-ziplist-entries 512  和 hash-max-ziplist-value 64,它的意思就是当字段个数少于hash-max-ziplist-entries并且每个字段名和字段值 的长度都小于hash-max-ziplist-value(单位是字节)的时候,就会触发ziplist编码,否则使用ht编码,redis的散列数据每次更变都会进行判断。

首先,对于ht编码方式,字段(注意不是键)和字段值都是使用redisObject进行存储的,因此,对于字符串类型的编码优化对也使用与散列的字段与字段值的优化。

ziplist编码方式是一种紧凑的编码格式,牺牲性能换取空间的思想,因此使用与元素较少的情况,它的结构如图所示:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第7张图片

简略的画了一下,他主要由五部分组成,整个表的最上面是zlbytes,他记录的是表所占据的空间大小,zltail表示到最后一个元素的偏移量(可以直接定位到尾部无需遍历),zllen指的是存储元素的数量,元素是以特定的结构存储的字段名以及字段值,最后zlend是一个值位255的单字节标识。其编码优化主要发生再元素内部,我们再来看看每个元素内部的结构:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第8张图片

可以看到,每个元素是由四个部分组成的,第一个部分记录前一个元素大小,以实现倒序查找,当前一个元素大小小于254字节的时候,占用1个字节,否则占用5个字节;第二部分记录元素编码方式,第三部分记录元素大小,二三部分共同进行编码优化,具体策略是:

      元素大小:  <= 63字节    两部分占1字节

                            >63字节 & <=16383字节   两部分占2字节

                            >16383字节   两部分占5字节

第四部分是记录元素内容,他又自己的优化策略,如果内容可以转换成数字的话,则会进行转换来节省空间。

观察上面两张图可以知道,散列再使用ziplist进行编码的时候,字段与字段名是按顺序存储再元素中的。

从性能上讲,ziplist的性能比较差,再查询数据的时候,从第一个元素开始,每次跳过一个元素(略去字段值),因此,查找一个元素需要对表进行遍历,效率比较低,因此,再修改配置文件的时候,建议不要将参数hash-max-ziplist-entries   和 hash-max-ziplist-value设置的过大。

3. 列表类型,列表类型就是List数据类型,它的特点是有序可重复,其内部是使用双向链表实现的,因此,你可以从头部或者尾部对数据进行操作,在命令里面,分为左侧操作和右侧操作,其时间复杂度是Q(1),两端的数据获取的快,同时,通过索引获取数据就很慢。对与实际开发中的应用场景,比如在一些社交网站上,人们只关心最新的内容,使用列表数据类型进行存储,即使新鲜事的总数达到了上千万个,获取其中最新的100条数据也是极快的,他额的另外一个左右就是实现队列。同时需要注意的就是,散列类型和列表类型对于最大字段数量是一样的,都是(2^23)-1个元素。列表类型由如下几种常用的命令:

         LPUSH key value [value...]

         RPUSH key value [value...]

上面两个命令是向列表中添加数据,我们上面说过,这种数据类型是双向的,所以它可以对列表的两侧进行操作,借助于它的命令,我们不妨将他的两侧分成左端和右端,因此,以上两个命令就可以理解为LPUSH就是向左侧添加数据,RPUSH就是向右侧添加数据,这两个命令的返回值就是列表的长度。如图所示我建立了两个列表类型的数据分别是names和numbers,names是使用lpush,numbers是使用rpush:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第9张图片

我们以lpush为例,我们可以将列表想象成一个横放在你面前的子弹夹,开口在左端,你每次使用lpush存放一个数据都类似于向子弹夹中添加一个子弹,新的子弹会不断地从左侧进入弹夹,同时将已经进去的子弹向右侧顶,这样,最后添加完成了,最早添加的子弹应该是在最右侧。

        LPOP key

         RPOP key

这两个命令就是分别从左右两侧取数据,我们依然可以将它理解成子弹夹,只不过是卸子弹的过程,LPOP的时候,是从左侧取数据。

         LINE key

         LRANGE key start stop

         LREM key count value

         LTRIM key start stop

上面三个命令也十分常用,同时需要注意的地方也较多。LINE是获取列表的长度,也就是元素个数,虽然它与SQL语句的SELECT COUNT(*)类似,但是它的时间复杂度是O(1),使用的时候直接读取现成的值,无需遍历;

LRANGE是截取列表片段,索引是从0开始,这里面需要注意的由一下两点:首先,这个命令不会像LPOP或者RPOP那样删除列表数据,只是读取,另外一点就是截取范围是包括最右侧的数据的,LRANGE还有一个非常常用的技巧,就是将索引设置成0 -1,则会返回所有元素。另外,LRANGE支持负索引,当你的命令参数是负数的时候,标识从右侧开始计算,-1是最右侧的元素,-2标识右侧第二个元素,但是在写的时候,LRANGE的start位置永远在stop位置的左侧!否则返回空。

下一个LREM命令就是删除列表中的指定数据,他的意思就是会删除列表前count个元素中值是value的元素,注意:当count大于0的时候,是从左侧开始计算,count小于0的时候是从右侧开始计算,count如果等于0,则删除全部数据。

最后一个LTRIM就死截取字符串的意思,相当于java中的trim()方法,这个没什么好说的。

         LINDEX key index

         LSET key index value

INDEX命令用于返回指定索引的元素,索引从0开始,如果是负数则是从右侧开始计算。LSET命令则是将指定索引位置的元素内容替换成value,注意这是替换,原来的内容会删除。

        LINSERT key BEFORE|AFTER pivot value

这个命令看起来有些复杂,它的作用就是像列表的指定数据前后插入新的数据,它的规则是这样的:首先,他会在列表中从左到右寻找值为pivot的元素,然后根据参数2来确定是在这个元素前面插入还是后面插入。返回值是插入后列表元素的个数。我们举个例子:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第10张图片

可以看到,numbers里面一共由五个数据,最左边的是5,最右边的是1,我先后存储了两个数据在3的两侧,可以看到after是指目标元素的右侧,before是指目标元素的左侧。

最后一个命令:

       RPOPLPUSH source destination

这是一个用于转移数据的命令,它的作用就是将元素从一个列表中转移到另一个列表中,我们可以对他的动作进行拆分:首先,在原始列表中执行RPOP命令,弹出一个元素,然后LPUSH将这个元素加入到目标列表的左侧,返回值是被操作的元素,举个例子就看明白了:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第11张图片

可以看到,我准备了两个list,list1:d c b a   list2:4 3 2 1,我的操作就是将list1中的右侧数据转移到list2的左侧,结果可以看出,最右侧元素a已经跑到了list2的最左侧。这个命令可以实现在多个队列中传递数据;当目标列表与源列表相同时,会不断的将队尾数据移动到队首。

好了列表数据类型简单的介绍了一下,下面开始说说redis对他的编码优化吧。列表类型有两种编码方式,分别是 REDIS_ENCODING_LINKDELIST和REDIS_ENCODING_ZIPLIST,与散列类型一样,在配置文件中,依然可以定义REDIS_ENCODING_ZIPLIST的编码时机,这里不再说了。首先REDIS_ENCODING_LINKDELIST编码方式即双向链表,链表中的每个元素都是使用redisObject类型存储的,锁以他的优化方式与String类型一样;而对于好了列表数据类型简单的介绍了一下,下面开始说说redis对他的编码优化吧。列表类型有两种编码方式,分别是 REDIS_ENCODING_LINKDELIST和REDIS_ENCODING_ZIPLIST编码方式,其具体表现与散列一样。对于redis后来增加的编码方式:REDIS_ENCODING_QUICKLIST,他就是俩个中编码方式的结合版本,其原理就是将一个长列表分割成若干个以链表形式组织的ziplist,从而达到减少占用空间的同时提升ziplist编码性能的效果。

4. 集合类型

集合类型相当于java中的set,无序不重复,其内部是使用值为空的散列实现的,所以它的时间复杂度都是O(1),集合之间可以进行常规的运算(并集、交集等)。集合常用的命令简单的介绍一下:

        SADD key member [member...]

        SREM key member [member...]

这两个命令很简单,就是向集合中添加或者删除元素,如果键不存在则会自动创建,返回值是操作成功的数量,同时,如果存储的元素已经存在,由于集合类型是不重复的,所以会忽略,如图所示:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第12张图片

srem就是删除操作,可以一次性删除多个数据,返回删除成功的元素的个数,如果元素不存在则自动忽略。

        SMEMBERS key

这个命令是获取几个所有元素,我们接着上面的set2继续操作:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第13张图片

                 SISMEMBER key member

这个命令是判断一个元素是否存在与这个集合存在的时候返回1,不存在返回0.

        SDIFF key [key...]

        SINTER key [key...]

        SUNION key [key...]

这三个命令式进行集合之间的运算的,SDIFF命令是差集运算,什么差集呢,举个例子,我有{一个香蕉,一个苹果,一个西瓜}(set不重复),你有{一个苹果,一个桃子},那么我 - 你 = {西瓜,香蕉},ok,我们就以这个使用一下命令看看:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第14张图片

很好理解吧,需要注意的是它支持多多键计算,计算顺序是从左到右;

下一个命令SINTER,它是交集运算,就是两个集合的公共部分,还是用上面的例子:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第15张图片

我和你都拥有的水果就是apple,这个命令也支持多键运算;

SUNION命令是并集运算,就是将两个集合的元素合并起来,去除想用的元素后的集合,看例子:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第16张图片

        SCARD key

这个命令是获取一个集合中的元素个数;

        SDIFFSTORE destination key [key...]

        SINTERSTORE destination key [key...]

        SUNIONSTORE destination key [key...]

进行集合运算并将结果存储在destination键中,举个例子:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第17张图片

        SRANDMEMBER key [count]

从集合中随机获取count个不同的元素,如果count为负数的时候,会默认取count的绝对值;

        SPOP key

从集合中随机弹出一个元素,弹出后这个元素将不存在与集合。

集合类型的编码方式有两种:REDIS_ENCODING_HT和REDIS_ENCODING_INTSET,转换机制就是当集合中的所有元素都是整数并且元素个数小于配置文件中的

set-max-intset-entries参数指定的数的时候会使用REDIS_ENCODING_INTSET编码方式,否则使用REDIS_ENCODING_HT来存储。

首先,REDIS_ENCODING_INTSET编码方式中定义了一个结构体intset:

 

【redis基础】-redis的基本数据类型以及一些内部编码优化_第18张图片

 

其中,我们集合中的数据实际上就是存储在contents里面,其占用空间大小与具体的编码方式有关,默认的encoding是INTSET_ENC_INT16(2字节),如果增加的元素超过两个字节,那么redis自动将其升级为INTSET_ENC_INT32(4字节),并调整整个集合的位置与长度,还可以升级到8字节;需要注意的是,REDIS_ENCODING_INTSET编码方式是进行有序存储的,因此在smembers输出的元素是有序的,因此也就可以使用二分法查找元素,性能会随着元素的增加而变差。

注意,一旦变成了INTSET编码,redis不会自动的还原成原来的编码方式。

5. 有序集合

顾名思义,它与集合类型的区别就在于是否有序,有序集合在集合的基础上新增加了一个分数,为每个元素关联这个分数后,可以根据分数排序,注意:有序集合依然是不允许集合元素相同,但是分数可以相同!起常用命令如下:

        ZADD key score member [score member...]

该命令就是增加元素,如果元素已经存在,则会对分数进行替换,返回值是新增元素的个数(不包含已经存在的元素),同时,分数支持双精度浮点数

【redis基础】-redis的基本数据类型以及一些内部编码优化_第19张图片

这是插入完数据后的class信息,可以看到每一个元素都有一个关联的分数。

        ZSCORE key member

获取元素的分数

        ZRANGE key start stop [winthscores]

        ZREVRANGE key start dstop [WITHSCORES]

ZRANGE命令会在索引范围中按照元素的分数从小到大返回元素,支持使用0 -1方式返回全部。后面的withscore可以获取元素的同时加上分数

【redis基础】-redis的基本数据类型以及一些内部编码优化_第20张图片

如图,输出的元素不仅有顺序,而且还有分数。 ZREVRANGE命令则是以从大到小的方式排序,其他与zrange一样。

        ZRANGEBYSCORE key  min max [withscores] [limit offset count]

这个命令的作用就是按照从小到大的顺序获取指定范围分数的元素,min 和max就是范围遍及额,默认是包含边界的,但是可以使用“(”来忽略边界元素,举个例子:

【redis基础】-redis的基本数据类型以及一些内部编码优化_第21张图片【redis基础】-redis的基本数据类型以及一些内部编码优化_第22张图片

如图所示,我查询的是85到95之间的数据,可以看到结果从大到小输出出来,并且我在第二次查询的时候使用了withscores来连带输出元素关联的分数。

需要注意的是,使用limit offset count,他的作用就是将返回的结果从第offset个元素开始截取count个,比如,我现在用zrange获取了四个元素1,2,3,4,如果语句后面有limit 1 2,那么就是从索引是1的元素开始,获取前2两个元素就是2,3,索引从0开始!这里就不举例了。

        ZINCREBY key increment member

这个命令的作用就是增加某个元素的分数,返回值是增加后的分数。increment可以使负数,就是减的意思。

        ZCARD key 

        ZCOUNT key min max

ZCARD 获取集合中元素的数量

ZCOUNT则是获取分数指定范围内的元素的个数

        ZREM key member [member...]

删除一个或者多个元素,返回值是成功删除的元素的个数,如果不加元素,则会将key中所有元素删除。

        ZREMRANGEBYRANK key start stop

        ZREMRANGEBYSCORE key min max

ZREMRANGEBYRANK 删除排名范围内的元素,它首先会对元素进行从小到大的排序,然后按照给定的索引范围进行删除,索引从0开始。返回值是删除元素的数量。

ZREMRANGEBYSCORE 则是根据分数范围来删除范围内的元素。

        ZRANK key member

        ZREVRANK key member

ZRANK 命令按照分数从小到大的顺序获取元素的排名

 ZREVRANK则是从大到小

有序集合的编码方式有两种:REDIS_ENCODING_SKIPLIST 和 REDIS_ENCODING_ZIPLIST,ziplist的启动时机也是在配置文件中设置,其按照“元素1的值”,“元素1的分数”,“元素2的值”,“元素2的分数”...的结构存储的。其优化规则与散列、列表类型一样;

使用SKIPLIST编码方式的时候,redis会使用散列表与跳跃列表两种数据结构存储有序集合,其中散列表用来存储元素与分数之间的映射关系,跳跃列表则用来存储元素的分数到其元素之的映射以实现排序功能,采用此种编码方式的时候,元素值是用redisObject存储的,优化方式与string一样,元素的分数是使用double类型存储的。

 

你可能感兴趣的:(redis基础,linux,redis,数据类型,编码优化)