Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

一、List

常用API

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

底层实现

List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现

可以通过设置每个ziplist的最大容量,quicklist的数据压缩范围,提升数据存取效率

//  单个ziplist节点最大能存储  8kb  ,超过则进行分裂,将数据存储在新的ziplist节点中
//  注意,配置中的数字不是实际的大小,要按照对应关系配置。-1=4kb, -2=8kb, -3 =16kb..
list-max-ziplist-size  -2        

//  0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推
list-compress-depth  1        

考虑: 为什么不使用链表?
1、链表的prve、next节点占用空间大,每个要4byte,这样如果list中数据存储的不是很多,就会比价浪费空间;
2、链表中元素存储不是连续的,会形成较多的内存碎片。

综上,redis使用较为紧凑的ziplist实现list.

ziplist底层设计

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

  • zlbytes:表示ziplist中存储数据的大小;
  • zltail:尾结点索引位置;
  • zlen:元素个数;
  • zlend: 表示数据结尾;
  • entry1~entryN:表示ziplist中的元素。

我们再来看一下每个Entry的结构:

  • prerawlen: 前一个元素的信息。如果小于254byte,表示当前的数据较小,只分配1byte去存储;如果大于254,需要再多分配4个byte;
  • len:当前元素的类型等信息;
  • data:当前元素的value

思考: Redis为何不直接使用ziplist来存储所有的数据呢?为什么还要引入quicklist呢?

如果我们的元素非常多的时候,删除元素会比较耗费性能,因为删除后要重新分配空间赋值(类比数组删除、修改)。

quicklist

Redis引入quicklist来讲多个ziplist关联起来。
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

  • head:关联头结点,指向尾结点的ziplist;
  • tail:关联尾结点,指向头结点的ziplist;
  • count:
  • quicklistNode:一个quicklist节点;

现在,不用将所有数据都放到一个ziplist中维护,而是每一个数据都有一个单独的ziplist存储。这样新增删除元素,只需要维护quicklist中链表即可。

此时,每一个ziplist的最大存储值默认是8kb,超过了8kb,就需要重新生成一个新的ziplist来存储。这个值也可以自己去配置大小:
注意,配置中的数字不是实际的大小,要按照对应关系配置。-1=4kb, -2=8kb, -3 =16kb…

list-max-ziplist-size  -2  

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

数据压缩
在热点数据中,头结点和尾结点的数据可能需要频繁访问,但是对于中间的一些元素,访问可能不是那么频繁。我们就可以对中间的元素进行压缩

//  0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推
list-compress-depth  1 

二、Hash

常用API

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

底层实现

Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置。

//  ziplist 元素个数超过 512 ,将改为hashtable编码
hash-max-ziplist-entries  512   

//  单个元素大小超过 64 byte时,将改为hashtable编码 
hash-max-ziplist-value    64      

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

当元素个数或者单个元素value值大小超过一定的阈值的时候,就会使用hashtable存储。、

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
我们可以验证一下,每当数据元素小于512且每个元素的value小于64byte,其数据结构应当是ziplist(有序)。
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

我们再插入一个value比价大的元素:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
发现其底层编码已经变成了hashtable.l

三、Set

常用API

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
smembers a-set: 查看set中所有元素。

smembers a-set
1) "1"
2) "3"
3) "4"
4) "5"
5) "9"
6) "10"

底层实现

Set 为无序的,自动去重、自动排序的集合数据类型,Set 数据结构底层实现为一个value 为 null 的 字典( dict ), 当数据可以用整形表示时,Set集合将被编码为intset数据结构

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

两个条件任意满足时Set将用hashtable存储数据。

  • 1, 元素个数大于 set-max-intset-entries ;
set-max-intset-entries 512       // intset 能存储的最大元素个数,超过则用hashtable编码
  • 2 , 元素无法用整型表示

Set中元素会自动去重和排序(全部是整型的时候)

127.0.0.1:6380> sadd a-set 1 3 5 10 9 4 4 4
127.0.0.1:6380> smembers a-set
1) "1"
2) "3"
3) "4"
4) "5"
5) "9"
6) "10"

Set存储

我们来查看刚才设置的a-set的编码:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
可以看到,此时是使用intset存储的。

我们此时加入一个a元素,再来查看:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
发现a这个元素将原本的顺序打乱了。因为无法用intset来存储这个a了,此时我们再来看一下底层编码,发现已经从intset转化成hashtable了。
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

