Skip to content

Redis 深度历险

基础数据结构

string

command
setkey val
getkey
existskey
delkey
mgetkey1 key2
msetkey1 val1 key2 val2
expirekey sec
setexkey sec val
setnxkey val
incrkey
incrbykey num

list

command
rpushkey val1 val2 val3
llenkey
lpopkey
rpopkey
lindexkey idx
lrangekey l_idx r_idx
ltrimkey l_idx r_idx

hash

command
hsetkey hkey hval
hgetallkey
hlenkey
hgetkey hkey
hmsetkey hkey1 hval1 hkey2 hval2
hincrbykey hkey num

set

command
saddkey val
saddkey val1 val2
smemberskey
sismemberkey val
scardkey #size()
spopkey

zset

value+score 也是用 Hash 结构存储的

command
zaddkey score val
zrangekey l_idx r_idx
zrevrangekey l_idx r_idx
zcardkey
zscorekey val
zrankkey val
zrangebyscorekey l_score r_score
zrangebyscorekey l_score r_score withscores
zremkey val

分布式锁

  • 过期问题 2.8 版本后, nx 和 ex 可以同时使用
  • 释放别人的锁问题 Lua 匹配随机数
  • 可重入 ThreadLocal 存储计数

    非必要操作

  • 加锁失败
    • 抛出异常
    • 重试
    • 异步处理

集群环境不一致 RedLock 向多个没有主从关系的 Redis 实例发送加锁, 过半成功则认为加锁成功 如果不是非常在乎高可用, 就别用

延时队列

消息队列
简单版消息队列队列,消费者定时轮询

  • 空轮询问题 使用阻塞读 blpop/brpop

延时队列
zset: score 是时间戳;
可以多线程争抢以避免单线程 break 掉任务;

位图

command
setbitkey pos bit
getbitkey pos
bitposkey 1 i_pos r_pos字节索引 不是 bit 索引
bitcountkey i_pos r_pos字节索引 不是 bit 索引
bitfieldkey get u4 0i=sined u=unsined 4=len 0=offset
bitfieldkey set i4 0 num
bitfieldkey overflow {strategy} incrby指定范围的位自增

len 最大长度 64 位,无符号数 u 只能获取到 63 位,因为 redis 协议里面 integer 是有符号的,不能传递 64 位无符号值

incrby 会溢出,溢出策略可以选择

  • sat 保持最大值
  • fail 不执行
  • wrap 溢出截断

HyperLogLog

使用场景:统计 UV

pf 是人名缩写

command
pfaddkey val
pfcountkey
pfmergekey1 key2

数据量比较多的情况下,HyperLogLog 每个 key 要占用 12k 字节的空间

214个桶,每个桶 6 bits, 共 2146÷8=12k(byte)

布隆过滤器

Redis 4.0 提供插件功能后, 作为插件, Jedis 提供了 JReBloom 包

command
bf.addkey val
bf.existskey val
bf.reservekey error_rate=0.01 initial_size=100

根据元素数量 n 和 误判率 f 可以得出 bits 长度 l 和 hash 函数个数 k

k=0.7ln

f=0.6185ln

  • 空间比(l/n)=8,f≈2%
  • f=10% 空间比 ≈5
  • f=1% 空间比 ≈10
  • f=0.1% 空间比 ≈15

简单限流

可以用 zset 维护 range, val score 都用毫秒时间戳

限流数较大的情况下空间占用高

漏斗限流

Redis-Cell 漏斗算法 灌水前触发漏水, 根据上次的时间戳和流速, 计算得到腾出的空间 关键参数:

  • 漏斗容量
  • 漏水速度
  • 剩余空间
  • 上次漏水时间
command
cl.throttlekey 15 30 60 115=漏斗初始容量 30 60=30 次/60s 1=quota 一次操作几个
res
00 允许 1 拒绝
15容量
14剩余空间 quota
-1重试时间 s
2多少秒之后,漏斗为空 s

GeoHash

场景:附近的人 二位坐标压缩成一维度,把平面分割成小块,并使用 bit 编码(二分 二叉查找树); 经纬度交替排列,然后使用 Base32 编码 最后用 zset 排序

存储空间占用是比较大的

命令很多, 以后再说

Scan

代替 keys

command
scan0 match key_prefix* count 10000=cursor 光标
key_prefix*=pattern
1000=字典槽数量,不是返回条数

所有的 key 都放在一个大 HashMap 里面

scan 顺序: 高位进位加法,如, 原始顺序:0->1 这样 rehash 扩容后,元素是相邻的,00->10->01->11 scan 顺序和扩容配合


线程 IO 模型

