<commit_before>---
layout: post
title: Redis 相关整理
categories: 架构
description: Redis 相关整理
keywords: java, token, 架构, architect, app
---

Redis 作为一款开源的 NoSQL 产品，在系统架构中起到了十分重要的作用。

## 优势/速度快的原因

Redis 之所以这样快，主要原因是以下几点：

- 纯内存操作
- 单线程操作，避免线程切换
  - 单线程处理所有 redis-cli 请求
- 采用 I/O 多路复用机制，非阻塞 I/O
  - 服务端运行一个 IO 多路复用程序
  - 同时接收到多个请求
  - 将这些请求置于队列
  - 转发请求到对应类型的命令处理器
- 数据结构简单

## 主要数据类型

- String
  - value 理论上最大是 512M
- List
  - 链表，底层基于 quicklist(ziplist+linkedlist
    - 既可以作为栈也可以作为队列。
    - 可以实现消息队列，时间轴
  - 常用命令
    - L(R)PUSH, L(R)POP
    - BL(R)POP
    - LRANGE
- Hash
  - 是一个 String 类型的键值对映射表
    - 它的添加和删除效率是平均的。
    - 适合用于存储对象，相比于将对象序列化成字符串值存储的方式而言占用更少的内存，并且可以更方便的存取整个对象。
    - 和 java 的 HashMap 类似；可以只修改某一项属性值。
  - 常用命令
    - HMSET KEY K1 V1 K2 V2...
    - HGET KEY K1
    - HGETALL KEY
- Set
  - String类型的无序集合，key不重复，支持求交集/并集/差集。
    - 可以实现已关注/粉丝/共同关注/二度好友/抽奖/赞/踩/标签 SRANDMEMBER
  - 常用命令
    - SADD KEY v1 v2 v3，SADD KEY:TAG V1 V2 V3
    - SMEMBER KEY(:TAG), SISMENBER KEY V "查看成员，验证是否为成员"
    - SINTER
- ZSet(Sorted Set)
  - 增加权重的 Set
    - 基于 skiplist
    - 元素修改后会重排序
  - 常用命令
    - ZADD KEY K1 V1
    - ZRANGE(byscore) KEY 0 -1 <withscores>

## key 过期策略

Redis 的 key 过期策略采用主动+被动的方式。

### 主动过期

  当 key 被 get 时若发现已过期，这个 key 就会被删除。

### 被动过期

Redis 有一个定时任务每秒执行 10 次，流程如下：

1. 随机取 20 个已设置过期时间的 key
2. 删除已过期的 key
3. 若本过程中有四分之一以上的 key 是已过期，则重新执行步骤 1

极端情况下，已过期但未被删除的 key 数量为每秒写入 key 总数的四分之一。

### 配置优化方案

- 建议设置 maxmemory-policy 为 volatile-lru
  - 3.0默认是 noeviction 会阻塞清理内存导致延迟。
  - volatile-lru含义是remove the key with an expire set using an LRU algorithm
- maxmemory-policy 数据驱逐策略，优化目的：保证热点数据占据内存
  - volatile-lru：从已设置过期时间的数据中挑选最近最少使用的数据淘汰
  - volatile-ttl：从已设置过期时间的数据中挑选将要过期的数据淘汰
  - volatile-random：从已设置过期时间的数据中任意选择数据淘汰
  - allkeys-lru：从数据中挑选最近最少使用的数据淘汰
  - allkeys-random：从数据中任意选择数据淘汰
  - no-enviction：禁止淘汰数据

## 缓存更新的策略

[缓存更新的套路](https://coolshell.cn/articles/17416.html)

[Redis在京东到家的订单中的使用](https://tech.imdada.cn/2017/06/30/daojia-redis)

- cache aside
  - 写：先写库再删缓存(需确保删除失败)
  - 读：先读缓存，未命中则读库再set回缓存"
- 经典思想 write through、write back
- 缓存穿透与雪崩
  - 缓存穿透(查询不存在的数据)
    - 定时任务预热
    - 缓存 key 过期时间，不删除数据，
      - get -> judge -> set
      - timerTask update
    - 缓存空值(必须设置timeout)
      - 缓存 miss -> 分布式锁 -> 再次判断缓存 miss -> 查db -> 结果写入 Cache(无数据则存 null)
    - 布隆过滤器
  - 缓存雪崩(大量数据同时失效/宕机)
    - 事前
      - 主从 + 哨兵
      - redis cluster
    - 事发
      - 失效时间错开(特定较小范围内随机偏差)
      - ehcache + hystrix 限流
      - 漏桶，令牌桶 + 降级
      - guava rate limiter
    - 事后
      - redis 持久化
- 一致性hash
- 缓存无底洞现象
- 事务
  - MULTI、EXEC、DISCARD、WATCH
- 发布/订阅
  - 可用于较简单的实时消息系统

## 运维相关

- 危险命令
  - 对于危险命令采用 rename-command 重命名
  - keys 正则查找操作不当导致 hang 住
    - 改用 scan
  - flushdb
  - flushall
  - config

## 持久化

- RDB snapshot
  - "定时存储内存数据快照到文件"
  - 优点
    - 单日志文件、按时间点存储内存快照
    - 方便数据恢复
    - 提升程序性能(只需子线程写入 snapshot 到文件)
    - 较大数据下恢复速度比 AOF 快
  - 缺点
    - 数据备份方式不灵活，极端情况会丢失数据
    - fork 子进程耗时，导致 redis 阻塞
  - 生成 dump 流程
    "fork子进程创建rdb文件，生成后替换旧的文件"
    - save <sconeds> <changed-keys>
      "注销即不存储；save "" 即忽略之前save设置"
- AOF append-only-mode
  - 存储记录redis指令，支持日志文件动态调整大小
  - 优点
    - 灵活的备份策略 appendfsync always/no/everysec(默认)
    - 日志文件是 append only，极端情况下写入中断可以通过 redis-check-aof 修复
    - 日志文件太大时可以异步重写，由新的线程重写日志，原写入操作不受影响
    - 文件格式易懂，极端 flushall 情况下 AOF 没重写仍可以通过修改 AOF 再重启以恢复内存
  - 缺点
    - 文件较大
    - 系统负载较高
    - AOF 极低概率存在 bug
- RDB 和 AOF 配置可以同时开启
  - 当 redis 重启时会采用 AOF 日志重建内存数据。

## 实际部署

### 主从复制

可以搭配 Keepalived 做高可用。

- 实现机制
  - master 间断发送(slave发起请求)更新命令给 slave 节点
  - master 和 slave 连接中断后，slave 重试发起增量同步请求
  - master 维护内部缓存区的 backlog
  - 若部分重新同步请求不可能，slave 则申请全量重新同步；
  - master会采用较复杂的机制来实现内存快照的发送和持续更新
- 要点
  - 异步机制，slave 发送同步进度确认
  - 一个 master 可以对应多个 slave
  - slave 与 slave 之间可以相互通讯，以及串联部署。新版本下所有 slave 会接收相同的同步数据
  - master 端的同步是非阻塞的
  - slave 端的同步几乎都是非阻塞的，当 slave 删除旧数据并加装新数据时会阻塞
- 具体流程
  - master 内部维护<Replication ID, offset>
    - redis 重启时或者升级为 master 时生成一个 replication ID；
    - 版本相同的主从实例具有相同的 replication ID
    - slave 提升成 master 后会有两个 Replication ID，方便其他 slave 连接到自身时不必进行全量更新
  - slave 发送 PSYNC <replication-id> <offset>给master
    - 旧版的 SYNC 不建议再使用
  - master 判断请求命令，是否可以实现增量同步
    - 全量同步，master 执行 bgsave 发送内存文件给 slave
    - 增量同步，master 将缓冲区的 backlog 发送给 slave
      - 此时将新产生的待发送数据放到缓存队列中

### 哨兵 sentinel

是官方推荐高可用方案，可实现故障切换。

### 集群 cluster

版本要求3.0以上。

- 自动分片 crc16(key)%16384  2^14
  - 未采用一致性hash算法
- 虚拟槽分区，共 2 ^ 14 = 16384 个虚拟槽
  - 解耦数据与节点关系，节点自身维护槽映射关系，分布式存储
  - 新增节点时，只需要把其他节点的某些哈希槽挪到新节点
  - 移除节点时，只需要把节点上的哈希槽挪到其他节点
- 故障容忍，每个节点都维护多个槽
  - 主节点无法连接集群后，集群将会启用从节点；主节点会停止写命令并返回错误
- 集群中有 N 个哈希槽，则每个节点中有 1 个 master 和 N-1 个 slave
- 备份节点的重要性：多数 master 节点故障时导致集群不可用
  - a、一个集群里面有M1、M2、M3三个节点，其中节点 M1包含 0 到 5500号哈希槽，节点M2包含5501 到 11000 号哈希槽，节点M3包含11001 到 16384号哈希槽。如果M2宕掉了，就会导致5501 到 11000 号哈希槽不可用，从而使整个集群不可用。
  - b、一个集群里面有 M1-S1、M2-S2、M3-S3 六个主从节点，其中节点 M1 包含 0 到 5500 号哈希槽，节点 M2 包含 5501 到 11000 号哈希槽，节点 M3 包含 11001 到 16384 号哈希槽。如果是 M2 宕掉，集群便会选举 S2 为新节点继续服务，整个集群还会正常运行。当M2、S2都宕掉了，这时候集群就不可用了。"
- 服务端口为 N，则集群通讯端口为 N + 1000
- 采用 hash tags 保证待处理的 key 集合均位于同一个哈希槽
  "this{foo}key 和 another{foo}key"
- 不支持 select database 默认是 0

## 内存问题定位

![image](https://github.com/stdupanda/stdupanda.github.io/raw/master/images/posts/redis_memory.png)

内存问题定位常用整理：

- info memory
  - 计算公式 used_memory = 自身内存+对象内存+缓冲内存+lua内存；
    "used_rss = used_memory + 内存碎片"
  - used_memory Redis 分配器分配的内存量，也就是实际存储数据的内存总量
  - used_memory_human以可读格式返回 Redis 使用的内存总量
  - used_memory_rss从操作系统的角度，Redis进程占用的总物理内存
  - used_memory_peak内存分配器分配的最大内存，代表used_memory的历史峰值
  - used_memory_peak_human以可读的格式显示内存消耗峰值
  - used_memory_luaLua引擎所消耗的内存
  - mem_fragmentation_ratioused_memory_rss /used_memory比值，表示内存碎片率
  - mem_allocatorRedis 所使用的内存分配器。默认: jemalloc
- 查找 big key
  - big key引发的问题
    - 导致内存空间不平衡，超时阻塞
    - 流量打满网卡影响其他系统
  - redis-cli -h 127.0.0.1 -p 7001  –-bigkeys
- info client 汇总展示 client list 详细定位 omem 大于0(命令可能阻塞主线程)
- 客户端缓冲区（超过 1G 时连接会被关闭）
  - redis 使用缓冲区临时保存请求命令，并从缓冲区里获取命令。
  - 普通客户端缓冲区(例如 monitor 命令)
  - pubsub 客户端缓冲区

## 推荐阅读

[Redis Documentation](https://redis.io/documentation "官网教程")

[阿里云 redis 开发规范](https://yq.aliyun.com/articles/531067)
<commit_msg>redis update<commit_after>---
layout: post
title: Redis 相关整理
categories: 架构
description: Redis 相关整理
keywords: 架构, architect, cache, 缓存, redis
---

Redis 作为一款开源的 NoSQL 产品，在系统架构中起到了十分重要的作用。

## 优势/速度快的原因

Redis 之所以这样快，主要原因是以下几点：

- 纯内存操作
- 单线程操作，避免线程切换
  - 单线程处理所有 redis-cli 请求
- 采用 I/O 多路复用机制，非阻塞 I/O
  - 服务端运行一个 IO 多路复用程序
  - 同时接收到多个请求
  - 将这些请求置于队列
  - 转发请求到对应类型的命令处理器
- 数据结构简单

## 主要数据类型

- String
  - value 理论上最大是 512M
- List
  - 链表，底层基于 quicklist(ziplist+linkedlist
    - 既可以作为栈也可以作为队列。
    - 可以实现消息队列，时间轴
  - 常用命令
    - L(R)PUSH, L(R)POP
    - BL(R)POP
    - LRANGE
- Hash
  - 是一个 String 类型的键值对映射表
    - 它的添加和删除效率是平均的。
    - 适合用于存储对象，相比于将对象序列化成字符串值存储的方式而言占用更少的内存，并且可以更方便的存取整个对象。
    - 和 java 的 HashMap 类似；可以只修改某一项属性值。
  - 常用命令
    - HMSET KEY K1 V1 K2 V2...
    - HGET KEY K1
    - HGETALL KEY
- Set
  - String类型的无序集合，key不重复，支持求交集/并集/差集。
    - 可以实现已关注/粉丝/共同关注/二度好友/抽奖/赞/踩/标签 SRANDMEMBER
  - 常用命令
    - SADD KEY v1 v2 v3，SADD KEY:TAG V1 V2 V3
    - SMEMBER KEY(:TAG), SISMENBER KEY V "查看成员，验证是否为成员"
    - SINTER
- ZSet(Sorted Set)
  - 增加权重的 Set
    - 基于 skiplist
    - 元素修改后会重排序
  - 常用命令
    - ZADD KEY K1 V1
    - ZRANGE(byscore) KEY 0 -1 <withscores>

## key 过期策略

Redis 的 key 过期策略采用主动+被动的方式。

### 主动过期

  当 key 被 get 时若发现已过期，这个 key 就会被删除。

### 被动过期

Redis 有一个定时任务每秒执行 10 次，流程如下：

1. 随机取 20 个已设置过期时间的 key
2. 删除已过期的 key
3. 若本过程中有四分之一以上的 key 是已过期，则重新执行步骤 1

极端情况下，已过期但未被删除的 key 数量为每秒写入 key 总数的四分之一。

### 配置优化方案

- 建议设置 maxmemory-policy 为 volatile-lru
  - 3.0默认是 noeviction 会阻塞清理内存导致延迟。
  - volatile-lru含义是remove the key with an expire set using an LRU algorithm
- maxmemory-policy 数据驱逐策略，优化目的：保证热点数据占据内存
  - volatile-lru：从已设置过期时间的数据中挑选最近最少使用的数据淘汰
  - volatile-ttl：从已设置过期时间的数据中挑选将要过期的数据淘汰
  - volatile-random：从已设置过期时间的数据中任意选择数据淘汰
  - allkeys-lru：从数据中挑选最近最少使用的数据淘汰
  - allkeys-random：从数据中任意选择数据淘汰
  - no-enviction：禁止淘汰数据

## 缓存更新的策略

[缓存更新的套路](https://coolshell.cn/articles/17416.html)

[Redis在京东到家的订单中的使用](https://tech.imdada.cn/2017/06/30/daojia-redis)

- cache aside
  - 写：先写库再删缓存(需确保删除失败)
  - 读：先读缓存，未命中则读库再set回缓存"
- 经典思想 write through、write back
- 缓存穿透与雪崩
  - 缓存穿透(查询不存在的数据)
    - 定时任务预热
    - 缓存 key 过期时间，不删除数据，
      - get -> judge -> set
      - timerTask update
    - 缓存空值(必须设置timeout)
      - 缓存 miss -> 分布式锁 -> 再次判断缓存 miss -> 查db -> 结果写入 Cache(无数据则存 null)
    - 布隆过滤器
  - 缓存雪崩(大量数据同时失效/宕机)
    - 事前
      - 主从 + 哨兵
      - redis cluster
    - 事发
      - 失效时间错开(特定较小范围内随机偏差)
      - ehcache + hystrix 限流
      - 漏桶，令牌桶 + 降级
      - guava rate limiter
    - 事后
      - redis 持久化
- 一致性hash
- 缓存无底洞现象
- 事务
  - MULTI、EXEC、DISCARD、WATCH
- 发布/订阅
  - 可用于较简单的实时消息系统

## 运维相关

- 危险命令
  - 对于危险命令采用 rename-command 重命名
  - keys 正则查找操作不当导致 hang 住
    - 改用 scan
  - flushdb
  - flushall
  - config

## 持久化

- RDB snapshot
  - "定时存储内存数据快照到文件"
  - 优点
    - 单日志文件、按时间点存储内存快照
    - 方便数据恢复
    - 提升程序性能(只需子线程写入 snapshot 到文件)
    - 较大数据下恢复速度比 AOF 快
  - 缺点
    - 数据备份方式不灵活，极端情况会丢失数据
    - fork 子进程耗时，导致 redis 阻塞
  - 生成 dump 流程
    "fork子进程创建rdb文件，生成后替换旧的文件"
    - save <sconeds> <changed-keys>
      "注销即不存储；save "" 即忽略之前save设置"
- AOF append-only-mode
  - 存储记录redis指令，支持日志文件动态调整大小
  - 优点
    - 灵活的备份策略 appendfsync always/no/everysec(默认)
    - 日志文件是 append only，极端情况下写入中断可以通过 redis-check-aof 修复
    - 日志文件太大时可以异步重写，由新的线程重写日志，原写入操作不受影响
    - 文件格式易懂，极端 flushall 情况下 AOF 没重写仍可以通过修改 AOF 再重启以恢复内存
  - 缺点
    - 文件较大
    - 系统负载较高
    - AOF 极低概率存在 bug
- RDB 和 AOF 配置可以同时开启
  - 当 redis 重启时会采用 AOF 日志重建内存数据。

## 实际部署

### 主从复制

可以搭配 Keepalived 做高可用。

- 实现机制
  - master 间断发送(slave发起请求)更新命令给 slave 节点
  - master 和 slave 连接中断后，slave 重试发起增量同步请求
  - master 维护内部缓存区的 backlog
  - 若部分重新同步请求不可能，slave 则申请全量重新同步；
  - master会采用较复杂的机制来实现内存快照的发送和持续更新
- 要点
  - 异步机制，slave 发送同步进度确认
  - 一个 master 可以对应多个 slave
  - slave 与 slave 之间可以相互通讯，以及串联部署。新版本下所有 slave 会接收相同的同步数据
  - master 端的同步是非阻塞的
  - slave 端的同步几乎都是非阻塞的，当 slave 删除旧数据并加装新数据时会阻塞
- 具体流程
  - master 内部维护<Replication ID, offset>
    - redis 重启时或者升级为 master 时生成一个 replication ID；
    - 版本相同的主从实例具有相同的 replication ID
    - slave 提升成 master 后会有两个 Replication ID，方便其他 slave 连接到自身时不必进行全量更新
  - slave 发送 PSYNC <replication-id> <offset>给master
    - 旧版的 SYNC 不建议再使用
  - master 判断请求命令，是否可以实现增量同步
    - 全量同步，master 执行 bgsave 发送内存文件给 slave
    - 增量同步，master 将缓冲区的 backlog 发送给 slave
      - 此时将新产生的待发送数据放到缓存队列中

### 哨兵 sentinel

是官方推荐高可用方案，可实现故障切换。

### 集群 cluster

版本要求3.0以上。

- 自动分片 crc16(key)%16384  2^14
  - 未采用一致性hash算法
- 虚拟槽分区，共 2 ^ 14 = 16384 个虚拟槽
  - 解耦数据与节点关系，节点自身维护槽映射关系，分布式存储
  - 新增节点时，只需要把其他节点的某些哈希槽挪到新节点
  - 移除节点时，只需要把节点上的哈希槽挪到其他节点
- 故障容忍，每个节点都维护多个槽
  - 主节点无法连接集群后，集群将会启用从节点；主节点会停止写命令并返回错误
- 集群中有 N 个哈希槽，则每个节点中有 1 个 master 和 N-1 个 slave
- 备份节点的重要性：多数 master 节点故障时导致集群不可用
  - a、一个集群里面有M1、M2、M3三个节点，其中节点 M1包含 0 到 5500号哈希槽，节点M2包含5501 到 11000 号哈希槽，节点M3包含11001 到 16384号哈希槽。如果M2宕掉了，就会导致5501 到 11000 号哈希槽不可用，从而使整个集群不可用。
  - b、一个集群里面有 M1-S1、M2-S2、M3-S3 六个主从节点，其中节点 M1 包含 0 到 5500 号哈希槽，节点 M2 包含 5501 到 11000 号哈希槽，节点 M3 包含 11001 到 16384 号哈希槽。如果是 M2 宕掉，集群便会选举 S2 为新节点继续服务，整个集群还会正常运行。当M2、S2都宕掉了，这时候集群就不可用了。"
- 服务端口为 N，则集群通讯端口为 N + 1000
- 采用 hash tags 保证待处理的 key 集合均位于同一个哈希槽
  "this{foo}key 和 another{foo}key"
- 不支持 select database 默认是 0

## 内存问题定位

![image](https://github.com/stdupanda/stdupanda.github.io/raw/master/images/posts/redis_memory.png)

内存问题定位常用整理：

- info memory
  - 计算公式 used_memory = 自身内存+对象内存+缓冲内存+lua内存；
    "used_rss = used_memory + 内存碎片"
  - used_memory Redis 分配器分配的内存量，也就是实际存储数据的内存总量
  - used_memory_human以可读格式返回 Redis 使用的内存总量
  - used_memory_rss从操作系统的角度，Redis进程占用的总物理内存
  - used_memory_peak内存分配器分配的最大内存，代表used_memory的历史峰值
  - used_memory_peak_human以可读的格式显示内存消耗峰值
  - used_memory_luaLua引擎所消耗的内存
  - mem_fragmentation_ratioused_memory_rss /used_memory比值，表示内存碎片率
  - mem_allocatorRedis 所使用的内存分配器。默认: jemalloc
- 查找 big key
  - big key引发的问题
    - 导致内存空间不平衡，超时阻塞
    - 流量打满网卡影响其他系统
  - redis-cli -h 127.0.0.1 -p 7001  –-bigkeys
- info client 汇总展示 client list 详细定位 omem 大于0(命令可能阻塞主线程)
- 客户端缓冲区（超过 1G 时连接会被关闭）
  - redis 使用缓冲区临时保存请求命令，并从缓冲区里获取命令。
  - 普通客户端缓冲区(例如 monitor 命令)
  - pubsub 客户端缓冲区

## 推荐阅读

[Redis Documentation](https://redis.io/documentation "官网教程")

[阿里云 redis 开发规范](https://yq.aliyun.com/articles/531067)

{'fraction_non_alphanumeric': 0.09449731071576334, 'fraction_numerical': 0.026975589573851884, 'mean_word_length': 3.015282392026578, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 27, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3501297', 'n_tokens_mistral': 8378, 'n_tokens_neox': 9492, 'n_words': 1103}