小对象压缩存储

ziplist、intset

  • hash:hashtable,小对象压缩用ziplist
  • set:值为NULL的hashtable,小整数对象压缩用intset
  • zset:skiplist,小对象压缩用ziplist

应用篇

分布式锁

如果setnx和expire分开设置,程序在设置expire之前挂掉,锁无法del或过期。 redis支持整合setnx和expire到一条指令:

set lock:codehole true ex 5 nx // ex 5:过期时间5s,nx:键不存在时才设置  
... critical  
del lock:codehole

再安全点,保证锁只能由自己释放:

tag = random.nextint()  
if redis.set(key, tag, nx=True, ex=5):  
  do_something()  
  redis.delIfEquals(key, tag)   # 假想的delIfEquals指令,由脚本实现保证原子性

无法解决超时问题。如果在临界区的执行时间太长,超时导致锁释放,临界区就有并发问题。使用redission框架可以在锁快过期时续期。

延时队列

用zset实现延时队列,把消息的序列化串作为value,把消息的到期处理时间作为score。用一个进程定时查询zset的score最小的元素,可用ZRANGEBYSCORE key -inf +inf limit 0 1 withscores。如果最小score<当前时间戳,就将消息取出来执行。

近似计数

HyperLogLog,基数估计(count distinct),空间复杂度O(lglgn)

参见:基数估计,count distinct

布隆过滤器

说集合不含某元素时肯定不含,说集合含某未知元素时可能不含

可用在推荐新闻的去重,爬虫URL的去重(会比精确结果多去掉一些)

简单限流

限制某行为在一定时间内的发生次数

每个行为一个zset,用score记录行为发生时间(value保持唯一即可),查询[now - period, now]范围内的发生次数。

漏桶限流

漏桶的剩余容量leftQuota、漏水速率leakingRate、上次漏水时间leakingTime

先漏这一次水 leftQuota += (currentTime-leakingTime)*leakingRate,再看剩余容量够不够 leftQuota≥needed

Redis-Cell模块支持漏桶限流 #? ,用两个数字ops、seconds表示漏水速率

查找附近

GeoHash算法将地图区域不断切分为小方块并编码,切分次数越多精度越高、编码越长。

参见:GeoHash算法详解及其实现

scan指令

redis的所有key都存储在一个dict中(一维数组+冲突链表),scan返回的游标是一维数组的槽位。scan遍历采用高位进位加法(在最高位加1然后往左进位),这样在扩容或缩容rehash后的槽位是相邻的,可以避免对之前槽位的重复遍历。在扩容或缩容后对当前槽位的遍历可能返回重复元素,所以scan返回的结果需要客户端去重。

原理篇

线程IO模型

redis是单线程

事件驱动IO(IO多路复用)

read_events, write_events = select(read_fds, write_fds, timeout)  
for event in read_events:  
  handle_read(event.fd)  
for event in write_event:  
  handle_write(event.fd)  
handle_others() # 处理其他事情,比如定时任务等

持久化

快照

fork出子进程序列化内存数据,父进程写时复制共享页面

通常在从节点进行,不在主节点进行

日志

记录对内存数据的修改指令

内存数据库对日志的处理与其他数据库不同:

  • 先修改内存再写日志。其他数据库是先写日志再修改内存。
  • 日志瘦身和快照类似,fork出子进程将内存数据转成修改指令写入日志,完成后再将操作期间父进程发生的增量修改追加进去。其他数据库是压缩日志中的相同键只保留最新值。

一般有个独立的异步线程每秒fsync()一次,将所写日志强制从内核缓存刷到磁盘

混合持久化

快照和(从快照持久化开始到结束这段时间的)增量日志一起,重启时先加载快照、再重放增量日志

事务(不具备原子性)

multi服务端队列开启、exec执行服务端队列中的指令、discard抛弃队列。exec即使指令执行失败,也会执行队列中的后续指令,redis事务不具备原子性(可中断性)

乐观锁watch+multi

在事务开始前监控某些键,先watch某些键再multi,然后在exec时如果发现这些键的值已被更改,就事务失败返回NULL。exec返回后自动取消所有的watch。

集群篇

主从复制

主节点将改变状态的指令存在本地缓存中(定长的环形数组),然后异步地将指令复制到从节点。如果太久没复制,本地缓存中的旧指令会被覆盖,这时需要先快照复制、再增量指令复制。

Sentinel

主节点故障自动转移到从节点

Stream

类似Kafka

拓展篇

Redlock算法

在分布式锁的基础上,向所有节点发送set(key, value, nx=True, ex=xxx)指令,只要过半set成功就认为加锁成功。锁的有效时间要减去获取过半数锁的过程耗费的时间。释放锁要向所有节点发送del指令。

过期策略

将所有设置了过期时间的key放入一个独立的dict

  • 定时抽样删除:每0.1s抽样从字典中随机选20个key,删除其中过期的key,如果过期比例超过1/4就如此重复
  • 惰性删除:客户端访问这个key时检查过期时间,如果过期就立即删除

如果大批量key要过期,要给过期时间加一个随机时间(比如一天内的随机秒数),防止同时过期导致缓存雪崩

近似LRU算法

抽样删除:如果内存超出限制,就随机选5个key,淘汰掉最旧的,如此重复

异步删除

unlink会用后台线程异步删除大对象回收内存

源码篇

字符串

  • 正常结构含成员capacity、len、content
  • ≤44字节用embstr结构。因为RedisObject头16字节、字符串头3字节、NULL尾1字节,jemalloc分配64字节的话64-16-3-1=44字节。

字典dict

struct dict {  
  ...  
  dictht ht[2]; // 内含两个哈希表  
}

渐进式rehash: 在后续的定时任务或hash操作指令中,将ht[0]的内容一点点地迁移到ht[1]

压缩列表ziplist

紧凑存储,通过offset值支持正反向遍历

quicklist是ziplist的双向链表。压缩深度k表示首尾各k个ziplist不压缩,其余ziplist用LZF算法压缩。

新引入的listpack是ziplist的改进版,且解决了ziplist的级联更新问题。

跳跃表

struct zset {  
  dict *dict;  // all values  value=>score  
  zskiplist *zsl;  
}

基数树rax

类似压缩过的Trie树

参考

《Redis深度历险——核心原理与应用实践》