Redis 简介 Redis 是完全开源免费的,用C语言编写的,遵守BSD开源协议,是一个高性能的支持网络、基于内存、(key/value)分布式内存数据库,并支持持久化的NoSQL内存型数据库,并提供多种语言的API支持。

- Redis 的九种数据结构及底层内部编码 Redis支持多种数据结构,包括String(字符串)、Hash(哈希表)、List(链表)、Set(集合)、Zset(有序集合)、bitmap(位图)、GEO、Hyperloglogs、Streams(Redis5.0引入)等。我们之前经常说的这几种结构知识对外的编码,对外的表现形式,实际上以上的每种数据结构都对应着底层的内部编码实现,有的是一种结构对应着多种编码实现,这些底层编码实现包括SDS简单动态字符串(int、raw、embstr )、intset(整数集合)、skiplist(跳表)、ziplist(压缩列表)、hashtable等内部编码。如下图所示




redis key的设计规则 redis key值设计_字符串


数据结构与内部编码对应图

Redis为什么这样设计呢?或者说他这样设计有什么好处? 1:这样可以在不改变外部结构和操作命令的情况下,改变内部编码结构对用户无感,假如后期有更加优秀的内部编码的时候直接替换即可,说到这里你是否想到一种设计模式呢?对的就是门面设计模式。 2:一种数据结构对应多种内部编码,这样可以用户存的数据进行选择合适的内部编码结构,这样可以让Redis发挥出最大的性能,在不同场景下发挥各自的优势。

  • String的应用场景及内部编码 应用场景:可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。这也是我们经常用到的类型之一,存储简单的键值对信息。同时也适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。 内部编码:由上图可知,String的3种内部编码分别是:int、embstr、raw。int类型很好理解,当一个key的value是整型时,Redis就将其编码为int类型(另外还有一个条件:把这个value当作字符串来看,它的长度不能超过20)。如下所示。这种编码类型为了节省内存。Redis默认会缓存10000个整型值(#define OBJSHAREDINTEGERS 10000),这就意味着,如果有10个不同的KEY,其value都是10000以内的值,事实上全部都是共享同一个对象:
[root@honest bin]# ./redis-cli 127.0.0.1:6379> set test '1234'OK127.0.0.1:6379> object encoding test"int"127.0.0.1:6379> set test 1234OK127.0.0.1:6379> object encoding test"int"127.0.0.1:6379>

接下来就是int和ebmstr两种内部编码的长度界限,请看下面的测试:


redis key的设计规则 redis key值设计_Redis_02


从图上的测试结果上来int和embstr的界限是20,当长度在1-19且存储的是数字类型的话编码是int。当存储的是字符数据的时候编码为embstr,那么ebmstr和raw的长度界限又在哪里呢? 接下来就是ebmstr和raw两种内部编码的长度界限,请看下面的源码:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44robj *createStringObject(const char *ptr, size_t len) {    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)        return createEmbeddedStringObject(ptr,len);    else        return createRawStringObject(ptr,len);}

也就是说,embstr和raw编码的长度界限是44,我们可以做如下验证。长度超过44以后,就是raw编码类型,不会有任何优化,是多长,就要消耗多少内存:

127.0.0.1:6379> set name "a1234567890123456789012345678901234567890123"OK127.0.0.1:6379> object encoding name"embstr"127.0.0.1:6379> set name "a12345678901234567890123456789012345678901234"OK127.0.0.1:6379> object encoding name"raw"

那么为什么有embstr编码呢?它相比raw的优势在哪里?embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能更好地利用缓存带来的优势。并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数。如下图所示,左边是embstr编码,右边是raw编码:


redis key的设计规则 redis key值设计_数据结构_03


  • ziplist内部编码

List,Hash,Zset的内部编码均都有 ziplist,他们分别在什么情况下会选用ziplist呢?

以Hash为例,我们首先看一下什么条件下它的内部编码是ziplist:

当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个);所有值都小于hash-max-ziplist-value配置(默认64个字节);

如果是sorted set的话,同样需要满足两个条件:

元素个数小于zset-max-ziplist-entries配置,默认128;所有值都小于zset-max-ziplist-value配置,默认64。

其中ziplist本质上是一本表(list)而不是一个链表(linked list),他和普通链表不一样,他会整体占用一大块内存,这样避免了内存碎片效率更为高效。 ziplist的源码在ziplist.c这个文件中,其中有一段这样的描述 -- The general layout of the ziplist is as follows::

zlbytes:表示这个ziplist占用了多少空间,或者说占了多少字节,这其中包括了zlbytes本身占用的4个字节; zltail:表示到ziplist中最后一个元素的偏移量,有了这个值,pop操作的时间复杂度就是O(1)了,即不需要遍历整个ziplist; zllen:表示ziplist中有多少个entry,即保存了多少个元素。由于这个字段占用16个字节,所以最大值是2^16-1,也就意味着,如果entry的数量超过2的16次方-1时,需要遍历整个ziplist才知道entry的数量; entry:真正保存的数据,有它自己的编码; zlend:专门用来表示ziplist尾部的特殊字符,占用8个字节,值固定为255,即8个字节每一位都是1。