select 系列的事件轮询 + 非阻塞 IO

  • 非阻塞 IO socket 对象上提供的选项,Non_Blocking

  • 事件轮询 操作系统提供给用户程序的 API select, 输入是读写描述符列表(每个客户端 socket 都有对应的读写描述符) 轮询获取事件进行处理

    现在操作系统改用 epoll 了

  • 指令队列 每个客户端 socket 都关联一个指令队列

  • 响应队列 每个客户端 socket 都关联一个响应队列,如果队列为空,不输入客户端描述符 write_fds,防止线程空转

  • 定时任务 维护最小堆,堆顶是最快要执行的任务,从此刻到堆顶任务执行的时间内,没有其他任务要处理

通信协议

redis serialization protocol 5 种最小单元类型,单元结束时加上 \r\n

类型形式样例
单行字符串以 + 开头+hello world\r\n
多行字符串以 $ 开头$11\r\nhello world\r\n
整数以 : 开头:1024\r\n
错误消息以 - 开头-WRONGTYPE Operation against a key
数组以 * 开头*3\r\n:1\r\n:2\r\n:3\r\n
NULL多行字符串$-1\r\n
空串多行字符串$0\r\n\r\n

持久化

快照 二进制序列化形式 单线程进行 文件 IO 不能多路复用, 还要不阻塞; 这怎么办呢? 使用了操作系统的 多进程 COW: 调用 glibc 的函数 fork 出一个子进程, 持久化交给子进程处理 并发问题使用 OS 的 COW 机制进行数据段中页面的分离, 数据段由很多操作系统的页面(4K 大小)组合而成, 当父进程修改一个页面的时候, 会复制一个副本然后对这个副本进行修改; 而 fork 出来的子进程对冷数据快照进行持久化.

AOF 客户端指令会先存到日志, 然后再执行 优化: AOF 重写 bgrewriteaof 指令, 开辟一个子进程, 对内存遍历, 转换成操作指令, 序列化到一个新的 AOF 文件中, 序列化完毕后再将操作期间发生的增量 AOF 日志追加到文件中

glibc 的 fsync 函数可以强制刷盘 刷盘周期可以配置, 1s / 让 OS 决定 / 每个指令都刷

运维 同步操作通常比较耗费资源, 所以通常使用从节点进行持久化, 这时就要考虑网络分区的问题, 如果网络分区发生了, 主节点又挂了, 那数据有丢失风险; 最好冗余一个从节点

Redis 4.0 混合持久化

管道

redis 的管道(Pipline)是客户端提供的

管道本质上是改变指令的读写顺序, 提升并行度, 减少网络开销

读写之前都有 buffer, OS 将 buffer 中的数据发送/读取

事务

watch -> muti -> exec/discard watch 算是一种乐观锁

PubSub

消息多播, 不过应该被 5.0 的 stream 替代了 最大的问题在于: 消费者断连 重启后丢消息

小对象压缩

支持 32 位编译, 能节省指针大小 一些结构在数据量少的情况下是 ziplist

内存回收 操作系统按页回收; 会重用未回收的空闲内存;

内存分配 直接使用第三方内存分配库;

  • jemalloc(FaceBook)
  • tcmalloc(Google)

info memory 指令可以看用的啥

主从同步

指令同步 + 快照同步

指令同步 同步 buffer 中的指令, 并告诉主库同步到哪里了; 如果网络分区发生, 主库的环状 buffer 有可能被覆盖, 这时就要用快照同步;

快照同步 主库进行一次 bgsave, 全部持久化, 然后将文件发送给从库, 加载完后再进行增量同步; 如果 buffer 太小或者同步时间过长, 就无法进行增量复制, 又会触发快照同步, 有可能死循环; buffer 务必大一点;

无盘同步 有盘同步太慢了, 而且会阻塞 fsync, Redis 2.8.18 开始有了无盘同步, 通过 socket 将序列化内容发给从库

wait 指令

  • wait N t 等待 wait 指令之前的所有写操作同步到 N 个节点, 最多等待 t 秒; t=0 是永远等待;

Sentinel

哨兵集群, 自动切换主从 哨兵集群由 3-5 个节点组成

客户端从哨兵获取主节点

配置:

sh
# 至少由一个从节点正常复制, 否则停止对外写服务
min-slaves-to-write 1
# 如果 10s 没有反馈, 说明从节点不正常
min-slaves-max-lag 10

Codis

以后再说

Cluster

Redis 集群至少分为 3 个节点, 所有数据划分为 16384 个槽位, 槽位信息会存在每个节点中, 但是每个节点只负责一部分槽位 客户端连接集群的时候也会得到槽位配置信息, 可以根据槽位定位到目标节点

配置信息会持久化到配置文件, 配置文件要确保可写

槽定位算法 key 会使用 crc32 算法进行 hash, 得到一个整数值, 然后对 16384 MOD 确定槽位