四、Set

常用API

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

127.0.0.1:6380> zadd a-zset 100 a 200 b 150 c
(integer) 3
127.0.0.1:6380> zrange a-zset
(error) ERR wrong number of arguments for 'zrange' command
127.0.0.1:6380> zrange a-zset 0 -1 
1) "a"
2) "c"
3) "b"
127.0.0.1:6380> zrange a-zset 0 -1 withscores
1) "a"
2) "100"
3) "c"
4) "150"
5) "b"
6) "200"
127.0.0.1:6380>

zset会按照分数自动排序

底层实现

ZSet 为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
满足一下条件时,会使用skiplist跳表编码:


zset-max-ziplist-entries  128    // 元素个数超过128 ,将用skiplist编码
zset-max-ziplist-value     64     //  单个元素大小超过 64 byte, 将用 skiplist编码

ZSet会按照分数自动排序

zset会按照分数自动排序

127.0.0.1:6380> zrange a-zset 0 -1 withscores
1) "a"
2) "100"
3) "c"
4) "150"
5) "b"
6) "200"

ZSet存储

我们此时看看上面的zset的编码:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
发现是ziplist. 在数据较小的情况下都是使用ziplist存储的。

我们现在增加一个非常大的元素(大于64byte),验证是否会用跳表编码:

Zset数据结构

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

五、跳表skiplist

1、产生背景
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

我们如果使用链表存储数据,如果要查询某个数据,就需要先找到头节点,然后依次遍历知道找到这个元素。时间复杂度为O(n) 。

我们可以考虑给链表加一个索引层来优化查询:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
有了这个索引层之后,如果我们要查询79的元素,可以先从索引层查询。这样查到78和84之间就可以找到79了。这样只需要查询大概一半的元素就可以找到目标值了。

那么还有没有优化空间呢?我们想,是不是这个索引层间隔可以再大一点,层可以再多一点?
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
现在我们有了两层索引,现在要查询79的话,先查询到索引层2的78,发现后面没有其他元素了。先后去到索引层1中的78,然后继续向后查询。发现了84,然后再去数据层中找到79.

这样,又可以减少查询次数。

我们继续优化,再加一层索引:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
一次查询一半,有点类似折半查找。这样查询的次数又减少了。

我们来计算一下此时的查询复杂度:

数据项    :    查找个数
------------------------------------
index 1		:		N / 2^1
index 2		:		N / 2^2
index 3		:		N / 2^3
....
index k		:		N / 2^k

我们设最顶层索引会将数据分成2端,则 N / 2^k = 2, 即:
2^k = N /2 
k = log N   

这就是跳表skiplist, 数组上面加索引,以空间换时间。

其实Redis底层并不是直接使用跳表,而是对其进行了一些修改。

Redis中的跳表实现

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
注意,zskiplist记录了和索引相关的信息:

  • header:指针指向索引层;
  • taile:指向尾节点;
  • length:元素个数;
  • level:层高,因为遍历的时候需要从上往下遍历,需要拿到最高层的这个数。

  • backwork指针是为了从前往后遍历使用的:
127.0.0.1:6380> ZREVRANGE a-zset 0 -1 withscores
1) "b"
2) "200"
3) "c"
4) "150"
5) "a"
6) "100"
127.0.0.1:6380>

数据插入

假如已有如下元素

127.0.0.1:6380> zrange b-zset 0 -1 withscores
1) "a"
2) "100"
3) "b"
4) "120"
5) "c"
6) "200"

其在Redis中的跳表如图所示:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
我们现在想添加一个元素:150 d. 下面是添加规则:

1、现在根据zskiplist中的level,从第一个元素NULL中找到L2;
2、根据NULL中的L2的指针找到真实的L2元素,发现是b 120;
3、判断要插入的数据150是否大于120,如果是,则继续往下找;
4、找到最右边的NULL,说明要往L1上去找;
5、在b节点上找到L1指针,继续向下找,找到L1连接着的元素c;
6、发现要插入的150小于200, 则元素150 d 应当插入到此处。
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

源码实现

1、找到要插入位置;
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
2、创建一个新节点,将其加入到跳表中;
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

六、跳表在Geo中的使用

GeoHash是一种地理位置编码方法。 由Gustavo Niemeyer 和 G.M. Morton于2008年发明,它将地理位置编码为一串简短的字母和数字。它是一种分层的空间数据结构,将空间细分为网格形状的桶,这是所谓的z顺序曲线的众多应用之一,通常是空间填充曲线。

