中间件-Redis

Redis

核心能力与原理

高性能

lru算法优化

  • 由于 LRU 算法需要用链表管理所有的数据,会造成大量的空间消耗,Redis随机采样一小部分键值,并选中其中最久没有访问的键,然后淘汰。
  • Redis 使用基于采样的近似LRU算法来替代普通的LRU算法,从而避免普通的LRU算法带来的过高的资源消耗,并不需要驱逐精确的那个最久没有访问的键。
  • 原理:
    • redis内部有全局的LRU时钟lruclock,每个redis对象被访问时,会根据时钟记录最后访问时间到LRU时间
    • 当需要淘汰数据时,Redis创建一个候选列表,列表大小通过maxmemory-samples参数来配置,默认配置为5,当为10时和普通LRU算法结果相当。;
    • 从allKey|ttlKey中随机采样填满候选列表,然后淘汰列表中LRU时间最小的key;
    • 检查是否还需要淘汰,如果需要则先清空候选列表再重新采样key填满候选列表,并且此次采样的数据的lru字段必须比上次淘汰数据的LRU时间小,
    • 然后再次淘汰数据,直到不需要内存足够不需要淘汰数据为止

淘汰策略

  • 不处理直接报错
  • 设置过期时间的key
    • volatile-lru:淘汰设置过期最久没有使用的key
    • volatile-random:淘汰设置过期随机key
    • volatile-ttl:淘汰设置过期剩余存活时间段的key
    • volatile-lfu:淘汰设置过期使用频率最少的key
  • 没有设置过期时间的key
    • allkeys-lru: 淘汰最久没有使用的key
    • allKeys-random:淘汰随机key
    • allkeys-lfu:淘汰使用频率最少的key

线程模型和IO模型

redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

6.0以前的模型

img img

redis命令的执行流程img img

6.0以后版本支持多线程IO,

使用多线程处理网络IO,读写IO和解析命令交给多线程处理,原来的单主线程仍然自己执行命令

img img

总结与思考

  • 开启多线程后对于简单命令qps可以达到20w左右,相比单线程有一倍的提升,性能提升效果明显

  • 业务系统所要处理的流量越来越大,Redis 的单线程模式会导致系统消耗更多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis 的性能有两个方向:

    • 优化网络 I/O 模块
    • 提高机器内存读写的速度
  • 后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:

    • 零拷贝技术或者 DPDK 技术
    • 利用多核优势
  • 模型缺陷

    • Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers模型。
    • Redis 的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。
    • 在我看来,Redis 目前的多线程方案更像是一个折中的选择:既保持了原系统的兼容性,又能利用多核提升 I/O 性能。

Redis数据结构的底层原理

参考博客

string
  • 通过动态字符串SDS实现字符串,SDS是redis定义的字符串结构体,封装了字符串的定义、操作api(类似Java的String封装)
  • SDS、C字符串对比
    • SDS缓存字符串长度,获取字符串长度为O(1),C字符串为O(n)
    • SDS封装好API,可以杜绝缓冲区溢出导致的内存问题
    • SDS通过空间预分配和空间惰性释放减少内存分配问题,减少内存拷贝次数。预分配:每次扩容分配额外内存,减少扩容次数;空间惰性释放:sds字符串缩短不会马上回收申请的内存,留着备用
    • 二进制安全,c字符串有编码和空字符限制,存储音频等二进制是可能有问题,SDS存储数据通过二进制处理存储,数据写入的格式和读取的格式一致
list
  • 版本3.2之前,双向链表linkedlist 或 压缩列表 ziplist
    • 当元素个数较少时,Redis 用 ziplist 来存储数据,当元素个数超过某个值时,链表键中会把 ziplist 转化为 linkedlist。当链表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码;
    • 问题:linkedList存在节点占用内存较大、节点存储不连续导致内存碎片问题;ziplist是连续存储,大数据量时对插入、遍历不友好
  • 版本3.2之后,quickList + ziplist
    • quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。本质上来说,quicklist里面保存着一个一个小的ziplist。
    • 结构如:
    • 优点:quickList折中了linkedlist 、ziplist的优点,将list进行分段存储,每个小的ziplist存储固定元素,弥补了ziplist插入、遍历时的缺点;使用ziplist可以有效减少quickListNode的个数,减少双向链表导致的节点占用内存较大、节点存储不连续导致内存碎片问题。
