redis

redis是一款高性能的NOSQL系列的非关系型数据库

应用场景

redis不可以做什么

不适合冷数据 大量的数据

作为缓存

缓存数据不重要,且不是全量数据

简单使用

可执行文件 作用
redis-server 启动Redis
redis-cli Redis命令行客户端
redis-benchmark Redis基准测试工具
redis-check-aof Redis AOF持久化文件检测和修复工具
redis-check-dump Redis RDB持久化文件检测和修复工具
redis-sentinel 启动Redis Sentinel
redis-server # 默认配置启动
redis-server --port 6379 # 指定配置

redis-cli -h 主机名 -p 连接端口
redis-cli get key # 直接执行get命令
redis-cli shutdown # 关闭redis server

慢查询分析

一条客户端命令的生命周期:

stateDiagram-v2
  客户端 --> Redis: 发送命令
  Redis --> Redis: 命令排队
  Redis --> Redis: 执行命令
  Redis --> 客户端: 返回结果

慢查询阈值设置:

config set slowlog-log-slower-than 2 # 设置阈值
slowlog get [n] # 获取慢查询日志 n 指定条数
slowlog len # 获取慢查询日志列表长度
slowlog reset # 清空慢查询日志

慢查询日志结构:

  1. id
  2. time
  3. duration
  4. command
    • 参数..
  5. ip:port

最佳实践:

redis shell

redis-cli -r 3 ping # 重复执行3次ping命令
redis-cli -r 3 -i 1 ping # 每隔1秒发一次ping 重复3此
echo "world" | redis-cli -x set hello # 从stdin读入 作为redis的最后一个参数
redis-cli --scan # scan命令
redis-cli --rdb ./bak.rdb # 生成rdb文件
echo -en '*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n*2\r\n$4\r\nincr\r\n$7\r\ncounter\r\n' | redis-cli --pipe # 直接发送命令给redis执行
redis-cli --bigkeys  # 分析内存占用比较大的键值对
redis-cli --latency # 查看客户端到目标redis的网络延时
redis-cli --latency-history -i 10 # 每隔10秒查看一次网络延时
redis-cli --latency-dist # 以统计图表的方式输出
redis-cli --stat # 获取redis的统计信息
redis-cli --raw get name # 返回数据不进行格式化(\xexxx)
redis-server --test-memory 1024 # 测试是否有足够的内存
redis-benchmark -c 100 -n 20000 # 100个客户 共请求20000次
redis-benchmark -c 100 -n 20000  -q # 只显示 requests per second
redis-benchmark -c 100 -n 20000 -r 10000 # -r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四位做随机处理
redis-benchmark -c 100 -n 20000 -P 10 # 每隔请求的pipline数据量
redis-benchmark -c 100 -n 20000 -q -k 1 # k为1代表启用客户端连接keepalive
redis-benchmark -t get,set -q # 只对指定的命令测试
redis-benchmark -t get,set -q --csv # 按照csv文件格式输出

Pipeline

Pipeline(流水线)机制能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端

客户端和服务端的网络延时越大,Pipeline的效果越明显

如果pipeline传递的数据过大 也会增加客户端的等待时间及网络阻塞

vs. 原生批量命令:

分布式

通用集群方案:

线程模型

redis 采用 IO 多路复用机制如 epoll 同时监听多个 socket,使用 Reactor 模型将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理

// 事件处理
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
 
    /* 若没有事件处理,则立刻返回*/
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    /*如果有IO事件发生,或者紧急的时间事件发生,则开始处理*/
    if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
       …
    }
    /* 检查是否有时间事件,若有,则调用processTimeEvents函数处理 */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    /* 返回已经处理的文件或时间*/
    return processed; 
}
graph TD
    A[bind/listen] --> B[epoll_wait]
    B --> FD
    B --> FD1
    B --> FD2
    FD --> D[AcceptEvent]
    FD1 --> D[AcceptEvent]
    FD2 --> D[AcceptEvent]
    FD --> E[ReadEvent]
    FD1 --> E[ReadEvent]
    FD2 --> E[ReadEvent]
    FD --> F[WriteEvent]
    FD1 --> F[WriteEvent]
    FD2 --> F[WriteEvent]
    D --> 队列
    E --> 队列
    F --> 队列
    队列 --> 事件分派器
    事件分派器 --> accept
    事件分派器 --> get
    事件分派器 --> put
    事件分派器 --> return