常用API

1、命令查看

127.0.0.1:6380> help @geo

  GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
  summary: Add one or more geospatial items in the geospatial index represented using a sorted set
  since: 3.2.0

  GEODIST key member1 member2 [m|km|ft|mi]
  summary: Returns the distance between two members of a geospatial index
  since: 3.2.0

  GEOHASH key member [member ...]
  summary: Returns members of a geospatial index as standard geohash strings
  since: 3.2.0

  GEOPOS key member [member ...]
  summary: Returns longitude and latitude of members of a geospatial index
  since: 3.2.0

  GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point
  since: 3.2.0

  GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member
  since: 3.2.0

  GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
  summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.
  since: 6.2

  GEOSEARCHSTORE destination source [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH] [STOREDIST]
  summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.
  since: 6.2

127.0.0.1:6380> 

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

地图功能实战

假如我们现在要实现一个功能,输入自己的位置后,要查看周围有哪些地点,就可以使用geo实现;

1、我们首先在百度搜索“拾取坐标地址”,然后进入网站http://api.map.baidu.com/lbsapi/getpoint/
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
2、我们点击地点,获取一些地点的经纬度,并将其添加到redis中:

// 116.400739,39.918672 -> 116.400739 39.918672
geoadd locations 116.400739 39.918672 gugong 

geoadd locations 116.344972 39.947884 beijingzoos

geoadd locations 116.303578 39.990794 haidianpark

3、查看故宫和北京动物园之间的距离:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

在这里插入代码片127.0.0.1:6380> GEODIST locations gugong beijingzoos km
"5.7602"
127.0.0.1:6380> 

可以看到,它们之间的巨鹿是5.7公里。

再看看,故宫到海淀公园的距离:

127.0.0.1:6380> GEODIST locations gugong haidianpark km
"11.5315"
127.0.0.1:6380>

4、地点查询

  • 经纬度半径查询:
    longitude 经度,latitude 纬度,radius 半径
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
 
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point

since: 3.2.0

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

  • 距离某个地点半径x公里查询
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member

since: 3.2.0

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
我们来查询故宫周围15km的地点:

127.0.0.1:6380> GEORADIUSBYMEMBER locations gugong 15 km
1) "gugong"
2) "beijingzoos"
3) "haidianpark"

可以看到,已经成功的查询出了故宫半径15公里内的所有地点。

我们只查故宫附近6公里的地点:

127.0.0.1:6380> GEORADIUSBYMEMBER locations gugong 6 km
1) "gugong"
2) "beijingzoos"

可以看到只查询出北京动物园。

GEO底层实现

我们来查看一下locations的数据结构:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
发现GEO是使用zset存储的。

我们再来看其底层编码格式:
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
是使用ziplist实现的,因为现在的数据量较少,数据量大的话应该会转化成hashtable存储。

GeoHash经纬度编码

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战
Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战

GeoHash优缺点

Redis核心数据结构底层原理、源码剖析+跳表+GEO地图实战


* 面试题

1、String和Hash两种类型的优缺点,你是如何选型的?

比如现在有数据:

user:
	id: 100
	name: nangua
	age:18
	...

这个结构我们采用String和Hash存储的优缺点分别是什么?

1、使用String存储

user:id 100
user:name nangua
user:age 18
......

缺点:
这样的问题很明显,需要多个key去存储一个对象,而key存储的时候是使用SDS存储的,key多了,自然需要更多的存储空间

其次,我们知道,String底层是使用dict(字典)来存储的。

当我们新加一个数据的时候,都需要hash(key) -> arr[] 。这样,如果数据非常多的时候,这个数据很快就会变得非常大。当数据元素增加到一定程度的时候还需要扩容rehash。

所以如果用String这样存储,还可能带来频繁的rehash

优点:
可以对每一个属性设置过期时间。

2、使用hash存储

优点:
使用hash的好处是只会用到一个key.

那么字段比如name, age等会被放到内部小的hashtable中存储。这样存储可以减少外部频繁的rehash

缺点:
我们知道,只有最为层的key可以设置过期时间的。如果我们使用hash存储,那么就无法精确的设置对象的某一个字段过期。

上一篇:redis设计与实现总结--对象


下一篇:redis数据结构-链表,压缩表,快速表