花了三个晚上读完《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都是使用hash实现。zset使用跳表原因,一是空间占用方面考虑,二是要支持range查询。哈希表效率高,但像golang LoadFactor 6.5空间效率极低。像rbtree也能实现O(lgn)的复杂度并支持range查询,但其平均每个节点需要额外2个指针,空间效率也低于跳表。
同时由于zset还需维护value与score的对应关系,因此实际是个skiplist+hash的组合实现。
bloom filter,HLL,bitmap没什么好讲,普通的实现。主要看下Geo。
Geo用于计算两个经纬度坐标之间的距离,它使用GeoHash算法将一个二维的经纬度,转换为一维的整数。在Redis进行Geo查询时,我们要时刻想到它的内部结构实际上是一个zset。其score为GeoHash算法算出的52位整数。通过score排序就可以得到坐标附近的元素,通过score还原为原始坐标即可找到附近的人。
二、Redis逻辑架构
一个问题:为什么Redis命令处理是单线程模型,但却qps这么高?
1. 非阻塞I/O
非阻塞I/O有个问题,就是线程要读数据,结果读了一部分就返回了,那么进程如何知道何时继续读呢?
2. 多路复用API就用来解决这个问题,包括select/pool/epool等。
第二个问题,快照涉及到磁盘I/O,是非常慢的,Redis怎么处理的?
redis快照其实是fork一个子进程,在子进程中做dump到磁盘操作。因为操作系统的COW(copy on write),父进程的写入也不会影响子进程的快照过程。
三、集群模式
1. sentinel解决单点问题,可以实现主备的自动切换。
2. codis vs. redis cluster
redis cluster是P2P的架构,类似于cassandra对数据分区的处理。codis依赖一个zk或etcd的中心,记redis实例与slot的对应关系。
codis有层codis proxy,后面是一堆的redis实例。
由于这种模型,一个事务中多个key时,就不支持事务了,因为多个command可能分到多个redis实例上面。这就要我们对key设置hashtag,同理pfmerge,pfcount等功能在codis也要设置hashtag,否则数据将不正确。
四、讨论一些有意思的问题
1)hash的扩容
redis hash扩容过程与Golang基本一致,也使用的是渐进式扩容的方式;
2)key过期实现
惰性删除,即访问时判断过期则删除 + 定时删除,1秒25次扫描。使用了贪心算法,如果一次扫描,过期key占比大,则进行第二次。但为防止循环,限制了次数。
3)lru vs. lfu