2019年3月

Redis 的通信协议 RESP

Redis 作者认为数据库系统瓶颈不在网络流量, 而在数据库自身逻辑处理上, 所以使用了浪费流量的文本协议, 来换取即可的访问性能

RESP

RESP (Redis Serialization Protocol) 是一种直观的文本协议, 优势是过程简单, 解析极好, 劣势是耗费流量

RESP 将传输的结构数据分为 5 种最小单元类型, 单元结束时统一加上回车换行符 \r\n

  • 单行字符串以 + 符号开头
// 单行字符串 Hello World
+Hello World\r\n
  • 多行字符串以 $ 符号开头, 后跟字符串的长度
// 多行字符串 Hello World
$11Hello World\r\n
  • 整数值以 : 符号开头, 后跟整数的字符串形式
// 1024
:1024\r\n
  • 错误消息以 - 符号开头
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
  • 数组以 * 号开头, 后跟数组的长度
// 数组 [1,2,3]
*3\r\n:1\r\n:2\r\n:3\r\n
  • NULL
// NULL 用多行字符串表示, 长度写成 -1
$-1\r\n
  • 空字符串
// 空字符串用多行字符串表示, 长度填 0
// 两个 \r\n 之间表示空字符串
$0\r\n\r\n

客户端 -> 服务端

客户端向服务端发送执行只有一种格式, 多行字符串数组

// set author codehole
*3\r\n$3\r\nset\r\n$6author\r\n$8codehole\r\n

控制台输出的样式

*3
$3
set
$6
author
$8
codehole

服务端 --> 客户端

单行字符串响应

127.0.0.1:6379> set author codehole
OK

服务端响应内容

+OK

错误响应

// 试图对一个字符串进行自增的错误
127.0.0.1:6379> incr author
(error) ERR value is not an integer or out of range

服务端响应内容

-ERR value is not an integer or out of range

整数响应

127.0.0.1:6379> incr books
(integer) 1

服务端响应内容

:1

多行字符串

// 双引号括起来的字符串其实是多行字符串
127.0.0.1:6379> get author
"codehole"

服务端响应内容

$8
codehole

数组响应

127.0.0.1:6379> hset info name testname
(integer) 1
127.0.0.1:6379> hset info age 30
(integer) 1
127.0.0.1:6379> hgetall info
1) "name"
2) "testname"
3) "age"
4) "30"

hgetall 响应内容

*4
$4
name
$8
testname
$3
age
$2
30

嵌套

127.0.0.1:6379> scan 0
1) "0"
2)  1) "info"
    2) "books"
    3) "author"

服务端响应内容

*2
$1
0
*3
$4
info
$5
books
$6
author

总结

RESP 使用大量冗余的回车换行符, 已然是一个非常受欢迎的协议. 在技术领域, 性能并不是总是一切, 还有简单性, 易理解性和易实现性, 总之需要学会平衡.

Redis 的优胜劣汰 LRU 算法

本章主要介绍:

  1. redis 内存满了以后会怎样 ?
  2. redis 的近似 LRU 算法和严格 LRU 算法有什么区别 ?

Redis 的五种最大内存时的释放策略

场景

  • 当 redis 内存超出物理内存限制时, 会频繁和硬盘交换(swap), 极大影响性能
  • redis 可配置 maxmemory 参数来限制内存超出期望大小
  • 当实际内存超出 maxmemory 时, redis 提供了五种策略来让用户自己觉得如何腾出新空间提供读写服务

五种策略

  • noeviction: 默认策略. 不提供除 del 之外的写请求, 读请求可以继续进行. 保证不会丢失数据
  • volatile-lru: 尝试淘汰设置了过期时间且最少使用的 key.
  • volatile-ttl: 尝试淘汰设置了过期时间且 ttl 小的 key.
  • volatile-random: 尝试随机淘汰设置了过期时间的 key.
  • allkeys-lru: 在全体 key 中淘汰最少使用的.
  • allkeys-random: 在全体 key 中随机淘汰

严格 LRU 算法

  • 维护一个链表, 将所有设置了过期时间的 key 放在这个链表中
  • 当字典中某个元素被访问时, 它在链表中的位置会被移动到链表头部
  • 当空间满时, 就从链表尾部开始移除元素

近似 LRU 算法

为了不维护严格算法的链表, 节省内存

  • 给每个 key 增加一个额外 24bit 长度的小字段, 存储该 key 的最后一次访问时间戳
  • 当空间满时, 随机采样取出 5 个 key (数量可配置), 按时间戳淘汰掉最旧的 key
  • 循环第二步, 直到内存低于 maxmemory 值
  • 随机采样的范围取决于配置的策略是 volatile 还是 allkeys

Redis 3.0 开始, 增加了 淘汰池 进一步提升了近似 LRU 的效果:
上一次随机采样后未淘汰的 key, 会放入 淘汰池 留待下一次循环,
下一次随机采样的 key 会先和 淘汰池 中的 key 合并后, 再计算淘汰最旧的 key

Redis 过期策略

Redis 主要是惰性策略和定时删除两种策略结合, 来清理过期 key.

此处输入图片的描述

惰性策略

客户端访问 Key 时, 先进行过期判断, 如果已经过期立即删除.

定时删除

  1. Redis 将设置了过期时间的 key 放在一个独立的字典中
  2. 没 10s 扫描一次此字典, 随机取 20 个 key
  3. 删除这 20 个 key 中的已经过期 key
  4. 如果已经过期的 key 占比超过 1/4, 则重复步骤 2-4

问题一: 某一个时刻大量 key 过期, 影响单线程的 redis 读写操作怎么办 ?

  • 大量 key 过期一是堵塞线程,造成卡顿
  • 二是大量内存回收导致 cpu 飙升

优化方案

  • 每次定时扫描设置了 25ms 的上限, 当客户端请求正好遇到在扫描时, 至多等待 25ms, 当然如果客户端超时时间设置低于此阀值, 则会造成大量链接因为超时而关闭, 且还不会出现在 slowlog 中
  • 业务开发人员在设置过期时间时, 建议加上一个随机值, 避免在同一时间过期

问题二: del 删除大 key 卡顿主线程怎么办 ?

  • del 操作是立马释放内存, 小 key 的时候没没有明显延迟
  • 如果一个包含了上千万的 hash key, 删除操作就会造成主线程明显卡顿

优化方案

  • 使用上尽量避免大 key 的产生
  • Redis 4.0 版本引入了 unlink 指令, 对删除操作懒处理, 交给异步线程执行
  • unlink 是线程安全的. Redis 中所有数据比作一颗大树, unlink 是指令是砍掉这个key所属的树枝, 丢到火堆(异步线程池)里慢慢烧, 在离开大树的瞬间, 就不会被主线程访问到.