zset
  • 当zset元素个数较小时,使用ziplist实现
    • 元素数量少于128的时候 且 每个元素的长度小于64字节
  • 不满足上述两个条件就会使用skiplist跳表,具体来说是组合了map和skiplist,skiplist能在O(logN)的时间内插入元素,并且实现快速的按分数范围查找
    • map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的scope分数
    • skiplist按从小到大的顺序存储分数
    • skiplist每个元素的值都是[score,value]对
  • skiplist结构:根据scope查询,时间复杂度为O(logn)
    • image-20230510230629672 image-20230510230629672
    • 每个跳表都必须设定一个最大的连接层数MaxLevel
    • 第一层连接会连接到表中的每个元素
    • 插入一个元素会随机生成一个连接层数值[1, MaxLevel]之间,根据这个值跳表会给这元素建立N个连接
    • 插入某个元素的时候先从最高层开始,当跳到比目标值大的元素后,回退到上一个元素,用该元素的下一层连接进行遍历,周而复始直到第一层连接,最终在第一层连接中找到合适的位置
  • zrank命令实现:
    • 命令描述:返回有序集中指定成员的排名
      • 1、跳表的每个元素的Next指针都记录了这个指针能够跨越多少元素,redis在插入和删除元素的时候,都会更新这个值
      • 2、搜索的过程中按经过的路径将路径中的span值相加得到rank
hash
  • hash这种结构有两种表示:zipmap和dict
  • zipmap:压缩map
    • 格式如:“foo”“bar”“hello”“world”,表示"foo" => “bar”, “hello” => “world”
    • 各部分的含义如下:
      • zmlen:1个字节,表示zipmap的总字节数
      • len:1~5个字节,表示接下来存储的字符串长度
      • free:1个字节,是一个无符号的8位数,表示字符串后面的空闲未使用字节数,由于修改与键对应的值而产生
    • 这种方式的缺点也很明显,就是查找的时间复杂度为O(n),所以只能当作一个轻量级的hashmap来使用
  • dict:字典,类似Java的HashMap的结构,数组+链表的实现
    • dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。
    • 在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。
    • dict可以扩容(占满时)、缩容(不足10%时),rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的
    • 渐进式rehash:
      • dict有rehash标志位,标志当前dict是否正在rehash
      • 在rehash期间,若此时有redis命令执行该dict,顺带进行单步rehash(类似helpHash)
      • 若redis空闲,没有客户端命令触发rehash,则通过定时任务进行rehash
Set
  • 如果对象的值可以编码为整数的话,则通过intset 实现(int集合,使用int数组存储,查询使用二分法)
  • 如果不能转换成整数,则通过hashtable实现,和Java的HashSet实现类似
BitMap
  • String 类型作为底层数据结构实现,String 类型是保存为二进制的字节数组,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,可以把 Bitmap 看作是一个 bit 数组
  • 应用场景:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • 可进行与、或、异或、取反运算,也可以合并多个bitmap
HyperLogLog

伯努利试验

硬币拥有正反两面,一次的上抛至落下,最终出现正反面的概率都是50%。假设一直抛硬币,直到它出现正面为止,我们记录为一次完整的试验,间中可能抛了一次就出现了正面,也可能抛了4次才出现正面。无论抛了多少次,只要出现了正面,就记录为一次试验。这个试验就是伯努利试验

那么对于多次的伯努利试验,假设这个多次为n次。就意味着出现了n次的正面。假设每次伯努利试验所经历了的抛掷次数为k。第一次伯努利试验,次数设为k1,以此类推,第n次对应的是kn

