速度快,完全基于内存,使用 C 语言实现,网络层使用 epoll 解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件;
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
1)完全基于内存,绝大部分请求是纯粹的内存操作。数据存在内存中,类似于 HashMap,查找和操作的时间复杂度都是 O(1);
2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3)采用单线程,避免了多线程不必要的上下文切换和竞争条件,不存在加锁释放锁操作,减少了因为锁竞争导致的性能消耗;(6.0 以后多线程)
4)使用 EPOLL 多路 I/O 复用模型,非阻塞 IO;
5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
使用场景:
1、如果有持久方面的需求或对数据类型和处理有要求的应该选择 redis。
2、如果简单的 key/value 存储应该选择 memcached。
Tair(Taobao Pair) 是淘宝开发的分布式 Key-Value 存储引擎,既可以做缓存也可以做数据源(三种引擎切换)
大访问少量临时数据的存储(kb 左右)
用于缓存,降低对后端数据库的访问压力
高速访问某些数据结构的应用和计算(rdb)
快速读取数据(fdb)
持续大数据量的存入读取(ldb),交易快照
高频度的更新读取(ldb),库存
**痛点:**redis 集群中,想借用缓存资源必须得指明 redis 服务器地址去要。这就增加了程序的维护复杂度。因为 redis 服务器很可能是需要频繁变动的。所以人家淘宝就想啊,为什么不能像操作分布式数据库或者 hadoop 那样。增加一个中央节点,让他去代理所有事情。在 tair 中程序只要跟 tair 中心节点交互就 OK 了。同时 tair 里还有配置服务器概念。又免去了像操作 hadoop 那样,还得每台 hadoop 一套一模一样配置文件。改配置文件得整个集群都跟着改。
分布式缓存一致性更好一点,用于集群环境下多节点使用同一份缓存的情况;有网络 IO,吞吐率与缓存的数据大小有较大关系;
本地缓存非常高效,本地缓存会占用堆内存,影响垃圾回收、影响系统性能。
以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况,每个实例都需要各自保存一份缓存,缓存不具有一致性。
解决缓存过期:
1、将缓存过期时间调为永久;
2、将缓存失效时间分散开,不要将缓存时间长度都设置成一样;比如我们可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
解决内存溢出:
第一步,修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx 参数一定不要忘记加。)
第二步,检查错误日志,查看 “OutOfMemory” 错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
Google Guava Cache
自己设计本地缓存痛点:
Guava Cache 的场景:
Guava Cache 的优势:
在 GuavaCache 中可以设置 Key 的过期时间,包括访问过期和创建过期。GuavaCache 在缓存容量达到指定大小时,采用 LRU 的方式,将不常使用的键值从 Cache 中删除。
GuavaCache 类似 CurrentHashMap,是线程安全的。提供了设置并发级别的 api,使得缓存支持并发的写入和读取,采用分离锁机制,分离锁能够减小锁力度,提升并发能力,分离锁是分拆锁定,把一个集合看分成若干 partition, 每个 partiton 一把锁。更新锁定
一般情况下,在缓存中查询某个 key,如果不存在,则查源数据,并回填缓存。(Cache Aside Pattern)在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降 GuavaCache 可以在 CacheLoader 的 load 方法中加以控制,对同一个 key,只让一个请求去读源并回填缓存,其他请求阻塞等待。(相当于集成数据源,方便用户使用)
统计
问题:
OOM-> 设置过期时间、使用弱引用、配置过期策略
EVCache 是一个 Netflflix(网飞)公司开源、快速的分布式缓存,是基于 Memcached 的内存存储实现的,用以构建超大容量、高性能、低延时、跨区域的全球可用的缓存数据层。
E:Ephemeral:数据存储是短暂的,有自身的存活时间
V:Volatile:数据可以在任何时候消失
EVCache 典型地适合对强一致性没有必须要求的场合
典型用例:Netflflix 向用户推荐用户感兴趣的电影
EVCache 集群在峰值每秒可以处理 200kb 的请求,
Netflflix 生产系统中部署的 EVCache 经常要处理超过每秒 3000 万个请求,存储数十亿个对象,
跨数千台 memcached 服务器。整个 EVCache 集群每天处理近 2 万亿个请求。
EVCache 集群响应平均延时大约是 1-5 毫秒,最多不会超过 20 毫秒。
EVCache 集群的缓存命中率在 99% 左右。
典型部署
EVCache 是线性扩展的,可以在一分钟之内完成扩容,在几分钟之内完成负载均衡和缓存预热。
1、集群启动时,EVCache 向服务注册中心(Zookeeper、Eureka)注册各个实例
2、在 web 应用启动时,查询命名服务中的 EVCache 服务器列表,并建立连接。
3、客户端通过 key 使用一致性 hash 算法,将数据分片到集群上。
和 Zookeeper 一样,CP 模型追求数据一致性,越来越多的系统开始用它保存关键数据。比如,秒杀系统经常用它保存各节点信息,以便控制消费 MQ 的服务数量。还有些业务系统的配置数据,也会通过 etcd 实时同步给业务系统的各节点,比如,秒杀管理后台会使用 etcd 将秒杀活动的配置数据实时同步给秒杀 API 服务各节点。
1、redis 数据类型
http://redisdoc.com
struct sdshdr{
int len;//记录buf数组中已使用字节的数量
int free; //记录 buf 数组中未使用字节的数量
char buf[];//字符数组,用于保存字符串
}
1、可以快速查找到需要的节点 O(logn) ,额外存储了一倍的空间
2、可以在 O(1) 的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。
Redis 整个数据库是用字典来存储的 (K-V 结构) —Hash + 数组 + 链表
Redis 字典实现包括: 字典 (dict)、Hash 表 (dictht)、Hash 表节点 (dictEntry)。
字典达到存储上限 (阈值 0.75),需要 rehash(扩容)
1、初次申请默认容量为 4 个 dictEntry,非初次申请为当前 hash 表容量的一倍。
2、rehashidx=0 表示要进行 rehash 操作。
3、新增加的数据在新的 hash 表 h[1] 、修改、删除、查询在老 hash 表 h[0]
4、将老的 hash 表 h[0] 的数据重新计算索引值后全部迁移到新的 hash 表 h[1] 中,这个过程称为 rehash。
渐进式 rehash
由于当数据量巨大时rehash的过程是非常缓慢的,所以需要进行优化。 可根据服务器空闲程度批量rehash部分节点
压缩列表 (ziplist) 是由一系列特殊编码的连续内存块组成的顺序型数据结构,节省内容
sorted-set 和 hash 元素个数少且是小整数或短字符串 (直接使用)
list 用快速链表 (quicklist) 数据结构存储,而快速链表是双向列表与压缩列表的组合。(间接使用)
整数集合 (intset) 是一个有序的(整数升序)、存储整数的连续存储结构。
当 Redis 集合类型的元素都是整数并且都处在 64 位有符号整数范围内 (2^64),使用该结构体存储。
快速列表 quickList
快速列表 (quicklist) 是 Redis 底层重要的数据结构。是 Redis3.2 列表的底层实现。
(在 Redis3.2 之前,Redis 采 用双向链表 (adlist) 和压缩列表 (ziplist) 实现。)
Redis Stream 的底层主要使用了 listpack(紧凑列表) 和 Rax 树 (基数树)。
listpack 表示一个字符串列表的序列化,listpack 可用于存储字符串或整数。用于存储 stream 的消息内容。
Rax 树是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。
跳表 (skip List) 是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为 O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供 O(logN)的时间复杂度。
Zset 数据量少的时候使用压缩链表 ziplist 实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存 member,第二个保存 score。ziplist 内的集合元素按 score 从小到大排序,score 较小的排在表头位置。 数据量大的时候使用跳跃列表 skiplist 和哈希表 hash_map 结合实现,查找删除插入的时间复杂度都是 O(longN)。
Redis 使用跳表而不使用红黑树,是因为跳表的索引结构序列化和反序列化更加快速,方便持久化。
搜索
跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 O(logN),最坏 O(N) 。
插入
选用链表作为底层结构支持,为了高效地动态增删。因为跳表底层的单链表是有序的,为了维护这种有序性,在插入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是 O(n),同理可得,跳表的遍历也是需要遍历索引数,所以是 O(logn)。
删除
如果该节点还在索引中,删除时不仅要删除单链表中的节点,还要删除索引中的节点;单链表在知道删除的节点是谁时,时间复杂度为 O(1),但针对单链表来说,删除时都需要拿到前驱节点 O(logN) 才可改变引用关系从而删除目标节点。
持久化就是把内存中的数据持久化到本地磁盘,防止服务器宕机了内存数据丢失。
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制,Redis4.0 以后采用混合持久化,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备。
RDB 是 Redis 默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为 dump.rdb。通过配置文件中的 save 参数来定义快照的周期。
优点:
1)只有一个文件 dump.rdb,方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘。
3)性能最大化,fork 子进程来进行持久化写操作,让主进程继续处理命令,只存在毫秒级不响应请求。
4)相对于数据集大时,比 AOF 的启动效率更高。
缺点:
数据安全性低,RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
AOF 持久化 (即 Append Only File 持久化),则是将 Redis 执行的每次写命令记录到单独的日志文件中,当重启 Redis 会重新将持久化的日志中文件恢复数据。
优点:
1)数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
缺点:
1)AOF 文件比 RDB 文件大,且恢复速度慢。
2)数据集大的时候,比 rdb 启动效率低。
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis 事务的本质是通过 MULTI、EXEC、WATCH 等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis 的事务总是具有 ACID 中的一致性和隔离性,其他特性是不支持的。当服务器运行在 AOF 持久化模式下,并且 appendfsync 选项的值为 always 时,事务也具有耐久性(持久性)。
Redis 事务功能是通过 MULTI、EXEC、DISCARD 和 WATCH 四个原语实现的。
**MULTI:**用于开启一个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。
**EXEC:**执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
**WATCH :**是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到 EXEC 命令。(秒杀场景)
**DISCARD:**调用该命令,客户端可以清空事务队列,并放弃执行事务,且客户端会从事务状态中退出。
**UNWATCH:**命令可以取消 watch 对所有 key 的监控。
内存淘汰策略,当redis到达最大内存限制时,会执行缓存失效策略
缓存失效策略
定时清除: 针对每个设置过期时间的 key 都创建指定定时器
惰性清除: 访问时判断,对内存不友好
定时扫描清除: 定时 100ms 随机 20 个检查过期的字典,若存在 25% 以上则继续循环删除。
1)全局的键空间选择性移除
**noeviction:**当内存不足以容纳新写入数据时,新写入操作会报错。(字典库常用)
**allkeys-lru:**在键空间中,移除最近最少使用的 key。(缓存常用)
**allkeys-random:**在键空间中,随机移除某个 key。
2)设置过期时间的键空间选择性移除
**volatile-lru:**在设置了过期时间的键空间中,移除最近最少使用的 key。
**volatile-random:**在设置了过期时间的键空间中,随机移除某个 key。
**volatile-ttl:**在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
CacheAside 旁路缓存
写请求更新数据库后删除缓存数据。读请求不命中查询数据库,查询完成写入缓存
业务端处理所有数据访问细节,同时利用 Lazy 计算的思想,更新 DB 后,直接删除 cache 并通过 DB 更新,确保数据以 DB 结果为准,则可以大幅降低 cache 和 DB 中数据不一致的概率
如果没有专门的存储服务,同时是对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,适合使用 Cache Aside 模式。如微博发展初期,不少业务采用这种模式
// 延迟双删,用以保证最终一致性,防止小概率旧数据读请求在第一次删除后更新数据库
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
高并发下保证绝对的一致,先删缓存再更新数据,需要用到内存队列做异步串行化。非高并发场景,先更新数据再删除缓存,延迟双删策略基本满足了
先查询缓存中数据是否存在, 如果存在则直接返回, 如果不存在, 则由缓存组件负责从数据库中同步加载数据。
先查询要写入的数据在缓存中是否已经存在, 如果已经存在, 则更新缓存中的数据,并且由缓存组件同步更新到数据库中。
用户读操作较多. 相较于 Cache aside 而言更适合缓存一致的场景。使用简单屏蔽了底层数据库的操作, 只是操作缓存。
场景:
微博 Feed 的 Outbox Vector(即用户最新微博列表)就采用这种模式。一些粉丝较少且不活跃的用户发表微博后,Vector 服务会首先查询 Vector Cache,如果 cache 中没有该用户的 Outbox 记录,则不写该用户的 cache 数据,直接更新 DB 后就返回,只有 cache 中存在才会通过 CAS 指令进行更新。
Write Behind Caching(异步缓存写入)
比如对一些计数业务,一条 Feed 被点赞 1 万 次,如果更新 1 万 次 DB 代价很大,而合并成一次请求直接加 1 万,则是一个非常轻量的操作。但这种模型有个显著的缺点,即数据的一致性变差,甚至在一些极端场景下可能会丢失数据。
浏览器本地内存缓存: 专题活动,一旦上线,在活动期间是不会随意变更的。
浏览器本地磁盘缓存: Logo 缓存,大图片懒加载
**服务端本地内存缓存:**由于没有持久化,重启时必定会被穿透
**服务端网络内存缓存:**Redis 等,针对穿透的情况下可以继续分层,必须保证数据库不被压垮
为什么不是使用服务器本地磁盘做缓存?
当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 iowait。
指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
https://www.cdsy.xyz/computer/soft/database/redis/20220219/cd164527597833047.html
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截;
2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒。这样可以防止攻击用户反复用同一个 id 暴力攻击;
3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。(宁可错杀一千不可放过一人)
这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1)设置热点数据永远不过期,异步线程处理。
2)加写回操作加互斥锁,查询失败默认值快速返回。
3)缓存预热
系统上线后,将相关**可预期(例如排行榜)**热点数据直接加载到缓存。
写一个缓存刷新页面,手动操作热点数据**(例如广告推广)**上下线。
在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和 DB 的数据不一致。缓存 rehash 时,某个缓存机器反复异常,多次上下线,更新请求多次 rehash。这样,一份数据存在多个节点,且每次 rehash 只更新某个节点,导致一些缓存节点产生脏数据。
数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成并发竞争读取的问题。
明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双 12、618 等线上促销活动,都很容易出现 Hot key 的情况。
如何提前发现 HotKey?
解决方案:
比如互联网系统中需要保存用户最新 1 万 个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发 feed 统计等。微博的 feed 内容缓存也很容易出现,一般用户微博在 140 字以内,但很多用户也会发表 1 千 字甚至更长的微博内容,这些长微博也就成了大 key
客户端分片:哈希 + 取余
节点伸缩:数据节点关系变化,导致数据迁移
迁移数量和添加节点数量有关:建议翻倍扩容
一个简单直观的想法是直接用 Hash 来计算,以 Key 做哈希后对节点数取模。可以看出,在 key 足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。
客户端分片:哈希 + 顺时针(优化取余)
节点伸缩:只影响邻近节点,但是还是有数据迁移
翻倍伸缩:保证最小迁移数据和负载均衡
一致性 Hash 可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的 Hash 环上,每个 key 在计算 Hash 后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在 Hash 环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这又带来均匀性的问题,即使可以将存储节点等距排列,也会在存储节点个数变化时带来数据的不均匀。
Codis 将所有的 key 默认划分为 1024 个槽位 (slot),它首先对客户端传过来的 key 进行 crc32 运算计算 哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。
Redis-cluster 把所有的物理节点映射到 [0-16383] 个 slot 上, 对 key 采用 crc16 算法得到 hash 值后对 16384 取模,基本上采用平均分配和连续分配的方式。
主从模式最大的优点是部署简单,最少两个节点便可以构成主从模式,并且可以通过读写分离避免读和写同时不可用。不过,一旦 Master 节点出现故障,主从节点就无法自动切换,直接导致 SLA 下降。所以,主从模式一般适合业务发展初期,并发量低,运维成本低的情况
主从复制原理:
①通过从服务器发送到 PSYNC 命令给主服务器;
②如果是首次连接,触发一次全量复制。此时主节点会启动一个后台线程,生成 RDB 快照文件;
③主节点会将这个 RDB 发送给从节点,slave 会先写入本地磁盘,再从本地磁盘加载到内存中;
④master 会将此过程中的写命令写入缓存,从节点实时同步这些数据;
⑤如果网络断开了连接,自动重连后主节点通过命令传播增量复制给从节点部分缺少的数据;
缺点
所有的 slave 节点数据的复制和同步都由 master 节点来处理,会照成 master 节点压力太大,使用主从从结构来解决,redis4.0 中引入 psync2 解决了 slave 重启后仍然可以增量同步。
由一个或多个 sentinel 实例组成 sentinel 集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景,比如在秒杀系统中用来缓存活动信息。如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。
当主服务器进入下线状态时,sentinel 可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证 redis 的高可用性。
Sentinel 每秒一次向所有与它建立了命令连接的实例 (主服务器、从服务器和其他 Sentinel) 发送 PING 命 令
实例在 down-after-milliseconds 毫秒内返回无效回复 Sentinel 就会认为该实例主观下线 (SDown)
当一个 Sentinel 将一个主服务器判断为主观下线后 ,Sentinel 会向监控这个主服务器的所有其他 Sentinel 发送查询主机状态的命令
如果达到 Sentinel 配置中的 quorum 数量的 Sentinel 实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线 (ODown)。
当一个主服务器被判定为客观下线后,监视这个主服务器的所有 Sentinel 会通过选举算法 (raft),选出一个 Leader Sentinel 去执行 failover(故障转移) 操作。
Raft 协议是用来解决分布式系统一致性问题的协议。Raft 协议描述的节点共有三种状态: Leader, Follower, Candidate。Raft 协议将时间切分为一个个的 Term(任期),可以认为是一种 “逻辑时间”。选举流程:
①Raft 采用心跳机制触发 Leader 选举系统启动后,全部节点初始化为 Follower,term 为 0
②节点如果收到了 RequestVote 或者 AppendEntries,就会保持自己的 Follower 身份
③节点如果一段时间内没收到 AppendEntries 消息,在该节点的超时时间内还没发现 Leader,Follower 就会转换成 Candidate,自己开始竞选 Leader。一旦转化为 Candidate,该节点立即开始下面几件事情:
– 增加自己的 term,启动一个新的定时器;
– 给自己投一票,向所有其他节点发送 RequestVote,并等待其他节点的回复。
④如果在计时器超时前,节点收到多数节点的同意投票,就转换成 Leader。同时通过 AppendEntries,向其他节点发送通知。
⑤每个节点在一个 term 内只能投一票,采取先到先得的策略,Candidate 投自己, Follower 会投给第一个收到 RequestVote 的节点。
⑥Raft 协议的定时器采取随机超时时间(选举的关键),先转为 Candidate 的节点会先发起投票,从而获得多数票。
当选举出 Leader Sentinel 后,Leader Sentinel 会根据以下规则去从服务器中选择出新的主服务器。
当 Leader Sentinel 完成新的主服务器选择后,Leader Sentinel 会对下线的主服务器执行故障转移操作,主要有三个步骤:
1、它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;
2、当客户端试图连接失效的 Master 时,集群会向客户端返回新 Master 的地址,使得集群当前状态只有一个 Master。
3、Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即 Master 主服务器的 redis.conf 配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。
为了避免单一节点负载过高导致不稳定,集群模式采用一致性哈希算法或者哈希槽的方法将 Key 分布到各个节点上。其中,每个 Master 节点后跟若干个 Slave 节点,用于出现故障时做主备切换,客户端可以连接任意 Master 节点,集群内部会按照不同 key 将请求转发到不同的 Master 节点
集群模式是如何实现高可用的呢?集群内部节点之间会互相定时探测对方是否存活,如果多数节点判断某个节点挂了,则会将其踢出集群,然后从 Slave 节点中选举出一个节点替补挂掉的 Master 节点。整个原理基本和哨兵模式一致。
虽然集群模式避免了 Master 单节点的问题,但集群内同步数据时会占用一定的带宽。所以,只有在写操作比较多的情况下人们才使用集群模式,其他大多数情况,使用哨兵模式都能满足需求
利用 Watch 实现 Redis 乐观锁
乐观锁基于 CAS(Compare And Swap) 比较并替换思想,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用 redis 来实现乐观锁(秒杀)。具体思路如下:
1、利用 redis 的 watch 功能,监控这个 redisKey 的状态值
2、获取 redisKey 的值,创建 redis 事务,给这个 key 的值 + 1
3、执行这个事务,如果 key 的值被修改过则回滚,key 不加 1
利用 setnx 防止库存超卖
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 利用 Redis 的单线程特性对共享资源进行串行化处理
// 获取锁推荐使用set的方式
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
String result = jedis.setnx(lockKey, requestId); //如线程死掉,其他线程无法获取到锁
// 释放锁,非原子操作,可能会释放其他线程刚加上的锁
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
// 推荐使用redis+lua脚本
String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object result = jedis.eval(lua, Collections.singletonList(lockKey),
分布式锁存在的问题:
计算时间内异步启动另外一个线程去检查的问题,这个 key 是否超时,当锁超时时间快到期且逻辑未执行完,延长锁超时时间。
redis 的过期时间是依赖系统时钟的,如果时钟漂移过大时 理论上是可能出现的, 会影响到过期时间的计算。
RedLock 算法
与 zookeeper 分布式锁对比
Redission 生产环境的分布式锁
Redisson 是基于 NIO 的 Netty 框架上的一个 Java 驻内存数据网格 (In-Memory Data Grid) 分布式锁开源组件。
但当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景 (重复下单,重复转账),请不要使用 redis 分布式锁。可以使用 CP 模型实现,比如:zookeeper 和 etcd。
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送 ACK 命令:
1、检测主从的连接状态 检测主从服务器的网络连接状态
lag 的值应该在 0 或 1 之间跳动,如果超过 1 则说明主从之间的连接有 故障。
2、辅助实现 min-slaves,Redis 可以通过配置防止主服务器在不安全的情况下执行写命令
min-slaves-to-write 3 (min-replicas-to-write 3 )min-slaves-max-lag 10 (min-replicas-max-lag 10)
上面的配置表示: 从服务器的数量少于 3 个,或者三个从服务器的延迟 (lag) 值都大于或等于 10 秒时,主服务器将拒绝执行写命令。
3、检测命令丢失,增加重传机制
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发 送 REPLCONF ACK 命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量, 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
简单来说就是不用 keys 等,用 range、contains 之类。比如,用户粉丝数,大 V 的粉丝更是高达几千万甚至过亿,因此,获取粉丝列表只能部分获取。另外在判断某用户是否关注了另外一个用户时,也只需要关注列表上进行检查判断,然后返回 True/False 或 0/1 的方式更为高效。
如果单个业务的 KV size 过大,需要分拆成多个 KV 来缓存。拆分时应考虑访问频率。
如果数据量巨大,则在缓存中尽可能只保留频繁访问的热数据,对于冷数据直接访问 DB。
如果小于 10 万 级别,简单分拆到独立 Cache 池即可
如果达到 100 万 级的 QPS,则需要对 Cache 进行分层处理,可以同时使用 Local-Cache 配合远程 cache,甚至远程缓存内部继续分层叠加分池进行处理。(多级缓存)
缓存的命中率对整个服务体系的性能影响甚大。对于核心高并发访问的业务,需要预留足够的容量,确保核心业务缓存维持较高的命中率。比如微博中的 Feed Vector Cache(热点资讯),常年的命中率高达 99.5% 以上。为了持续保持缓存的命中率,缓存体系需要持续监控,及时进行故障处理或故障转移。同时在部分缓存节点异常、命中率下降时,故障转移方案,需要考虑是采用一致性 Hash 分布的访问漂移策略,还是采用数据多层备份策略。
可以设置较短的过期时间,让冷 key 自动过期;也可以让 key 带上时间戳,同时设置较长的过期时间,比如很多业务系统内部有这样一些 key:key_20190801。
平均缓存穿透加载时间在某些业务场景下也很重要,对于一些缓存穿透后,加载时间特别长或者需要复杂计算的数据,而且访问量还比较大的业务数据,要配置更多容量,维持更高的命中率,从而减少穿透到 DB 的概率,来确保整个系统的访问性能。
对于缓存的可运维性考虑,则需要考虑缓存体系的集群管理,如何进行一键扩缩容,如何进行缓存组件的升级和变更,如何快速发现并定位问题,如何持续监控报警,最好有一个完善的运维平台,将各种运维工具进行集成。
对于缓存的安全性考虑,一方面可以限制来源 IP,只允许内网访问,同时加密鉴权访问。
在 Redis 需要升级版本或修复 bug 时,如果直接重启变更,由于需要数据恢复,这个过程需要近 10 分钟的时间,时间过长,会严重影响系统的可用性。面对这种问题,可以对 Redis 扩展热升级功能,从而在毫秒级完成升级操作,完全不影响业务访问。
热升级方案如下,首先构建一个 Redis 壳程序,将 redisServer 的所有属性(包括 redisDb、client 等)保存为全局变量。然后将 Redis 的处理逻辑代码全部封装到动态连接库 so 文件中。Redis 第一次启动,从磁盘加载恢复数据,在后续升级时,通过指令,壳程序重新加载 Redis 新的 redis-4.so 到 redis-5.so 文件,即可完成功能升级,毫秒级完成 Redis 的版本升级。而且整个过程中,所有 Client 连接仍然保留,在升级成功后,原有 Client 可以继续进行读写操作,整个过程对业务完全透明。