Redis

Redis 的特性:单线程、原子性、基于内存、优化数据结构、I/O 多路复用的非关系型数据库;

6.0 版本前网络 I/O 和命令处理都是单线程,6.0 版本后网络 I/O 改用多个线程处理

  • 大部分操作都在内存中完成(制约 Redis 性能的瓶颈并不是 CPU,而是内存和网络带宽)
  • 避免了多线程之间的竞争
  • I/O 多路复用机制

数据结构

5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)
3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)

持久化

保证即使在服务器重启的情况下也不会丢失数据(或少量损失)

AOF 日志

将每条执行成功的写操作命令追加到日志文件中,持久化代码命令在主进程上执行

写回策略(防止AOF日志在持久化过程中丢失):

img

重写机制(防止AOF日志文件过大):

  • 删去已经无意义的日志;

  • 重写操作由后台子进程完成,子进程共享父进程内存(为什么不用线程?父子进程以只读的方式共享数据,如果任意一方修改了该共享内存,父子进程会有独立的数据副本,即写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。而线程共享内存需要加锁);

  • AOF 重写缓冲区用于保存重写子进程启动后,新产生的AOF日志,并在重写结束后追加到新的AOF文件中;

RDB 快照

某一时刻内存数据的快照(二进制数据),恢复时直接读入内存即可

执行 bgsave 时,想要修改数据,需采用写时复制(Copy-On-Write, COW),以减少性能损耗

混合持久化

在AOF日志重写过程中,将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件,前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据,即重写缓冲区数据。

策略

过期删除策略

惰性删除+定期删除

内存淘汰策略

仅淘汰过期数据:random、ttl、lru、lfu

淘汰所有数据:lru、lfu

高可用

主从复制

模式一:全量复制【多用于初始化】

  • bgsave 命令创建子进程来做生成 RDB 文件的工作,是异步工作的

为了避免过多的从服务器和主服务器进行数据同步(全量复制),可以把一部分从服务器升级成分发节点,利用从节点分担读取操作的压力

模式二:基于长连接的命令传播【多用于保持同步】

主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接,保证第一次同步后的主从服务器的数据一致性。

模式三:连接恢复时可以采用增量复制:repl_backlog_buffer 环形缓冲区保存了最近传播的写命令,若其中存着从服务器请求的 offset,就采用增量复制;否则采用全量复制。若想降低主从服务器断开后全量同步的概率,需要增大 repl_backlog_buffer

QA

如何应对异步复制导致的主从数据不一致?

外部程序来监控主从节点间的复制进度

主从切换如何减少数据丢失?

  • 设置复制延迟限制 min-slaves-max-lag
  • 客户端降级措施,当 master 不可写,就把数据存到本地或 kafka 中等待主节点正常后再消费

集群脑裂导致数据丢失

主节点必须要有至少 min-slaves-to-write 个从节点连接,主从数据复制和同步的延迟不能超过 min-slaves-max-lag 秒。否则原主节点就会被限制接收客户端写请求,客户端也就不能在原主节点中写入新数据,等到新的主节点上线一切恢复正常。

哨兵机制

自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性

哨兵之间建立连接

img

哨兵与从节点建立连接

img

第一轮投票:判断主节点下线

主观下线:主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令

客观下线:一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,通过多个哨兵节点一起判断主节点是否下线。哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值则判断主节点客观下线

第二轮投票:选出哨兵 leader

  • 哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者,所谓的候选者就是想当 Leader 的哨兵。
  • 候选者会向其他哨兵发送命令,表明希望成为 Leader 来执行主从切换,并让所有其他哨兵对它进行投票。Raft 算法选择哨兵 Leader:
    • 第一,拿到半数以上的赞成票;
    • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

由哨兵 leader 进行主从故障转移

  • 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点,选择的规则:
    • 过滤掉已经离线的从节点;
    • 过滤掉历史网络连接状态不好的从节点;
    • 将剩下的从节点,进行三轮考察:优先级、复制进度、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。
  • 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
  • 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
  • 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;

