# Redis
Redis,英文全称是 Remote Dictionary Server(远程字典服务),是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。
与 MySQL 数据库不同的是, Redis 的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过 10 万次读写操作。因此 redis 被广泛应用于缓存,另外, Redis 也经常用来做分布式锁。除此之外, Redis 支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
# Redis 为何这么快
- Redis 是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 IO 上,所以读取速度快。
- Redis 使用的是非阻塞 IO 、 IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
# 为何使用单线程
因为 Redis 是基于内存的操作,CPU 不会成为 Redis 的瓶颈,而最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
# Redis 在持久化时 fork 出一个子进程,这时已经有两个进程了,怎么能说是单线程呢
Redis 是单线程的,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的。而 Redis 的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说 Redis 是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。
# 如何保证 Redis 的高并发
Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。
# 持久化
redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
RDB ,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。
AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
# RDB 持久化
RDB 方式,是将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
对于 RDB 方式,redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 redis 极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
缺点: RDB 文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的。
# AOF 持久化
AOF,英文是 Append Only File,即只允许追加不允许改写的文件。
因为采用了追加方式,如果不做任何处理的话,AOF 文件会变得越来越大,为此,redis 提供了 AOF 文件重写(rewrite)机制,即当 AOF 文件的大小超过所设定的阈值时,redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。
与 RDB 持久化相对应, AOF 的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
# AOF 重写
在重写即将开始之际,redis 会创建(fork)一个 “重写子进程”,这个子进程会首先读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外。
当 “重写子进程” 完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中。
当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中了。
# 过期键删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器 timer。让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键。如果没有过期,就返回该键。
- 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在上述的三种策略中定时删除和定期删除属于不同时间粒度的主动删除,惰性删除属于被动删除。三种策略都有各自的优缺点:
- 定时删除 对内存使用率有优势,但是对 CPU 不友好。
- 惰性删除 对内存不友好,如果某些键值对一直不被使用,那么会造成一定量的内存浪费。
- 定期删除 是定时删除和惰性删除的折中。
# Redis 实现
Reids 采用的是惰性删除和定期删除的结合,一般来说可以借助最小堆来实现定时器,不过 Redis 的设计考虑到时间事件的有限种类和数量,使用了无序链表存储时间事件,这样如果在此基础上实现定时删除,就意味着 O (N) 遍历获取最近需要删除的数据。
# 定期删除策略
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:
- 对于每个数据库:
- 随机抽取 20 次 key
- 删除这 20(可能存在重复) 个 key 中过期的 key
- 删除期间达到时间限制,结束本次删除。
从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
# 内存淘汰策略
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。这个一般很少用。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key ,这个是最常用的。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key 。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key 。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key 。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
LRU 算法
推荐一篇文章:为什么 LRU 算法原理和代码实现不一样?
# 数据存储
Redis 数据结构
- 字符串 String
- 字典 Hash
- 列表 List
- 集合 Set
- 有序集合 Zset
# Redis 对象
Redis 中的每个对象都由一个 redisObject 结构表示, 该结构中和保存数据有关的三个属性分别是 type 属性、 encoding 属性和 ptr 属性:
typedef struct redisObject { | |
// 类型 | |
unsigned type:4; | |
// 编码 | |
unsigned encoding:4; | |
// 指向底层实现数据结构的指针 | |
void *ptr; | |
// 引用计数 | |
int refcount; | |
// ... | |
} robj; |
# 内存回收
因为 C 语言并不具备自动的内存回收功能, 所以 Redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时, 引用计数的值会被初始化为 1 。
- 当对象被一个新程序使用时, 它的引用计数值会被增一。
- 当对象不再被一个程序使用时, 它的引用计数值会被减一。
- 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。
# 字典的实现
Redis 中的字典由 dict.h/dict 结构表示:
typedef struct dict { | |
// 类型特定函数 | |
dictType *type; | |
// 私有数据 | |
void *privdata; | |
// 哈希表 | |
dictht ht[2]; | |
//rehash 索引 | |
// 当 rehash 不在进行时,值为 -1 | |
int rehashidx; /* rehashing not in progress if rehashidx == -1 */ | |
} dict; |
type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:
- type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
- 而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType { | |
// 计算哈希值的函数 | |
unsigned int (*hashFunction)(const void *key); | |
// 复制键的函数 | |
void *(*keyDup)(void *privdata, const void *key); | |
// 复制值的函数 | |
void *(*valDup)(void *privdata, const void *obj); | |
// 对比键的函数 | |
int (*keyCompare)(void *privdata, const void *key1, const void *key2); | |
// 销毁键的函数 | |
void (*keyDestructor)(void *privdata, void *key); | |
// 销毁值的函数 | |
void (*valDestructor)(void *privdata, void *obj); | |
} dictType; |
ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht [0] 哈希表, ht [1] 哈希表只会在对 ht [0] 哈希表进行 rehash 时使用。
除了 ht [1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。
# 哈希冲突
Redis 通过链式哈希解决冲突,也就是同一个桶里面的元素使用单向链表保存。但是当链表过长就会导致查找性能变差可能。所以 Redis 为了追求块,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。
因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 所以为了速度考虑, 程序总是将新节点添加到链表的表头位置(复杂度为 O (1)), 排在其他已有节点的前面。
# 扩容
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 。
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 。
其中哈希表的负载因子可以通过公式:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小 | |
load_factor = ht[0].used / ht[0].size |
# rehash
- 定时任务:Redis 定时任务 serverCron 会在每个周期内执行 1ms 渐进式 Rehash 操作。
- 附着于其他操作:在 Redis 执行 dictAddRaw, dictGenericDelete, dictFind, dictGetSomeKeys 和 dictGetRandomKey 等操作前会执行 Rehash 操作。
# 渐进式 rehash
扩展或收缩哈希表需要将 ht [0] 里面的所有键值对 rehash 到 ht [1] 里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
哈希表渐进式 rehash 的详细步骤:
- 为 ht [1] 分配空间, 让字典同时持有 ht [0] 和 ht [1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht [0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht [1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht [0] 的所有键值对都会被 rehash 至 ht [1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
渐进式 rehash 执行期间的哈希表操作
因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht [0] 和 ht [1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht [0] 里面进行查找, 如果没找到的话, 就会继续到 ht [1] 里面进行查找, 诸如此类。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht [1] 里面, 而 ht [0] 则不再进行任何添加操作: 这一措施保证了 ht [0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
# 事务
Redis 事务没有回滚机制
Redis 通过 MULTI,EXEC,WATCH 等命令来实现事务功能。
事务首先以一个 MULTI 命令 开始,然后将多个命令放入事务中,最后由 EXEC 命令将这个事务提交给服务器执行。
# 事务的三个阶段
- 事务开始
- 命令入队
- 事务执行
当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作:
如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执行这个命令。
如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令 以外的 其他命令,那么服务器 并不立即执行 这个命令,而是将 这个命令 放入 一个事务队列里面,然后 向客户端返回 QUEUED 回复。
MULTI、EXEC、DISCARD、WATCH 这四个指令构成了 redis 事务处理的基础。
- MULTI 用来组装一个事务。
- EXEC 用来执行一个事务。
- DISCARD 用来取消一个事务。
- WATCH 用来监视一些 key,一旦这些 key 在事务执行之前被改变,则取消事务的执行。
# 一致性
入队错误
- 如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情 况,那么 Redis 将拒绝执行这个事务。
执行错误
除了入队时可能发生错误以外,事务还可能在执行的过程中发生错误。 关于这种错误有两个需要说明的地方:
- 执行过程中发生的错误都是一些不能在入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发。
- 即使在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执 行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会 被出错的命令影响。
# Redis 如何保证原子性
- Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。
- 业务中常常有先 get 后 set 的业务常见,在并发下会导致数据不一致的情况。
- 使用 incr、decr、setnx 等原子操作
- 使用事务
- 使用 Lua 脚本实现 CAS 操作
# watch 命令
很多时候,要确保事务中的数据没有被其他客户端修改才执行该事务。Redis 提供了 watch 命令来解决这类问题,这是一种乐观锁的机制。客户端通过 watch 命令,要求服务器对一个或多个 key 进行监视,如果在客户端执行事务之前,这些 key 发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。
# 缓存
# 如何设计 Redis 的过期时间
- 热点数据不设置过期时间,使其达到「物理」上的永不过期,可以避免缓存击穿问题。
- 在设置过期时间时,可以附加一个随机数,避免大量的 key 同时过期,导致缓存雪崩。
# 缓存错误
# 缓存穿透
问题描述
客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案
- 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值。
- 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。
# 缓存击穿
问题描述
一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
解决方案
- 永不过期:热点数据不设置过期时间,所以不会出现上述问题,这是 “物理” 上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。
- 加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
# 缓存雪崩
问题描述
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是 Redis 节点发生故障,导致大量请求无法得到处理。
解决方案
- 避免数据同时过期:设置过期时间时,附加一个随机数,避免大量的 key 同时过期。
- 启用降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息 / 空值 / 错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给 Redis ,而是直接返回。
- 构建高可用的 Redis 服务:采用哨兵或集群模式,部署多个 Redis 实例,个别节点宕机,依然可以保持服务的整体可用。
# 分布式锁
# 基本可用的锁实现
加锁代码
def acquire_lock(conn, lockname, acquire_timeout=10, lock_timeout=10): | |
identifier = str(uuid.uuid4()) | |
lockname = "lock:" + lockname | |
end = time.time() + acquire_timeout | |
while time.time() < end: | |
if conn.set(lockname, identifier, ex=lock_timeout, nx=True): | |
return identifier | |
time.sleep(0.001) | |
return False |
实现锁的关键指令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
生存时间(TTL,以秒为单位)
Redis 2.6.12 版本开始:(等同 SETNX 、 SETEX 和 PSETEX)
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
由于 NX 命令的特殊性,可以实现 CAS 的原子操作。
为什么要设置锁的过期时间
一个业务进程 A 获取锁之后,如果在释放锁之前宕机,那么在服务端会认为进程 A 一直在持有者这把锁,其它进程将永远获取不到这把锁,导致死锁。设置过期时间后,即使加锁的进程宕机,Redis 服务器也能在过期时间到达时自动释放锁。
如果锁的过期时间已经到达,业务进程还没有完成业务操作?
锁的过期时间一般要设置为业务操作时长的两倍,当如果还是有个别业务时需时间较长就需要我们主动续期。在加锁之后将锁放入进程对应的一个锁队列,启动一个线程监测锁的过期时间,在锁即将过期时为锁自动延续过期时间。这种线程一般被叫做「看门狗」线程。
主从结构锁丢失
事实上这类琐最大的缺点就是它加锁时只作用在一个 Redis 节点上,即使 Redis 通过 sentinel 保证高可用,如果这个 master 节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
- 在 Redis 的 master 节点上拿到了锁。
- 但是这个加锁的 key 还没有同步到 slave 节点。
- master 故障,发生故障转移,slave 节点升级为 master 节点。
- 导致锁丢失。
正因为如此,Redis 作者 antirez 基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。
释放锁代码
def release_lock(conn, lockname, identifier): | |
with conn.pipeline() as pipe: | |
lockname = 'lock:' + lockname | |
while True: | |
try: | |
pipe.watch(lockname) | |
if pipe.get(lockname) == identifier: | |
pipe.multi() | |
pipe.delete(lockname) | |
pipe.execute() | |
return True | |
pipe.unwatch() | |
break | |
except WatchError: | |
pass | |
return False |
# Redlock 实现
antirez 提出的 redlock 算法大概是这样的:
在 Redis 的分布式环境中,我们假设有 N 个 Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在 N 个实例上使用与在 Redis 单实例下相同方法获取和释放锁。现在我们假设有 5 个 Redis master 节点,同时我们需要在 5 台服务器上面运行这些 Redis 实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
- 获取当前 Unix 时间,以毫秒为单位。
- 依次尝试从 5 个实例,使用相同的 key 和具有唯一性的 value(例如 UUID)获取锁。当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以避免服务器端 Redis 已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁。
- 客户端使用当前时间减去开始获取锁时间(步骤 1 记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 - 如果因为某些原因,获取锁失败(没有在至少 N/2+1 个 Redis 实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
# 高可用
所谓的高可用,也叫 HA(High Availability),是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。
# 主从模式
一般,系统的高可用都是通过部署多台机器实现的。redis 为了避免单点故障,也需要部署多台机器。
因为部署了多台机器,所以就会涉及到不同机器的的数据同步问题。
为此,redis 提供了 Redis 提供了复制 (replication) 功能,当一台 redis 数据库中的数据发生了变化,这个变化会被自动的同步到其他的 redis 机器上去。
redis 多机器部署时,这些机器节点会被分成两类,一类是主节点(master 节点),一类是从节点(slave 节点)。一般主节点可以进行读、写操作,而从节点只能进行读操作。同时由于主节点可以写,数据会发生变化,当主节点的数据发生变化时,会将变化的数据同步给从节点,这样从节点的数据就可以和主节点的数据保持一致了。一个主节点可以有多个从节点,但是一个从节点会只会有一个主节点,也就是所谓的一主多从结构。
# 复制机制
- 从数据库连接主数据库,发送 SYNC 命令。
- 主数据库接收到 SYNC 命令后,可以执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。
- 主数据库 BGSAVE 执行完后,向所有从数据库发送快照文件,并在发送期间继续记录被执行的写命令。
- 从数据库收到快照文件后丢弃所有旧数据,载入收到的快照。
- 主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令。
- 从数据库完成对快照的载入,开始接受命令请求,并执行来自主数据库缓冲区的写命令。(从数据库初始化完成)
- 主数据库每执行一个写命令就会向从数据库发送相同的写命令,从数据库接收并执行收到的写命令(从数据库初始化完成后的操作)
- 出现断开重连后,2.8 之后的版本会将断线期间的命令传给从数据库,增量复制。
- ** 主从刚刚连接的时候,进行全量同步。全同步结束后,进行增量同步。** 当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
# 哨兵模式
主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这种方式并不推荐,实际生产中,我们优先考虑哨兵模式。这种模式下,master 宕机,哨兵会自动选举 master 并将其他的 slave 指向新的 master。
在主从模式下,redis 同时提供了哨兵命令 redis-sentinel,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵进程向所有的 redis 机器发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。
哨兵可以有多个,一般为了便于决策选举,使用奇数个哨兵。哨兵可以和 redis 机器部署在一起,也可以部署在其他的机器上。多个哨兵构成一个哨兵集群,哨兵直接也会相互通信,检查哨兵是否正常运行,同时发现 master 宕机哨兵之间会进行决策选举新的 master。
哨兵集群中哨兵实例之间可以相互发现,基于 Redis 提供的发布 / 订阅机制(pub/sub 机制),
哨兵可以在主库中发布 / 订阅消息,在主库上有一个名为 \__sentinel__:hello
的频道,不同哨兵就是通过它来相互发现,实现互相通信的,而且只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。
# 哨兵模式的作用
- 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到 master 宕机,会自动将 slave 切换到 master,然后通过发布订阅模式通过其他的从服务器,修改配置文件,让它们切换主机。
- 然而一个哨兵进程对 Redis 服务器进行监控,也可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
# 哨兵模式的工作机制
- 每个 Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他 Sentinel(哨兵)进程发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
- 如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态
- 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)
- 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、Slave 从服务器发送 INFO 命令。
- 当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。
# 集群模式
Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容。
redis 集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis 集群不需 要 sentinel 哨兵∙也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点 (官方推荐不超过 1000 个节点)。
根据官方推荐,集群部署至少要 3 台以上的 master 节点,最好使用 3 主 3 从六个节点的模式 。
# 运行机制
- 在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383,可以从上面 redis-trib.rb 执行的结果看到这 16383 个 slot 在三个 master 上的分布。还有一个就是 cluster,可以理解为是一个集群管理的插件,类似的哨兵。
- 当我们的存取的 Key 到达的时候,Redis 会根据 crc16 的算法对计算后得出一个结果,然后把结果和 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
- 当数据写入到对应的 master 节点后,这个数据会同步给这个 master 对应的所有 slave 节点。
- 为了保证高可用,redis-cluster 集群引入了主从模式,一个主节点对应一个或者多个从节点。当其它主节点 ping 主节点 master 1 时,如果半数以上的主节点与 master 1 通信超时,那么认为 master 1 宕机了,就会启用 master 1 的从节点 slave 1,将 slave 1 变成主节点继续提供服务。
- 如果 master 1 和它的从节点 slave 1 都宕机了,整个集群就会进入 fail 状态,因为集群的 slot 映射不完整。如果集群超过半数以上的 master 挂掉,无论是否有 slave,集群都会进入 fail 状态。
- redis-cluster 采用去中心化的思想,没有中心节点的说法,客户端与 Redis 节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。