key 还可以挂一个 tag, 来使用 tag 的槽位

如果客户端访问到了错误的节点, 会返回一个 "-MOVED slot_id ip" 形式的结果, 告诉客户端访问错误

迁移

redis-trib 可以手动调整槽位, 迁移的单位是槽 这个槽在原节点的状态为 migrating, 在目标节点的状态为 importing, 是中间态

  • 迁移过程: 从源节点获取 key => 存到目标节点 => 从源节点删除

  • 请求流量在迁移过程中的变化: 客户端请求源节点 => 源节点返回/返回重定向-ASK => asking 指令请求目标节点 => 重新请求目标节点 因为在迁移过程中目标节点是不认的, 会返回-MOVED, 这样会造成死循环, asking 指令就是命令目标节点处理

网络抖动 cluster-node-timeout 主从切换的 timeout 阈值

节点下线 在去中心化的 Redis Cluster 中, 一个节点认为某个节点失联, 不能说明它失联, 集群会有协商的过程, 只有大多数节点认为某个节点失联, 集群才会进行主从切换

节点采用 Gossip 协议广播自己的状态和自己对集群状态的认知, 如果一个节点发现某一个节点失联(PFail), 会将这条信息向整个集群广播; 如果一个节点收到某个节点失联的通知超过半数, 就会标记该节点已经下线(Fail), 然后通知整个集群宣布该节点下线, 并立即进行主从切换

感知集群变更 如果目标节点挂掉, 客户端会 ConnectionError 并随机挑选一个节点重试, 会收到 -moded, 随即更新客户端配置 如果当前节点被移出了集群, 客户端会收到 ClusterDown, 这时客户端会关闭所有连接, 清空槽位映射表; 下一条指令过来时, 会重新尝试初始化集群信息


Stream

xadd Stream 有一个消息链表, 所有加入的消息都串起来, 每个消息都有一个唯一 id 和对应的内容 每个 Stream 可以挂多个消费组 Group, 每个 Group 都有一个游标 last_delivered_id, Group 需要指令 xgroup create 创建, 并且指定一个消息的 id 每个消费者组可以挂多个消费者, 竞争消费. 消费者内有一个 PEL, 保存被客户端读取但没有 ack 的消息 id, 防止消息丢失

Info 指令

  • Server: 服务器环境
  • Clients: 客户端信息
  • Memory: 内存统计
  • Persistence: 持久化信息
  • Stats: 统计数据
  • Replication: 主从复制信息
  • CPU
  • Cluster:集群
  • KeySpace: 统计数量
sh
#查看哪些key比较频繁
redis-cli monitor
# 查看缓冲区
redis-cli info replication | grep backlog
# 查看半同步复制失败次数
redis-cli info stats | grep sync

过期策略

有一个独立的字典存放设置了过期的 key, 每秒 10 次扫描, 每次随机选 20 个, 过期的删除, 如果过期占比超过 25%, 再选 20 个; 为防止卡顿, 每次扫描不超过 25ms 从库是被动删除, 执行从主库同步过来的 del 指令

LRU

过期集

  • volatile-lru(least recently used) 从已设置过期时间的数据集(server.db[i].expires)中挑选最早访问的数据淘汰
  • volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

全集

  • allkeys-lru(least recently used) 当内存不足以容纳新写入数据时,在键空间中,移除最早访问的 key(这个是最常用的)
  • allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰

禁止

  • noeviction 禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错, 只能 get 和 del。

4.0 版本后增加以下两种

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

LRU 实现 真实的 lru 消耗空间比较大 Redis 里面的 lru 是近似 lru, 每个 key 上加一个时间戳, 当空间不足时, 随机选 5(可配置) 个, 淘汰掉最旧的, 直到空间可用 Redis 3.0 还增加了淘汰池, 新采样出来的加入池子, 再找出最早的

惰性删除

Redis 4.0, 大 key UNLINK unlink 后, 主线程会将对象的引用从 HashMap 移除, 封装成内存回收任务, 扔进一个线程安全的队列, 后台线程从中取出任务并执行

AOF sync 异步 刷盘动作也是一个任务队列

其他异步删除行为

sh
# key 过期
lazyfree-lazy-expire key
# LRU 淘汰
lazyfree-lazy-eviction
# rename
lazyfree-lazy-server-del rename
# 从库接收完rdb后立即flush
slave-lazy-flush

Jedis

主要是从连接池中获取到 Jedis 对象之后要放回去释放资源

保护 Redis

指令安全 配置文件

sh
# 重命名指令
rename-command keys abckeysabc

端口安全

sh
# 绑定服务器的ip 而不是客户端的ip
bind 10.100.20.13
# 要求密码
requirepass yoursecurepasswordhereplease

Redis 安全通信

官方推荐 SSL 代理软件 spiped 以后再说