如下就是一个真实的ziplist编码,包含了2和5两个元素:

![0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff] | | | | | | zlbytes zltail entries "2" "5" end

  • linkedlist内部编码 这个他的底层就是用java的LinkedList实现,采用双向链表的数据结构,这里简单讲一下LinkedList的特点,它允许插入所有元素,包括null,同时,它是线程不同步的,也就是现场不安全。他的结构如下图: 双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个first指针,指向头节点,和last指针,指向尾节点。 相比数组,链表的特点就是在指定位置插入和删除元素的效率较高,但是查找的效率就不如数组那么高了。
  • skiplist内部编码 这个他的底层就是用java的跳表实现,跳表(SkipList),是一种可以快速查找的数据结构,类似于平衡树。它们都可以对元素进行快速的查找。因为跳表是基于链表的(具体结构等下会将),因此,它的插入和删除效率比较高。因此在高并发的环境下,如果是平衡树,你需要一个全局锁来保证整个树的线程安全,而对于跳表,你只需要局部锁来控制即可。对于查询而言,通过“空间来换取时间”的一个算法,建立多级索引,实现以二分查找遍历一个有序链表。时间复杂度等同于红黑树,O(log n)。但实现却远远比红黑树要简单。
  • hashtable内部编码 这个他的底层就是用java的HashMap实现,HashMap 的大致结构如下图所示,其中哈希表是一个数组,我们经常把数组中的每一个节点称为一个桶,哈希表中的每个节点都用来存储一个键值对。在插入元素时,如果发生冲突(即多个键值对映射到同一个桶上)的话,就会通过链表的形式来解决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过key的哈希值先定位到桶,再遍历桶上的所有键值对,找出key相等的键值对,从而来获取value。


redis key的设计规则 redis key值设计_Redis_04


intset内部编码

Set特殊内部编码,当满足下面的条件时Set的内部编码就是intset而不是hashtable:

Set集合中必须是64位有符号的十进制整型;元素个数不能超过set-max-intset-entries配置,默认512;

验证如下:

127.0.0.1:6379> sadd scores 135(integer) 0127.0.0.1:6379> sadd scores 128(integer) 1127.0.0.1:6379> object encoding scores"intset"

那么intset编码到底是个什么东西呢?看它的源码定义如下,很明显,就是整型数组,并且是一个有序的整型数组。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数采取了不同的编码,尽量对内存的使用进行了优化。这样的数据结构,如果执行SISMEMBER命令,即查看某个元素是否在集合中时,事实上使用的是二分查找法:

typedef struct intset {    uint32_t encoding;    uint32_t length;    int8_t contents[];} intset;// intset编码查找方法源码(人为简化),标准的二分查找法:static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;    int64_t cur = -1;    while(max >= min) {        mid = ((unsigned int)min + (unsigned int)max) >> 1;        cur = _intsetGet(is,mid);        if (value > cur) {            min = mid+1;        } else if (value < cur) {            max = mid-1;        } else {            break;        }    }    if (value == cur) {        if (pos) *pos = mid;        return 1;    } else {        if (pos) *pos = min;        return 0;    }}#define INTSET_ENC_INT16 (sizeof(int16_t))#define INTSET_ENC_INT32 (sizeof(int32_t))#define INTSET_ENC_INT64 (sizeof(int64_t))
  • bitmap内部编码

这是Redis实现的一个布隆过滤器,bitmap并不是一种真实的数据结构,它本质上是String数据结构,只不过操作的粒度变成了位,即bit。因为String类型最大长度为512MB,所以bitmap最多可以存储2^32个bit。假设已经有3个元素a、b和c,分别通过3个hash算法h1()、h2()和h2()计算然后对一个bit进行赋值,接下来假设需要判断d是否已经存在,那么也需要使用3个hash算法h1()、h2()和h2()对d进行计算,然后得到3个bit的值,恰好这3个bit的值为1,这就能够说明:d可能存在集合中。再判断e,由于h1(e)算出来的bit之前的值是0,那么说明:e一定不存在集合中,如下图


redis key的设计规则 redis key值设计_key设计 短信验证存redis_05


  • GEO内部编码 Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充。Redis中将经纬度使用52位的整数进行编码,放进zset中,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实就是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。 GEO数据结构可以在Redis中存储地理坐标,并且坐标有限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下: 有效的经度从-180度到180度。 有效的纬度从-85.05112878度到85.05112878度。
redis> GEOADD city 114.031040 22.324386 "shenzhen" 112.572154 22.267832 "guangzhou"(integer) 2redis> GEODIST city shenzhen guangzhou"150265.8106"

当坐标位置超出上述指定范围时,该命令将会返回一个错误。添加地理位置命令如下: 总之,Redis中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标。

  • Streams内部编码 streams底层的数据结构是radix tree(基数树),事实上就几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix 树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。在Redis源码的rax.h文件中有一段这样的描述,这样看起来是不是就比较直观了:
  • HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。 比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。