Redis 单线程模型指的是只有一条线程来处理命令,单线程对每个命令的执行时间是有要求的 某个命令执行过长 就会造成其他命令的阻塞

Redis 的以下操作会产生阻塞

如果阻塞点不在关键路径上,就可以异步执行。Redis 主线程启动后,会创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行:

static unsigned int bio_job_to_worker[] = {
    [BIO_CLOSE_FILE] = 0,
    [BIO_AOF_FSYNC] = 1,
    [BIO_CLOSE_AOF] = 1,
    [BIO_LAZY_FREE] = 2,
};
// 内部通过 bioCreateXXJob后,由线程执行bioProcessBackgroundJobs方法执行具体的内容

6.0 之后,Redis 引入了 IO 多线程,Redis 会在初始化过程中,根据用户设置的 IO 线程数量,创建对应数量的 IO 线程,这样 Redis 在进入事件循环流程前,都会将待读写客户端以分配给 IO 线程,由 IO 线程负责读写

void *IOThreadMain(void *myid) {
    ...
    while(1) {
        ...
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_WRITE) {
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }
        ...
    }
}

发现阻塞

此时可以进行日志记录 监控系统通过日志来进行监控报警 需要注意的是要改造Redis客户端 使其记录具体的Redis实例

开源的监控系统:CacheCloud

阻塞原因

内在原因

有些操作的时间复杂度为O(n) 这在高并发场景是不能接受的

这种情况需要重点注意慢查询以及大对象 针对它们进行优化

请求量很大 需要进行水平扩容来降低单实例的压力

fork阻塞:如避免使用过大的内存实例和规避fork缓慢的操作系统等

AOF刷盘阻塞:当硬盘压力过大 fsync命令可能会导致阻塞

HugePage写阻塞:对于开启Transparent HugePages的操作系统,每次写命令引起的复制内存页单位由4K变为2MB 会拖慢写操作的速度

外在原因

进程竞争:当其他进程过度消耗CPU时,将严重影响Redis吞吐量

CPU绑定:如果将Redis绑定在某个核上 那么在持久化的时候子进程与父进程共存 会导致父进程可用CPU不足

内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降

连接拒绝:网络闪断 连接数超过redis的最大连接数 linux文件符限制或者back_log限制导致的连接溢出

网络延迟:避免物理具体过远

网卡软中断:单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况

单线程模型也能高效率的原因

Redis的内存

内存消耗

内存使用统计:info memory命令

属性名 属性说明
used_memory Redis 分配器分配的内存总量,也就是内部存储的所有数据内存占用量
used memory_human 以可读的格式返回used_memory
used_ memory_rss 从操作系统的角度显示Redis进程占用的物理内存总量
used_memory_peak 内存使用的最大值,表示used_memory 的峰值
used memory peak_human 以可读的格式返回used_memory_peak
used_ memory_lua Lua引擎所消耗的内存大小
mem_fragmentation_ratio used_memory_rss/used_memory比值,表示内存碎片率
mem_allocator Redis所使用的内存分配器。默认为jemalloc

内存消耗划分:

  1. 对象内存 内存占用最大的一块 简单理解为sizeof(keys)+sizeof(values) 应当避免使用过长的键
  2. 缓冲内存 客户端缓存 复制积压缓冲 AOF缓冲等
  3. 内存碎片 默认的内存分配器采用jemalloc,可选的分配器还有:glibc、tcmalloc 频繁更新以及过期键的删除会使碎片率上升 使用整齐的是数据结构减少碎片 或者使用高可用架构重启服务器来整理内存碎片,4.0后通过将 activedefrag 配置项设置为 yes 来让 Redis 自动清理内存碎片

子进程内存消耗:

Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出

内存管理

Redis默认无限使用服务器内存

设置内存上限:maxmemory配置项 限制的是Redis实际使用的内存量,也就是used_memory统计项对应的内存

动态调整内存上限:config set maxmemory

内存回收

删除过期键对象:

graph TD
  默认采用慢模式运行 --> 每个数据库空间随机检查20个键
  每个数据库空间随机检查20个键 --> A{是否超过25%的键过期}
  A --> |yes| 循环执行
  A --> |no| 退出
  循环执行 --> B{执行时间超过25ms}
  B --> |yes| 每次Redis事件之前采用快模式运行
  B --> |no| 退出

循环执行指的是执行回收逻辑 直到不足25%或运行超时为止

内存溢出淘汰策略:设置内存最大使用量,当内存使用量超出时,会执行数据淘汰策略