其中,对于这n伯努利试验中,必然会有一个最大的抛掷次数k,例如抛了12次才出现正面,那么称这个为k_max`,代表抛了最多的次数。

伯努利试验容易得出有以下结论:

  1. n 次伯努利过程的投掷次数都不大于 k_max。
  2. n 次伯努利过程,至少有一次投掷次数等于 k_max

最终结合极大似然估算的方法,发现在nk_max中存在估算关联:$n = 2^{k_max} $

  • 存储时使用的是字符串的形式进行存储,其中字符串分成16384个每个6bit的桶位,每个桶位保存hash的bit位连续0的数量
  • 每个 HyperLogLog 键只需要花费16384*6=12 KB 内存,就可以计算接近 2^64 个不同元素的基数
  • 节省内存依据:通过概率统计实现,牺牲一定的准确度。
    • 计算机中数据以二进制存储,每个bit位是0或1,在大数据量的情况下每个bit位的值是1或者0可以当成是随机事件
    • 依据伯努利实验,把bit位的1当成硬币正面,每个集合元素当成每次实验,统计bit位连续0的最大值,则可以估算集合的数量
    • redis为了消除误差,将数据集合进行分桶,分成16384($2^{14}$)个小集合,然后对所有小集合的计算结果进行调和平均数计算得出最终的集合基数
      1. 使用MurmurHash64A计算出value的64位hash
      2. hash的低14位用于分桶(所以桶数是$2^{14}$),从hash值第15位开始统计bit位连续0的数量K
      3. 比较K与桶位的旧值,把较大的值存到桶位。这里相当于记录伯努利实验的k_max
      4. 估算集合基数时,分别计算每个桶位的k_max对应的集合基数,然后把所有桶位的计算结果进行调和平均数计算
GEO
  • 底层是使用zset进行实现,scope为geohash,value为key
  • geohash是将空间(经度、纬度)循环二分,每分割一次进行一次编码,所以分割越多次编码越长越精细,然后将经纬度的编码按照(偶数位放经度,奇数位放纬度)组合成hash
    • 处理边界问题可以在查询时同时查询相邻格子解决
    • 处理突变问题,可以对查询结果再进行一次距离计算,剔除掉不符合的空间(根据geohash查询已经过滤掉大部分数据)
Stream

高可用

  1. 主从复制:通过使用主从复制,实现数据的冗余备份和读取负载均衡。当主节点发生故障时,通过自动或手动将一个从节点提升为主节点来实现故障转移。
  2. 哨兵模式:哨兵可以自动检测主节点状态,发生故障时哨兵通过一定的策略选举出新的主节点实现故障转移
  3. cluster集群:redis分布式部署,通过在多个节点上分片存储数据的方式,内置的分片机制和故障转移机制,可以在节点故障时自动进行数据迁移和故障转移

主从复制

通过主从可以实现读写分离,同时从数据库的数据备份提供了故障容错。缺点是故障时不能自动故障转移,需要手动调整主节点

哨兵模式

哨兵是独立的服务进程,对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 系统的可用性。 Redis 哨兵具备的能力:

  • 监控:持续监控 master 、slave 是否处于预期工作状态
  • **自动切换主库:**当 Master 运行故障,哨兵启动自动故障恢复流程:从 slave 中选择一台作为新 master。
  • **通知:**让 slave 执行 replicaof ,与新的 master 同步;并且通知客户端与新 master 建立连接。
哨兵配置

为了实现高可用,哨兵也需要实现高可用,避免哨兵宕机而失去监控redis集群的能力,正常哨兵的实例个数为奇数。

  • 为什么是奇数:集群发生宕机时,超过半数节点正常的情况下集群可以恢复系统。
  • 宕机相同数量节点的情况下,奇数和偶数是一样的,奇数可以节省一个节点。如:总3-挂1正常-挂2不正常;总4-挂1正常-挂2不正常
哨兵监控、哨兵通讯

哨兵配置只指定master的地址,哨兵通过心跳持续检测master节点的状态。并且心跳包返回了整个主从关系的节点,包括ip和端口号,通过这些信息哨兵可以和slave建立连接;

哨兵之间通过redis的Redis提供的发布(pub)/订阅(sub)机制完成。哨兵节点不会直接与其他哨兵节点建立连接,而是首先会和主库建立起连接,然后向一个名为"sentinel:hello"频道发送自己的信息(IP 和端口),其他订阅了该频道的哨兵节点就会获取到该哨兵节点信息,从而哨兵节点之间互知。

哨兵切换主库
  1. 主观下线:当某个哨兵发送心跳后没收到回复后,这个哨兵把这个主服务器标记为下线,此时只有一个哨兵标记为下线,一个哨兵没有收到回复并不能证明主服务器宕机,这就是主观下线;

  2. 客观下线:当其他哨兵同样没收到主服务器的回复后,也标记为下线。哨兵之间交流后认为已经有足够数量的实例证明该服务已经不可用,这就是客观下线。主节点客观下线之后,哨兵集群自动进行主从切换

  3. 选举哨兵leader:主从切换只需要交给其中一个哨兵即可完成,哨兵也是集群部署所以需要选举出一个哨兵leader执行这个操作。哨兵选举leader的过程类似于Raft算法,每个哨兵都设置一个随机超时时间,超时后向其他哨兵发送申请成为领导者的请求,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况

  4. 选举redis集群master:选择新master过程也是有优先级的,在多个slave场景下,优先级按照:slave-priority配置 > 数据完整性 > runid较小者进行选择。

  5. 选择出新主库,哨兵 Leader 会给该节点发送slaveof no one命令,让该节点成为 Master。之后,哨兵 Leader会给故障节点的所有 Slave 发送slaveof $newmaster命令,让这些 Slave 成为新 Master 的从节点,开始从新的Master 上同步数据(这里会进行全量复制)。最后哨兵 Leader 把故障节点降级为 Slave,并写入到自己的配置文件中,待这个故障节点恢复后,则自动成为新 Master 节点的 Slave。至此,整个故障切换完成。

  6. 通知客户端:基于Redis提供的发布(pub)/订阅(sub)机制完成的,客户端可以从哨兵订阅消息,故障转移后,客户端会收到订阅消息。

redis集群脑裂

脑裂是指在主从集群中,同时有两个以上主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失

  1. 出现脑裂原因
    • 网络问题:分布式系统中常见的问题网络分区,master节点与哨兵节点不在同个网络分区下,哨兵认为master客观下线而进行主节点切换
    • 主机资源问题:服务器资源临时被其他程序大量占用,导致master节点暂时无法做出心跳响应
    • Redis 主节点阻塞:主库自身遇到了阻塞的情况,例如,处理 bigkey 或是发生内存 swap,短时间内无法响应心跳
  2. 如何避免脑裂现象
    1. Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag
      • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量,即至少要保证N个从库能进行数据同步;
      • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)
    2. 当不满足这两个配置后master就不会再接收客户端的请求了,客户端不写数据到master,就不会造成数据丢失
  3. 配置:min-slaves-to-write 设置为 N/2+1, min-slaves-max-lag 设置为十几秒(例如 10~20s)

cluster集群

  • redis自带的集群方案,是一种去中心化的集群方案。
  • 集群的数据分布是通过hash slot算法,将键值空间分为16384个slot,所有节点均衡去负责部分slot;
  • 节点主要工作:处理客户端请求;记录集群状态;集群维护(发现新节点、剔除坏节点、选举主节点)
  • 集群的稳定由节点之间的内部通信完成完成,节点之间使用gossip协议建立cluster bus,不断发送数据包完成节点之间的 集群信息同步、节点发现、存活检测、发布订阅等;
  • 客户端可以访问到集群中所有节点,数据处理数据时需要client和server协调定位到负责处理的节点,比如:客户端请求的节点发现该key对应的slot不在自己上,则返回ask或move告诉客户端去其他节点;
  • 节点可以是主从实现高可用
  • 节点失效下线时,会在一定时间完成slot的迁移。失效下线和新节点上线是一样的,都是可以说是cluster的在线重配置。先标记迁出的slot、另一个节点标记导入的slot;接着进入迁移阶段,使用原子操作按key进行迁移;迁出的slot会处理迁移期间的key,若迁移期间key已迁出则返回ask;导入的slot只处理asking的key,否则返回move

持久化机制

AOF

当服务器执行指令时,将命令追加到 AOF 日志文件。当 Redis 重新启动时,他会在本地启动一个伪客户端,并按顺序重新发送日志中的命令以恢复数据

  • AOF时机先执行指令,然后再写 AOF 日志

    • MySQL 中的 binlog 是写前日志(Write Ahead Log, WAL),即先写日志再保存数据,而 AOF 日志则是写后日志,即先保存数据再写日志。
    • 优点:可以确保AOF都是没有错误的指令,不用额外的语法/类型检查,不用考虑回滚日志;不因为写日志而阻塞当前指令的执行;
    • 缺点:执行完指令后宕机来不及写AOF,AOF日志会丢失;Redis 命令由单个线程执行,因此可能因为写日志而阻塞后一条指令的执行;
  • AOF刷盘策略

    • 刷盘操作:

      • 执行写指令后,日志先写入 Redis 自己的 AOF 缓冲区;
      • 然后Redis调用操作系统的 write 函数从 AOF 缓冲区写入操作系统缓冲区;
      • 最后Redis调用 fsync 函数或操作系统自己刷盘,让内核缓冲区中的数据真正写入磁盘;
    • 第三步为日志刷盘fsync之前日志都在内存里,宕机就出现永久性丢失

    • 通过 appendfsync 配置刷盘策略:

      指令时机性能宕机时丢失的数据
      AOF_FSYNC_NO不主动刷盘,由操作系统自己决定刷盘时机所有未写入磁盘的数据
      AOF_FSYNC_EVERYSEC每秒保存一次一秒内的数据
      AOF_FSYNC_ALWAYS每个命令执行后保存一次一条指令的数据
  • AOF重写

    • 原理:AOF 文件会越来越大,AOF 提供重写机制,即当 AOF 文件膨胀到一定程度时,Redis fork子进程重新扫描当前数据库中的数据,然后把它们重写到一个新 AOF 文件中,并替换旧的 AOF 文件,这个新的 AOF 文件会比原本的文件更小
    • 时机自动判断或者BGREWRITEAOF命令触发
      • 状态:当前没有正在执行的 AOF 重写或 RDB 生成操作;
      • 配置:当前的 AOF 文件大于 server.aof_rewrite_min_size 配置;
      • 配置:当前 AOF 的文件大小比最后一次 AOF 重写后的文件膨胀大小满足 AOF 重写的增值比例;
    • 增量数据处理
      • 7.0 之前:主进程同时写两份 AOF 文件,一份是正常的AOF缓存,一份是额外的AOF重写缓存。重写完成后,主线程将AOF重写缓存追加到新的AOF文件,然后覆盖就得AOF文件
      • 7.0 之后:主进程将增量数据写到新的增量 AOF 文件,重写完成后主线程将两个AOF文件合并
  • 混合持久化

    • AOF 恢复数据相对比较耗时,Redis 在 4.0 以后允许通过 aof‐use‐rdb‐preamble 配置开启混合持久化
    • 当 AOF 重写时,先生成 RDB 快照,然后将其写入新的 AOF 文件,再把增量数据追加到这个新 AOF 文件中。如此一来,当 Redis 通过 AOF 文件恢复数据时,将会先加载 RDB,然后再重放后半部分的增量数据。这样就可以大幅度提高数据恢复的速度。
RDB
  • 时机

    • save命令,bgsave命令
    • save配置项
  • 原理

    • 某个时刻的全量数据快照。以二进制的方式保存了某个时刻 Redis 数据库中的全部数据,Redis 启动后只需要将其加载进内存即可恢复数据。
    • 生成RDB快照时,redisfork子进程进行通过写时复制机制,将内存中的数据通过紧凑的二进制的形式生成文件

应用场景

接口缓存

token共享

实时聊天-不适用

排行榜

通过zset实现,zset首先根据score排序,再更具value进行ASCII排序

  • zrange 实现topN
  • zrank 实现查询具体成员实时排名
  • zrank+zrange实现具体成员排名上下n个成员
  • zunionstore+分小时一个key实现
  • 设计周、天、月排行榜,rediskey按小时分key,再使用zunionstore合并集合统计
  • 相同分数排行榜:
    • 参考方案
    • 方案:调整score=分数+时间差
      • score为64双精度浮点数,有效数据位为16十进制数
      • 方案1:以十进制对整数位进行划分分值范围小
        • Redis ZSet的score值在超过$2^{54}$后存储和计算会出现问题,保险起见,采用$2^{53}$最为最大整数:9007 1992 5474 0992
        • 秒级时间戳需要10位,因此低10位由秒级时间戳填充
        • 高6位则可以由分值填充,但分值最大为900718
      • 方案2:以二进制对整数位进行划分与方案一差不多,分值范围大一些
        • 仍旧是$2^{53}$作为最大整数
        • 采用低32作为时间戳(可表示到2106-02-07 14:28:16)
        • 剩余高21位作为分值(可表示到2097152)
      • 方案3:利用double的浮点计算
        • double的有效位可以保证在15位以上
        • 将分值作为score的整数部分
        • 将时间戳逆向后作为score的小数部分
        • 优点:可读性较高;利用浮点数特点,分值取值范围可以很大(起码2^53没有问题了)
        • 缺点:也是因为浮点数特点,随着分值(整数部分)的逐渐增大,时间戳(小数部分)精度逐渐变小
        • 对于分值上限小(百万级别以下)的业务场景,方案3可以保障时间高精度
        • 对于分值上限高(千万级别以上)的业务场景,分值往往是从小累计到大的且在小分值时竞争激烈(容易出现同分多,达到同分的时间间隔小的情况)大分值时则不激烈,采用方案3可以在分值较小时仍旧保障时间高精度,分值大时一般对时间精度要求也低了
        • 另外还可以根据业务来收缩时间戳(小数部分)的范围来扩大秒级时间精度下的分值(整数部分)范围
  • 多维度排行榜:拆分score,将多个维度放到score中,与并列排行类似

应用计数,播放数、浏览数

分布式锁

限流

位统计:天\周\月\年内连续登录的用户

全局id

优缺点

拓展

Redis相比memcached优势

  • memcached数据类型单一,redis支持丰富的数据类型
  • redis速度比mencached快
  • redis支持持久化

一个字符串类型的值能存储最大容量

  • 512M

Redis 集群方案什么情况下会导致整个集群不可用?

  • 至少有一个哈希槽不可用;大部分主节点都进入下线状态;

MySQL 里有 2000w 数据,redis 中只存 20w 的数据,保证 redis 中是热点数据

  • 这个可以通过设置合适的淘汰策略,然后限制redis的内存实现。内存不足了redis自动根据淘汰策略镜像数据淘汰

Redis 集群的写操作丢失

  • redis cluster并不能保证数据的强一致性,这表示在某些特定情况下可能会出现写入丢失;比如:集群出现网络分区,并且有从节点被选举称为主节点,那么分区恢复后写入源主节点的数据就会丢失

Redis 集群最大节点个数

  • 16384,2的14次幂
  • redis节点发送心跳包时需要把所有的槽放到这个心跳包里,发送心跳包时使用2k的bitmap进行压缩
  • 一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择

Redis 常见性能问题和解决方案?

  • master 不做持久化
  • 保护数据场景下,可用某些slave开启aof
  • 最好master-slave都在同个局域网
  • 尽量使用一主多从

缓存和数据库的⼀致性问题

  1. 想要提高应用的性能,可以引入「缓存」来解决

  2. 引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」

  3. 更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,解决方案是加「分布锁」,但存在「缓存资源浪费」和「机器性能浪费」的情况

  4. 采用「先删除缓存,再更新数据库」方案,在「并发」场景下依旧有不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估

    • 延迟双删:先删除缓存,再更新数据库,再删一次缓存。第二次删除不是立即删,而是延迟一定时间后删除,保证DB数据一致后再删
  5. 采用「先更新数据库,再删除缓存」方案,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据最终一致

  6. 采用「先更新数据库,再删除缓存」方案,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率

  • 性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案
  • 掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率并发缓存 + 数据库一起成功问题
  • 失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案
  • 订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致