一、List
常用API
底层实现
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底层设计
- 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关联起来。
- 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
数据压缩
在热点数据中,头结点和尾结点的数据可能需要频繁访问,但是对于中间的一些元素,访问可能不是那么频繁。我们就可以对中间的元素进行压缩
// 0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推
list-compress-depth 1
二、Hash
常用API
底层实现
Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置。
// ziplist 元素个数超过 512 ,将改为hashtable编码
hash-max-ziplist-entries 512
// 单个元素大小超过 64 byte时,将改为hashtable编码
hash-max-ziplist-value 64
当元素个数或者单个元素value值大小超过一定的阈值的时候,就会使用hashtable存储。、
我们可以验证一下,每当数据元素小于512且每个元素的value小于64byte,其数据结构应当是ziplist(有序)。
我们再插入一个value比价大的元素:
发现其底层编码已经变成了hashtable.l
三、Set
常用API
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数据结构。
两个条件任意满足时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的编码:
可以看到,此时是使用intset存储的。
我们此时加入一个a元素,再来查看:
发现a这个元素将原本的顺序打乱了。因为无法用intset来存储这个a了,此时我们再来看一下底层编码,发现已经从intset转化成hashtable了。
四、Set
常用API
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编码结构存储。
满足一下条件时,会使用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的编码:
发现是ziplist. 在数据较小的情况下都是使用ziplist存储的。
我们现在增加一个非常大的元素(大于64byte),验证是否会用跳表编码:
Zset数据结构
五、跳表skiplist
1、产生背景
我们如果使用链表存储数据,如果要查询某个数据,就需要先找到头节点,然后依次遍历知道找到这个元素。时间复杂度为O(n) 。
我们可以考虑给链表加一个索引层来优化查询:
有了这个索引层之后,如果我们要查询79的元素,可以先从索引层查询。这样查到78和84之间就可以找到79了。这样只需要查询大概一半的元素就可以找到目标值了。
那么还有没有优化空间呢?我们想,是不是这个索引层间隔可以再大一点,层可以再多一点?
现在我们有了两层索引,现在要查询79的话,先查询到索引层2的78,发现后面没有其他元素了。先后去到索引层1中的78,然后继续向后查询。发现了84,然后再去数据层中找到79.
这样,又可以减少查询次数。
我们继续优化,再加一层索引:
一次查询一半,有点类似折半查找。这样查询的次数又减少了。
我们来计算一下此时的查询复杂度:
数据项 : 查找个数
------------------------------------
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中的跳表实现
注意,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中的跳表如图所示:
我们现在想添加一个元素: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 应当插入到此处。
源码实现
1、找到要插入位置;
2、创建一个新节点,将其加入到跳表中;
六、跳表在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>
地图功能实战
假如我们现在要实现一个功能,输入自己的位置后,要查看周围有哪些地点,就可以使用geo实现;
1、我们首先在百度搜索“拾取坐标地址”,然后进入网站http://api.map.baidu.com/lbsapi/getpoint/ ;
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、查看故宫和北京动物园之间的距离:
在这里插入代码片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
- 距离某个地点半径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
我们来查询故宫周围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的数据结构:
发现GEO是使用zset存储的。
我们再来看其底层编码格式:
是使用ziplist实现的,因为现在的数据量较少,数据量大的话应该会转化成hashtable存储。
GeoHash经纬度编码
GeoHash优缺点
* 面试题
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存储,那么就无法精确的设置对象的某一个字段过期。