【学习笔记】《Redis深度历险》

花了三个晚上读完《Redis深度历险 – 核心原理与应用实践》

Redis在工作中大量使用到,但读完书仍有些收获,同时整理出了几道适合作为面试题的问题。并分几个模块进行总结。

书的优点是讲了一些4.x,5.0新版本的特性,实际线上应用的还停留在3.x。因此像stream等新特性暂未尝过鲜。

缺点是为了凑篇幅吧,相同的算法(如分布式锁)使用python和java分别实现了一遍。同时数据结构的描述深度也不够,如HLL算法实现原理没有详细讲解,仅一笔带过。

读书笔记分几个模块:

  1. 基本数据类型
  2. Redis逻辑架构
  3. 集群模式
  4. 一些有意思的问题讨论(如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

原文地址:https://www.cnblogs.com/gm-201705/p/12897939.html