花了三个晚上读完《Redis深度历险 – 核心原理与应用实践》
Redis在工作中大量使用到,但读完书仍有些收获,同时整理出了几道适合作为面试题的问题。并分几个模块进行总结。
书的优点是讲了一些4.x,5.0新版本的特性,实际线上应用的还停留在3.x。因此像stream等新特性暂未尝过鲜。
缺点是为了凑篇幅吧,相同的算法(如分布式锁)使用python和java分别实现了一遍。同时数据结构的描述深度也不够,如HLL算法实现原理没有详细讲解,仅一笔带过。
读书笔记分几个模块:
- 基本数据类型的底层实现
- 集群模式
- 应用实践
- Redis逻辑架构
- 一些有意思的问题讨论(如key过期,LFU算法)
一、基础数据结构
整个redis数据,可看作一个大的K/V,V又分多种类型:
string
list
hash
set
zset
bloom filter
bitmap
HyperLogLog
Geo
而这些基本数据类型,底层使用的数据结构并不复杂:
SDS(simple dynamic string),用于实现string存储:
struct SDS<T> {
T capacity; //数组容量
T len; // 数组长度
byte flags;
byte[] content; // 数据内容
}
capacity和len为什么使用泛型呢?因为在content长度很小的时候,T可以是short等,从而缩小内存占用。从这一个小点上,可以看到Redis对内存优化的极致。
capacity是因为redis的string是可修改的,支持append操作。冗余空间可减少内存分配和拷贝的开销。
冗余了一个len表示string长度,因为strlen(str)是一个O(n)的算法,len可以O(1)返回字符串长度。
List也非使用普通双向链表实现。其实现为,元素较少时,使用ziplist。元素较多时,使用多个ziplist串联起的quicklist
ziplist结构如下:
zlbytes|zltail_offset|zllength|entry|entry|entry|…|entry|slend|
可以看到,使用数组实现,省去了指标的开销。zltail_offset指向最后一个元素,用于快速定位到队尾,然后倒着遍历。
entry存放的内容不同,长度不一致。那么,倒着遍历如何定位到上一个entry呢?
struct entry {
int<var> prevlen;
int<var> encoding;
optional byte[] content;
}
其中的prevlen代表前一个entry的大小,所以可以用它来找到前一个元素,倒着遍历;
quicklist是一个双向链表,其每一个node是一个ziplist。
注意,元素较少时,hash/set/zset等结构的实现,都是使用ziplist。当然若set中的元素都是int,则使用的是一个intset的结构,其专为int设计,因此更节省空间,redis处处有此类设计。
当set其中只要有一个元素不为int时,则intset退化成ziplist.
元素多时,hash和set都是使用跳表实现。使用跳表原因,也是空间占用方面考虑。哈希表效率高,但像golang loadFactor 6.5p空间效率极低。像rbtree也能实现O(lgn)的复杂度,但其平均每个节点需要额外2个指针,空间效率也低于跳表。
同时由于zset还需维护value与score的对应关系,因此实际是个skiplist+hash的组合实现。
bloom filter,HLL,bitmap没什么好讲,普通的实现。主要看下Geo。
Geo用于计算两个经纬度坐标之间的距离,它使用GeoHash算法将一个二维的经纬度,转换为一维的整数。在Redis进行Geo查询时,我们要时刻想到它的内部结构实际上是一个zset。其score为GeoHash算法算出的52位整数。通过score排序就可以得到坐标附近的元素,通过score还原为原始坐标即可找到附近的人。
二、一些应用实践