策略 描述
volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰(最常用
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
volatile-lfu 从已设置过期时间的数据集中挑选访问频率最低的数据淘汰
allkeys-lru 从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random 从所有数据集中任意选择数据进行淘汰
allkeys-lfu 从所有数据集中挑选访问频率最低的数据淘汰
noeviction 禁止驱逐数据,当内存不足时,写入操作会被拒绝

内存溢出淘汰策略可以采用config set maxmemory-policy{policy}动态配置

对于 lru 算法:在 redisObject 结构体中,有个 24 位的 lru 字段(秒级),这个记录了一个时间戳,每次操作 key 该字段都会被更新,当内存不足,Redis 会随机从全局哈希表中找出过期的 key,把这些 key 删除

对于 lfu:lru 字段存储了 16 位的时间(分钟级),以及 8 位的访问次数,当键值对被再次访问时,lru 变量中的访问次数,会先根据上一次访问距离当前的时长,执行衰减操作,然后才按照一定的概率对访问次数进行增加,访问次数越大,执行增加操作的概率越小,在淘汰数据时,访问次数越小,就容易被淘汰

缩减键值对象

设计键时,在完整描述业务情况下,键值越短越好 值对象尽量选择更高效的序列化工具进行压缩

共享对象池

当数据大量使用[0-9999]的整数时,共享对象池可以节约大量内存

当启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池

编码优化

通过不同编码实现效率和空间的平衡

编码转换的流程:

graph TB
  hset --> A{判断当前编码类型}
  A --> |hashtable| hashtable编码
  A --> |ziplist| B{判断新数据长度\nhash-max-ziplist-value}
  B --> |大于| hashtable编码
  B --> |小于等于| C{比较集合长度\nhash-max-ziplist-entries}
  C --> |大于| hashtable编码
  C --> |小于等于| ziplist编码

控制键的数量

对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存

对于需要对如hash的内部数据进行过期处理 就必须通过外部定时任务扫描的方式来进行过期处理

整合Lua

redis-cli eval "return 1+1" 0
EVAL "local msg='hello world' return msg..KEYS[1]" 1 AAA BBB
local count = redis.call("get", "count")
redis.call("incr","count")
return count
redis-cli --eval test.lua 0

部署

加载到redis

redis-cli script load "$(cat test.lua)"

得到sha1值

执行

redis-cli evalsha "7a2054836e94e19da22c13f160bd987fbc9ef146" 0

lua脚本管理

redis运维

Linux配置优化

含义
0 表示内核将检查是否有足够的可用内存。如果有足够的可用内存,内存申请通过,否则内存申请失败,并把错误返回给应用进程
1 表示内核允许超量使用内存直到用完为止
2 表示内核决不过量的( "never overcommit")使用内存,即系统整个内存地址空间不能超过swap+50%的RAM值,50%是overcommit ratio默认值,此参数同样支持修改
策略
0 Linux3.5以及以上:宁愿用OOM killer也不用swap,Linux3.4以及更早:宁愿用swap也不用OOM killer
1 Linux3.5以及以上:宁愿用swap也不用OOM killer
60 默认值
100 操作系统会主动地使用swap

删库补救

持久化文件是恢复数据的媒介

误操作之后大AOF重写参数auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,让Redis不能产生AOF自动重写

以及拒绝手动bgrewriteaof

安全

bigkey处理

bigkey是指key对应的value所占的内存空间比较大

使用redis-cli --bigkeys统计bigkey

热点key

性能诊断checklist

  1. 获取 Redis 实例在当前环境下的基线性能
  2. 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做
  3. 是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除
  4. 是否存在 bigkey? 对于 bigkey 的删除操作,如果 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成
  5. Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘
  6. Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况
  7. 在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了
  8. 是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞
  9. 是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上

监控

Redis 会对 命令事件、AOF事件、fork事件、过期key事件、缓存替换事件 这些延迟事件的执行情况进行记录

内部记录了一个事件的最大延迟以及一段时间内的延迟历史

struct latencySample {
    int32_t time; /* We don't use time_t to force 4 bytes usage everywhere. */
    uint32_t latency; /* Latency in milliseconds. */
};

/* The latency time series for a given event. */
struct latencyTimeSeries {
    int idx; /* Index of the next sample to store. */
    uint32_t max; /* Max latency observed for this event. */
    struct latencySample samples[LATENCY_TS_LEN]; /* Latest history. */
};

redis vs memcached