集群

  • 同一个分区内的 Redis 节点之间的数据完全一样,多个节点保证了数据有多份副本冗余保存,且可以提供高可用保障;
  • 不同分片之间的数据不相同;
  • 通过水平增加多个分片的方式,可以实现整体集群容量的扩展;

集群数据分布策略

Hash 槽

  • 切片 slot 槽,共有16384个槽位;
  • 数据 key 通过 “CRC16 算法 + 16384 取模”,得到对应存储的哈希槽;
  • 数据存储在该槽位对应的 Redis 分区中,每个分区都有独立的主从哨兵保证高可用;

一致性Hash

虚拟节点

缓存

缓存雪崩、击穿、穿透

缓存穿透:大量请求了缓存和数据库里都没有的数据,请求穿透缓存和数据库

  • 参数校验:参数不合法直接抛出异常
  • 缓存无效 key:适用于 key 变化不频繁
  • 布隆过滤器:校验 Key 是否合法(有误差,可能会把未存的值当作已有,即不合法的 key 当作合法的)
  • 接口限流:固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法

缓存击穿:瞬间大量请求未缓存/已过期的数据库数据,请求击穿缓存直接打到数据库上

  • 热点数据永不过期或过期时间长
  • 预热热点数据:定时任务预热(定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中)、kafka 异步预热(将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存)
  • 访问数据库互斥锁

缓存雪崩:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上

  • Redis 集群
  • 限流
  • 多级缓存:本地缓存+Redis
  • 热点数据失效时间长
  • 预热热点数据

如何保证数据库和缓存一致性

采用 Cache Aside 旁路缓存策略:以数据库中的数据为准,缓存中的数据是按需加载的;缺点:数据写入频繁时,缓存中的数据会被频繁的清理,降低缓存命中率;

更新数据库+删除缓存:延迟双删

更新数据库+更新缓存:分布式锁

更新数据库+不更新缓存:较短的缓存有效期

删除缓存的时候失败了怎么办?

采用异步删除:

  • 消息队列确保缓存删除成功
  • 订阅 MySQL 的 binlog,数据修改时删除缓存

缓存预热

提高访问速度,平滑流量峰值,减少后端压力,保证数据的时效性

Redis 缓存预热的方法:系统启动时加载;定时任务加载;

实战

分布式锁

Redis + Lua 脚本,可以以原子性的方式执行一组命令,可用于保证锁释放操作的原子性(Lua 脚本在处理的过程中不会被任意其它请求打断),但无法用于事务执行失败的回滚。Redisson 采用的就是这种方法保证分布式锁的原子性。

SET 命令的 NX 参数可以实现:key 不存在才插入,可以用于实现分布式锁

优点:性能高效、实现方便、分布式高可用

缺点:不好设置超时时间(解决方法:守护线程定期续约)、主从异步复制可能导致不可靠性(解决方法如下)

提高可靠性(Redlock 算法):客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么就认为,客户端成功地获得分布式锁,否则加锁失败,所有的 redis 实例都会进行解锁;

  • 单机的 redis 是 cp 的,但是集群情况下 redis 是 ap 的,所以运行 Redisson 的节点必须是主节点,不能有从节点,防止主节点加锁成功未同步从节点就宕机,而客户端却收到加锁成功,导致数据不一致。
@Resource
private RedissonClient redisson;

延迟队列

有序集合 Zset;score 存储延迟执行的时间

大 Key 问题

会导致的问题

  • 由于 Redis 单线程处理,大 Key 的增删改查都很耗时,会导致工作线程阻塞、客户响应超时、网络传输阻塞
  • 内存分布不均,集群场景下 slot 槽数据量不均;
  • 持久化阻塞主线程,会主线程的两个阶段耗时增加:创建子进程复制页表结构、修改共享数据写时复制;

解决办法

  • 拆分大 key;
  • 及时/定时清理大 key,unlink 异步删除代替 del 同步删除;

发布者/订阅者

订阅命令:subscribe channel [channel … ]

img

Link

事务回滚

Redis 不支持回滚,事务没有原子性(要么全部成功要么全部失败)

管道

批处理技术,可以一次处理多个 Redis 命令,减少多个命令执行时的网络等待