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.
我们再来看一下每个Entry的结构:
思考: Redis为何不直接使用ziplist来存储所有的数据呢?为什么还要引入quicklist呢?
如果我们的元素非常多的时候,删除元素会比较耗费性能,因为删除后要重新分配空间赋值(类比数组删除、修改)。
Redis引入quicklist来讲多个ziplist关联起来。
现在,不用将所有数据都放到一个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 数据结构底层实现为一个字典( 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
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存储数据。
set-max-intset-entries 512 // intset 能存储的最大元素个数,超过则用hashtable编码
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"
我们来查看刚才设置的a-set的编码:
可以看到,此时是使用intset存储的。
我们此时加入一个a元素,再来查看:
发现a这个元素将原本的顺序打乱了。因为无法用intset来存储这个a了,此时我们再来看一下底层编码,发现已经从intset转化成hashtable了。
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会按照分数自动排序!
127.0.0.1:6380> zrange a-zset 0 -1 withscores 1) "a" 2) "100" 3) "c" 4) "150" 5) "b" 6) "200"
我们此时看看上面的zset的编码:
发现是ziplist. 在数据较小的情况下都是使用ziplist存储的。
我们现在增加一个非常大的元素(大于64byte),验证是否会用跳表编码:
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底层并不是直接使用跳表,而是对其进行了一些修改。
注意,zskiplist记录了和索引相关的信息:
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、创建一个新节点,将其加入到跳表中;
GeoHash是一种地理位置编码方法。 由Gustavo Niemeyer 和 G.M. Morton于2008年发明,它将地理位置编码为一串简短的字母和数字。它是一种分层的空间数据结构,将空间细分为网格形状的桶,这是所谓的z顺序曲线的众多应用之一,通常是空间填充曲线。
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、地点查询
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
我们来查询故宫周围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"
可以看到只查询出北京动物园。
我们来查看一下locations的数据结构:
发现GEO是使用zset存储的。
我们再来看其底层编码格式:
是使用ziplist实现的,因为现在的数据量较少,数据量大的话应该会转化成hashtable存储。
比如现在有数据:
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存储,那么就无法精确的设置对象的某一个字段过期。