Redis 作为缓存使用, 单进程单实例存在的问题:
单点故障
容量有限
压力过大
1|1Redis 主从复制解决单点故障:
AKF 拆分原则:
x 轴: 全量, 镜像. 复制多个镜像, 解决单点故障
y 轴: 按业务功能拆分为多个实例, 同时在 x 轴方向同时创建多份镜像.
z 轴: 优先级, 逻辑再拆分. 比如说某个模块数据过多, 可以拆分为多个 Redis 客户端, 全量数据分为多份, 每个 Redis 中存一部分数据.
此时虽然解决了单实例存在的三个问题, 那么又会带来数据一致性问题.
解决数据一致性问题:
同步阻塞方式:
假如说一个客户端做了一个写操作, 到达主 Redis, 那么 client 将阻塞, 直到主 Redis 通知两个备 Redis 都成功写入才返回结果. 这时候为强一致性, 带来的问题是, 假如说备用 Redis 这时候挂掉, 没有写入成功, 那么主 Redis 等待超时之后, 就返回给客户端失败, 相当于服务不可用, 破坏了可用性.
异步方式:
弱一致性:
容忍数据丢失一部分, 当 client 发送一个写请求, Redis 立刻返回 OK, 这时候通知备 Redis 去写, 如果两个都写失败了, 那么就会丢失数据.
最终一致性:
这种方式虽然数据最终会一致, 但是在这期间如果有客户端去读数据, 可能会造成脏读.
小知识: 主备与主从的区别
主备: 备机一般不参与业务, 当主挂了之后, 备机可以代替主去提供服务.
主从: 客户端可以访问主, 也可以访问从.
Redis 一般使用主从复制的模式, 但是此时主自己又是一个单点.
对主做 HA 高可用:
对主做高可用并不是说不让主出现问题, 而是对外表现为没有出现问题. 人工可以去把其中一个从机设置为主, 让另一个从去追随它. 但是人往往是不可靠的, 所以需要技术或程序来实现, 主要是一个程序就会有单点故障的问题, 所以程序也必须是一个集群.
假如说有三个监控程序监控一个主 Redis 的存活状态, 那么也就是说 Redis 的存活状态由三个监控程序说了算.
强一致性, 都给出 OK: 假如说都给出 OK, 才表示 Redis 存活, 那么必然会破坏可用性, 比如说其中一个监控阻塞了, 而实际 Redis 还存活, 这相当于监控不可用, 所以不可取.
一部分给出 OK, 另一部分不算数: 那么一部分是几个呢? 假如拿三个监控举例, 那么就只能是 1 或者 2.
推导过程:
1 个: 统计不准确, 不够势力范围, 因为每个都可以做主. 可能会导致数据不一致, 会有网络分区的问题, 对外表现为同一服务拿到的数据不同, 也就是脑裂.
并不是说发生脑裂不好, 有个概念叫分区容忍性. 比如说 SpringCloud 中的 Eureka 注册中心, 假如说本来有十个服务注册到不同的 Eureka 中, 负载的时候需要打到不同的机器上, 每个负载发现的服务机器数不一致, 但并不影响, 对客户端来说, 只要有服务可用即可.
2 个: 这个时候会有两台结成势力范围, 两台之间互相通信, 这时候给出的结果就是 Redis 要么存活, 要么挂了, 不会有中间状态.
2 在 3 个节点成功解决脑裂问题, 3 在 4/5 个节点成功解决脑裂问题, 可以得出结论, 当有 n 个节点的时候需要 n/2+1, 也就是过半, 一般使用奇数台.
为什么是奇数台?
3 台, 4 台能承受的风险都是只允许 1 台出现问题, 4 台的成本更高.
4 台的时候比 3 台更容易出现问题, 即 1 台出现问题的概率更大.
主从复制配置:
replica-serve-stale-data yes
表示一个 Redis 在启动之后, 并且将追随一个主 Redis, 在主给生成 RDB 文件到从 Redis 去 load RDB 文件之前, 是否提供查询服务. no 的话, 直到全部同步完之前不提供服务.
replica-read-only yes
备机是否开启只读模式.
repl-diskless-sync no
主 Redis 发送 RDB 有两种方式, 第一种方式是通过落到磁盘, 从 Redis 再去 load, 第二种方式是直接通过网络发送 RDB 传给从 Redis. 这就取决于磁盘 IO 和网络 IO 哪个更快一些. 配置 no 的话默认是走磁盘方式.
repl-backlog-size 1mb
主从复制, 增量复制. 在 Redis 中, 除了写入 RDB 文件外, 还维护一个小的队列. 当从 Redis load 完 RDB 之后, 突然挂掉了, 然后服务又好了, 这时候又需要去同步数据, 但是此时的 RDB 文件已经过时, 可以把 RDB 文件重新覆盖一遍, 但是如果此时文件很大的话, 又需要浪费时间. 此时可以把一个偏移量给到主 Redis, 然后根据偏移量去获取增量数据, 但是这时候取决于队列大小, 默认为 1MB, 如果写的速度非常慢, 在这期间没有超过设置的大小, 那么是可以的, 但是如果写的数据非常多, 超过了设置的大小, 那么又会走全量复制, 所以要根据实际写入的数据设置合适的队列大小.
min-replicas-to-write 3 min-replicas-max-lag 10
可以设置最少写几个写成功, 当关心数据一致性的时候, 可以设置. 默认是注释掉的, 如果设置的话其实是在向强一致性靠拢, 所以需根据实际应用场景配置.
HA 高可用(x 轴):
sentinel 哨兵代替人工去自动修复故障, 可以是单机也可以是集群, 只监控 master 节点, 因为 master 节点上有 slave 节点信息, 通过发布订阅发现其他哨兵.
1|2Redis 解决容量问题:
解决方案 client 端:
当数据可以拆分的时候, 可以按业务逻辑拆分, 并分配到各个 Redis 实例.
当数据不可拆分的时候, 有三种解决方案(sharding 分片):
这三种模式都只能作为缓存用, 不能做数据库用.
利用算法: hash + 取模(modula), 模的大小为 Redis 实例数. 当 key 经过 hash 之后, 存放在某一台 Redis 实例中.
弊端: 取模的数必须固定, 当再增加 Redis 实例时, 可能会取不到原来的值, 需要重新取模, 全局洗牌, 影响分布式下的扩展性.
random 随机: 这种有一个应用场景, 一般用于 list 类型, 即消息队列. 当并发流量大时, 可以用 Redis 作为缓冲, 但是一台实例有扛不住, 可以多搞几台, 每一台上有相同的 key, 当 client 去 lpush 一个 key 时, 随机写到一台 Redis 实例中, 另一个 client 去 rpop, 这个 key 其实相当于 topic, 而每个 Redis 相当于 partition, 就是 kafka 的模型, 只不过 kafka 是基于磁盘的, 数据可以重新消费, Redis 是基于内存的, 速度快.
一致性哈希(ketama)
规划一个环形哈希环, 环上有很多点, 比如说 0~2^32, 然后有一个映射算法 (如 hash,md5 等很多), 有两个 Redis 节点, 分别为 node01,node02, 把这两个节点信息通过一个 hash 算法映射到这个环上某一个点, 这两个点是物理的, 其他的点都是虚拟的. 当一个数据(data) 进来时, 也经过这个 hash 算法映射在环上某个点, 假如说这些点都在一个排好序的集合里(比如说 TreeMap), 然后遍历这个 map 去找大于这个点的最近的物理点在哪, 找到这个点代表的物理机然后存放进去, 即存到了 node02 中. 假如说现在想加一个 node03, 经过 hash 后恰巧分配到了两个物理节点之间.
优点: 增加节点可以分担其他节点的压力, 不会造成像取模一样全局洗牌.
缺点:
问题: 新增节点导致一小部分数据不能命中, 可能会造成缓存击穿.
方案: 每次取的时候, 找离我最近的两个物理节点.
那么在取不到的数据存在 node02 节点上会造成空间浪费, 可以利用 Redis 自带的回收策略, 例如 LRU,LFU.
扩展: 在哈希环上增加虚拟节点, 一个物理设备可以通过 hash 算法映射为多个虚拟节点, 使数据存储更均匀.
server 端方案:
上述算法都是发生在客户端的, 那么当客户端连接 Redis 的时候, socket 连接过多, 对 server 端的影响是连接成本很高.
此时可以用代理的方式解决, 让所有的客户端去连接中间代理, 此时 server 端的 socket 连接压力不大, 只需要关心代理层性能即可.
所谓无状态, 就是本身并不需要数据库, 不需要存储数据, 数据是存在后端的, 只有达到无状态的代理, 像 Nginx 这种, 才更容易一变多.
如果客户端很多, 前面压力过大, 一台代理撑不住的话, 代理可以做一个集群, 在代理之前还可以加一层 LVS, 不需要对代理层做高可用, 因为如果 LVS 挂掉的话, 后面服务都不可用了, 所以 LVS 会做一个主备, 主备之间靠 keepalived 来管理, 除了可以监控 LVS 之外, 还可以监控代理层的健康状态, 如果其中一台代理挂了, 那么只会走另一台.
预分区:
不管是在 client 还是 proxy 的算法, 新增一台 Redis 的时候总会有问题, 要么重新取模全局洗牌, 要么丢失一部分数据, 所以干脆一开始取模值大一点, 比如说取模为 10, 模数值的范围 09. 此时中间还需要一层 `mapping` 做映射, 假如说一开始有两台 Redis1 和 Redis12,Redis1 上是 04 槽位, Redis2 上是 5~9 槽位, 这样新增一个节点的时候, 只需要从之前的 Redis 上让出几个槽位即可. 那么在数据迁移的过程中, 允不允许修改? 这时候可以先把时点数据 RDB 传过去, 再把增量的日志传过去.
Redis 集群实现:
cluster 模式: 只需要一个 client, 并且是无主模型, client 连哪一个都行, 在每一个 Redis 都有 hash 算法, 还需要有其他 Redis 上的映射关系, 假如说 client 要 get 一个 k1, 而 k1 根据 hash 取模算出来在 4 号槽位, 而此时 client 连接 Redis2, 那么会返回给客户端并跳转到 key 对应所在的 Redis 节点, 找到之后直接返回给 client 即可.(查询路由)
数据分治会导致聚合操作难以实现, 比如说求两个集合的交集, 两个集合在不同的 Redis 节点上. 事务也难以支持, Redis 并没有去实现, 但可以人为定义进行 hash 的算法, 比如说用相同的前缀(键哈希标签), 这样数据就会存储到同一 Redis 节点上.
比如这两个键 {user1000}.following 和 {user1000}.followers 会被哈希到同一个哈希槽里, 因为只有 user1000 这个子串会被用来计算哈希值.
2|0Redis 常见问题
2|1 缓存击穿
Redis 的 key 会有过期时间, 包括自带的 LRU,LFU 导致的淘汰 key, 当在过期的时候, 正好有大量并发请求来查询这个 key, 这个时候请求会直接打到数据库上, 称为缓存击穿. 强调的是高并发对某个过期 key 查询.
解决方案:
Redis 是单进程单实例的, 用 setnx()加锁, 第一个去获取 key 发现没有, 然后加一把锁, 加锁成功的话, 就去访问数据库, 后面的加锁失败, 就 sleep 等待, 一般根据实际业务场景选择等待时间.
问题 1: 只要加锁就可能会有死锁的问题, 假如说第一个人挂了怎么办?
可以设置锁的过期时间
问题 2: 如果没挂, 但是访问数据库阻塞了, 导致锁超时了怎么办?
使用多线程, 一个线程取数据库, 一个线程监控是否取回来, 更新锁的时间. 但是这样的话会增加业务代码的复杂度.
2|2 缓存穿透
从业务系统接收到的查询请求的是系统中根本不存在的数据, 称为缓存穿透.
解决方案:
布隆过滤器: 三种使用方式
client 包含: 压力到不了 Redis, 客户端代码复杂度高.
client 只写算法, bitmap 在 Redis.
Redis 集成布隆, 客户端轻盈.
布隆过滤器的一个缺点是不能删除, 如果有必要的话可以用布谷鸟过滤器, 或者把 key 的值设为 null.
2|3 缓存雪崩
大量的 key 同时失效, 间接造成大量的访问到达数据库, 为缓存雪崩.
解决方案:
两方面, 一是时点性无关的话, 把过期时间随机, 防止大量 key 同时过期.
如果是业务上 0 点, 或者 1 点失效的话, 可以强依赖击穿方案, 也可以在业务层加判断, 做零点延迟, 这样压力不会到 Redis.
来源: http://www.linuxidc.com/Linux/2020-03/162725.htm