Redis
前言
Redis是一种支持key-value等多种数据结构的存储系统,通过在内存中读取数据,大大提高了数据的读取速度,作为一个缓存中间件,是实现网站高并发以及高可用不可或缺的一部分。可应用于缓存,事件发布或者订阅,高速队列等场景,支持网络,提供字符串、哈希表、列表、队列、集合结构直接存储,基于内存并可持久化。
一、概念和基础
概念
Redis是一款基于内存的高速缓存数据库,全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,支持丰富的数据类型。
Redis与其他key-value缓存产品有以下几个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘,重启的时候再次加载进行使用
- Redis不仅仅支持简单的key-value类型数据,同时还提供list、set、zset以及hash等数据结构的存储
- Redis支持数据的备份,即master-slave模式的数据备份。
优势
- 性能极高 — Redis能读的速度是110000次/s,写的速度是81000次/s
- 原子性 — Redis所有操作都是原子性的,同时Redis还支持对几个操作全并之后的原子性执行
- 丰富的特性 — Redis支持publish/subscribe,通知,key过期等等特性。
- 持久化 — Redis支持RDB,AOF等持久化方式
- 发布订阅 — Redis支持发布/订阅模式
- 分布式 — Redis Cluster 集群
官方资料
Redis官网:http://redis.io/
Redis官方文档:http://redis.io/documentation
Redis下载:http://redis.io/download
参考资料: http://redisdoc.com/index.html https://www.cnblogs.com/kismetv/p/8654978.html#t41 https://www.pdai.tech/md/outline/x-outline.html#nosql-db—redis%E8%AF%A6%E8%A7%A3
使用场景
热点数据的缓存
缓存是Redis中最常见的应用场景,Redis读写性能优异,在高并发服务中成为首选的缓存组件,并且Redis支持事务,能保证数据的一致性。
限时活动
Redis中可以使用expire命令射在一个键的生存时间,过期之后Redis会自动删除,利用这一特性可以应用在限时抢购活动、获取手机验证码等常见业务场景。
计数器相关问题
Redis中有incr命令可以实现原子性的递增,可以运用于高并发的秒杀活动、分布式序列号的生成以及其他限制次数的业务中。
分布式锁
Redis中有setnx命令,该命令全写为:“set if not exists”,如果不存在则设置成功并返回1,否则返回0。在分布式集群系统中,一个定时任务可能会在多个机器上运行,为了保持数据的一致性,可以先在定时任务中运用setnx命令设置一个锁,如果成功设置则执行任务,否则说明任务已经开始执行,从而避免多个任务同时执行对数据产生影响。该分布式锁常运用于大型秒杀系统。
延时操作
在电商系统中,当用户下单,订单产生之后会占用库存,可以设置一个时效检验用户是否已经付款购买,如果超时则让该单据失效,同时还原库存。在Redis2.8.0版本之后还提供了Keyspace Notifications功能,运行客户端订阅Pub/Sub频道,接受Redis数据集的变化事件。通过上述我们就可以解决实际应用的问题,当订单产生时,设置一个key,同时设置15分钟后过期,在后台实现一个监听器,监听key的时效,key失效后仍没有完成订单则取消订单。
点赞、好友等相互关系的存储
Redis利用集合的一些命令,如求交集、并集、差集等。
在微信朋友圈中,每个用户的好友存入一个集合中,很容易实现求出两个人的共同好友,并将共同好友的信息呈现在朋友圈中。
二、数据类型:5种基础的数据类型
Redis数据结构简介
Redis所有的key(键)都是字符串,基础数据结构包括String、List、Set、Zset、Hash。每种结构都至少有两种编码,这样的好处在于:一方面接口与实现实现了分离,需要增加或改变内部编码时,用户不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。
结构类型 | 结构存储的值 | 结构的读写能力 |
---|
String字符串 | 二进制安全的,可以包含任何数据,比如jpg格式的图片或者可以序列化的对象,一个键最大能存储512MB | 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作; | List列表 | 本质是链表,链表上的每个节点都包含一个字符串 | 链表的两端都可以进行push 和pop 操作,读取单个或多个数据 | Set集合 | 包含字符串的无序集合并且存储的值唯一,集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 | 字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等 | Zset有序集合 | 包含键值对的无序散列表,是一个 string 类型的field 和 value 的映射表,hash 特别适合用于存储对象。 | 添加、获取、删除单个元素 | Hash散列表 | string 类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。 | 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素 |
基础数据结构详解
String字符串
String是redis中最基本的数据类型,一个key对应一个value。
-
命令使用
命令 | 简述 | 使用 |
---|
SET | 将字符串值 value 关联到 key ,如果 key 已经持有其他值, SET 就覆写旧值, 无视类型 | SET key value | SETNX | “Set If Not Exists”的缩写,键key 不存在的情况,将key 设为 valule ,并返回1,若键 key 已经存在, 则 SETNX 命令不做任何动作,并返回0 | SETNX key value | SETEX | 将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值 | SETEX key seconds value | PSETEX | 这个命令和 SETEX 命令相似, 但它以毫秒为单位设置 key 的生存时间, 而不是像 SETEX 命令那样以秒为单位进行设置 | PSETEX key milliseconds value | GET | 返回与键 key 相关联的字符串值 | GET key | GETSET | 将键 key 的值设为 value , 并返回键 key 在被设置之前的旧值 | GETSET key value | APPEND | 如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。如果key 不存在,则该命令与SET,命令效果相同 | APPEND key value | INCR | 为键 key 储存的数字值加上一。如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误 | INCR key | INCRBY | 为键 key 储存的数字值加上增量 increment | INCRBY key increment | INCRBYFLOAT | 为键 key 储存的值加上浮点数增量 increment | INCRBYFLOAT key increment | DECR | 为键 key 储存的数字值减去一。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECR 操作。如果键 key 储存的值不能被解释为数字, 那么 DECR 命令将返回一个错误 | DECR key | DECRBY | 将键 key 储存的整数值减去减量 decrement | DECRBY key decrement | MSET | 同时为多个键设置值。 | MSET key value [key value …] | MGET | 返回给定的一个或多个字符串键的值。 | MGET key [key …] |
-
实战场景
- 缓存:把常用消息,字符串,照片等信息存入Redis中,把Redis当作缓存层,Mysql作为持久层,以减轻Musql的读写压力
- 计数器:Redis是单线程模型,一个命令执行完才会执行下一个。
- Session:Spring Session + Redis实现Session共享。
List列表
实质为链表,用双端链表实现
-
命令使用
命令 | 简述 | 使用 |
---|
LPUSH/RPUSH | 将一个或多个值 value 插入到列表 key 的表头(表尾) | LPUSH /RPUSH key value [value …] | LPUSHX | 将值 value 插入到列表 key 的表头,当且仅当 key 存在并且是一个列表。和 LPUSH 命令相反,当 key 不存在时 LPUSHX 命令什么也不做。 | LPUSHX key value | LPOP/RPOP | 移除并返回列表 key 的头(尾)元素。 | LPOP /RPOP key | RPOPLPUSH | 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 | RPOPLPUSH source destination | LREM | 根据参数 count 的值,移除列表中与参数 value 相等的元素;count > 0: 从表头开始向表尾搜索,移除与 value相等的元素,数量为 count; count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。count = 0 : 移除表中所有与 value 相等的值。 | LREM key count value | LLEN | 返回列表 key 的长度。 | LLEN key | LINDEX | 返回列表 key 中,下标为 index 的元素。 | LINDEX key index | LSET | 将列表 key 下标为 index 的元素的值设置为 value 。 | LSET key index value | LRANGE | 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 | LRANGE key start stop | LTRIM | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 | LTRIM key start stop | BLPOP | 它是 LPOP key 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 | BLPOP key [key …] timeout |
-
列表的使用技巧
lpush +lpop =Stack (栈)lpush +rpop =Queue (队列)lpush +ltrim =Capped Collection (有限列表)push +brpop =Message Queue (消息队列) -
实战场景
- 朋友圈:
lpush 新的动态,lpop 展示新的动态 - 消息队列:利用
RPOPLPUSH 实现一个安全的消息队列,不仅返回一个消息同时将这个消息备份到一个备份列表中,如果使用LPUSH 将消息放入队列,而另一个客户端中 BRPOP 取出队列,该队列方式是不安全的,如果一个客户端取出消息后崩溃,而未处理完的消息也将因此丢失 - 事件提醒:有时候为了等待一个新的元素到达数据中,需要使用轮询的方式对数据进行探查,另一种更好的方式是,使用系统提供的阻塞原语,在新元素到达时立即进行处理,而新元素没达到时,一直阻塞,避免轮询占用资源
Set集合
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
-
命令使用
命令 | 简述 | 使用 |
---|
SADD | 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略 | SADD key member [member …] | SISMEMBER | 判断 member 元素是否集合 key 的成员;如果 member 元素是集合的成员,返回 1 。 如果 member 元素不是集合的成员,或 key 不存在,返回 0 | SISMEMBER key member | SPOP | 移除并返回集合中的一个随机元素。 | SPOP key | SRANDMEMBER | 只提供 key 参数时,返回一个元素; 如果提供了 count 参数,那么返回一个数组;如果集合为空,返回空数组 | SRANDMEMBER key [count] | SREM | 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 | SREM key member [member …] | SMOVE | 将 member 元素从 source 集合移动到 destination 集合。 | SMOVE source destination member | SCARD | 返回集合 key 的基数(集合中元素的数量)。 | SCARD key | SMEMBERS | 返回集合 key 中的所有成员。 | SMEMBERS key | SINTER | 返回一个集合的全部成员,该集合是所有给定集合的交集。不存在的 key 被视为空集。当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。 | SINTER key [key …] | SINTERSTORE | 这个命令类似于 SINTER 命令,但它将结果保存到 destination 集合,而不是简单地返回结果集。 | SINTERSTORE destination key [key …] | SUNION | 返回一个集合的全部成员,该集合是所有给定集合的并集。 | SUNION key [key …] | SDIFF | 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 | SDIFF key [key …] |
-
实战场景
- 标签(tag):给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人
- 点赞,或点踩,收藏等,可以放到set中实现
Hash散列
一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
-
命令使用
命令 | 简述 | 使用 |
---|
HSET | 将哈希表 hash 中域 field 的值设置为 value 。如果给定的哈希表并不存在, 那么一个新的哈希表将被创建并执行 HSET 操作。如果域 field 已经存在于哈希表中, 那么它的旧值将被新值 value 覆盖。 | HSET hash field value | HSETNX | 当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value ;如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作;如果哈希表 hash 不存在, 那么一个新的哈希表将被创建并执行 HSETNX 命令 | HSETNX hash field value | HGET | HGET 命令在默认情况下返回给定域的值。 | HGET hash field | HEXISTS | 检查给定域 field 是否存在于哈希表 hash 当中。 | HEXISTS hash field | HDEL | 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 | HDEL key field [field …] | HLEN | 返回哈希表 key 中域的数量。 | HLEN key | HINCRBY | 为哈希表 key 中的域 field 的值加上增量 increment 。 | HINCRBY key field increment | HMSET | 同时将多个 field-value (域-值)对设置到哈希表 key 中。 | HMSET key field value [field value …] | HMGET | 返回哈希表 key 中,一个或多个给定域的值。 | HMGET key field [field …] | HVALS | 返回哈希表 key 中所有域的值。 | HVALS key | HGETALL | 返回哈希表 key 中,所有的域和值。 | HGETALL key |
-
实战场景
- 缓存:可以更方便更直观地维护缓存信息,如果用户信息等,并且比String更节省空间,方便管理。
Zset有序集合
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
-
命令使用
命令 | 简述 | 使用 |
---|
ZADD | 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 | ZADD key score member [[score member] [score member] …] | ZSCORE | 返回有序集 key 中,成员 member 的 score 值。 | ZSCORE key member | ZINCRBY | 为有序集 key 的成员 member 的 score 值加上增量 increment 。 | ZINCRBY key increment member | ZREVRANGE | 返回有序集 key 中,指定区间内的成员。 | ZREVRANGE key start stop [WITHSCORES] | ZREM | 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 | ZREM key member [member …] | ZRANK | 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 | ZRANK key member | ZREVRANK | 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。 | ZREVRANK key member | ZREMRANGEBYRANK | 移除有序集 key 中,指定排名(rank)区间内的所有成员。 | ZREMRANGEBYRANK key start stop | ZREMRANGEBYSCORE | 移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 | ZREMRANGEBYSCORE key min max |
-
实战场景
- 排行榜:可以应用于一些需要排序的排行榜中,例如常见的微博热搜等。
三、特殊的数据类型
三种特殊类型讲解
除了上文中的五种基础数据类型,还有三种特殊的数据类型分别是 **HyperLogLog(基数统计), Bitmaps (位图) 和 geospatial (地理位置)。
HyperLogLog
Redis 2.8.9 版本更新:新增Hyperloglog 数据结构
数据存入后无法取出,只能用于基数的统计
-
什么是基数? 例如,A={1, 3, 5, 7, 5, 7, 8,}那么基数(不重复的元素)为5,基数集为{1,3,5,7,8};基数估计就是在误差可接受的范围内,快速计算基数。 -
HyperLogLog 基数统计用来解决什么问题? 作为一个高级不精确去重的数据结构,它常常用于统计数据时去重的操作。特点是可以利用极小的内存空间完成独立总数的统计,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。 -
优势体现在哪? 一个大型网站中需要统计IP数,粗略计算一个IP消耗10字节,100万的IP就是15MB,而HyperLogLog在Redis中每个键占用的内容都是12字节,理论存储近似接近264个值,它基于一个基数估算的算法,只能比较准确的估算出基数,仍然会存在一定不可避免的误差,但一个带有0.81%误差的近似值在实际应用场景也是可以接受的。 -
命令使用
命令 | 简述 | 使用 | 返回值 |
---|
PFADD | 将任意数量的元素添加到指定的 HyperLogLog 里面 | PFADD key element [element …] | 整数回复: 如果 HyperLogLog 的内部储存被修改了, 那么返回 1 , 否则返回 0 | PFCOUNT | 命令作用于单个键时, 返回储存在给定键的 HyperLogLog 的近似基数, 如果键不存在, 那么返回 0 | PFCOUNT key [key …] | 返回的可见集合(observed set)基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值。 | PFMERGE | 将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集。 | PFMERGE destkey sourcekey [sourcekey …] | 字符串回复:返回 OK |
Bitmap
itmap 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。
Geospatial
Redis 3.2 .0版本更新:新增Geospatial 数据结构,可以用于推算地理位置的信息
常用命令
-
GEOADD key longitude latitude member [longitude latitude member …] GEOADD 命令以标准的 x,y 格式接受参数, 所以用户必须先输入经度, 然后再输入纬度。 GEOADD 能够记录的坐标是有限的: 非常接近两极的区域是无法被索引的。 精确的坐标限制由 EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义, 具体如下:
- 有效的经度介于 -180 度至 180 度之间。
- 有效的纬度介于 -85.05112878 度至 85.05112878 度之间。
127.0.0.1:6379> geoadd china:city 144.05 22.52 shenzhen 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 3
-
GEOPOS key member [member …] 从键里面返回所有给定位置元素的位置(经度和纬度)。 因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。 GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。 当给定的位置元素不存在时, 对应的数组项为空值。 127.0.0.1:6379> GEOPOS china:city shenzhen hangzhou
1) 1) "144.05000120401382446"
2) "22.5200000879503861"
2) 1) "120.1600000262260437"
2) "30.2400003229490224"
-
GEODIST key member1 member2 [unit] 返回两个给定位置之间的距离。 指定单位的参数 unit 必须是以下单位的其中一个:
m 表示单位为米。km 表示单位为千米。mi 表示单位为英里。ft 表示单位为英尺。 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。 127.0.0.1:6379> GEODIST china:city shenzhen hangzhou
"2524417.0471"
-
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。 在给定以下可选项时, 命令会返回额外的信息:
WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。WITHCOORD : 将位置元素的经度和维度也一并返回。WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。 命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。 在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。 127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km
1) "xian"
2) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist
1) 1) "xian"
2) "483.8340"
2) 1) "hangzhou"
2) "977.5143"
127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang
(integer) 3
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist count 2
1) 1) "xian"
2) "483.8340"
2) 1) "manjing"
2) "864.9816"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist withcoord count 2
1) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "manjing"
2) "864.9816"
3) 1) "118.75999957323074341"
2) "32.03999960287850968"
-
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] 这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。 127.0.0.1:6379> GEORADIUSBYMEMBER china:city taiyuan 1000 km withcoord withdist
1) 1) "manjing"
2) "859.5256"
3) 1) "118.75999957323074341"
2) "32.03999960287850968"
2) 1) "taiyuan"
2) "0.0000"
3) 1) "112.54999905824661255"
2) "37.86000073876942196"
3) 1) "xian"
2) "514.2264"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
四、数据库
常用指令
命令 | 简述 | 使用 | 返回值 |
---|
EXISTS | 检查给定 key 是否存在 | EXISTS key | 若 key 存在,返回 1 ,否则返回 0 | TYPE | 返回 key 所储存的值的类型 | TYPE key | none (key不存在) 、string (字符串) 、list (列表) 、set (集合) 、zset (有序集) 、hash (哈希表) 、stream (流) | RENAME | 将 key 改名为 newkey ,当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 | RENAME key newkey | 改名成功时提示 OK ,失败时候返回一个错误。 | RENAMENX | 当且仅当 newkey 不存在时,将 key 改名为 newkey ;当 key 不存在时,返回一个错误 | RENAMENX key newkey | 修改成功时,返回 1 ; 如果 newkey 已经存在,返回 0 。 | MOVE | 将当前数据库的 key 移动到给定的数据库 db 当中,如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果 | MOVE key db | 移动成功返回 1 ,失败则返回 0 。 | DEL | 删除给定的一个或多个 key ,不存在的 key 会被忽略。 | DEL key [key …] | 被删除 key 的数量 | DBSIZE | 返回当前数据库的 key 的数量。 | DBSIZE | 当前数据库的 key 的数量。 | KEYS | 查找所有符合给定模式 pattern 的 key , 比如说: KEYS * 匹配数据库中所有 key ;KEYS h?llo 匹配 hello , hallo 和 hxllo 等; KEYS h*llo 匹配 hllo 和 heeeeello 等; KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo | KEYS pattern | 符合给定模式的 key 列表。 | FLUSHDB | 清空当前数据库中的所有 key,此命令从不失败 | FLUSHDB | 总是返回 OK 。 | FLUSHALL | 清空整个 Redis 服务器的数据(删除所有数据库的所有 key ),此命令从不失败 | FLUSHALL | 总是返回 OK 。 | SELECT | 切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值,默认使用 0 号数据库 | SELECT index | OK | SWAPDB db1 db2 | 对换指定的两个数据库, 使得两个数据库的数据立即互换 | SWAPDB db1 db2 | OK |
设置键的生存时间或过期时间
通过EXPIRE 或者PEXPIRE 命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,后面简称TTL),在经过了指定的时间后服务器就会删除生存时间为0的键。
127.0.0.1:6379> SET key value
OK
127.0.0.1:6379> EXPIRE key 5 //设置过期时间为5s
(integer) 1
127.0.0.1:6379> get key //5s之内
"value"
127.0.0.1:6379> get key //5s后
(nil)
在之前提到的SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,这个命令是一个限制了类型的命令,只适用于字符串类型的数据,但SETEX 命令设置过期时间的原理与EXPIRE 命令设置过期时间的原理是完全一致的。
TTL 和PTTL 命令可以接收一个待用生存时间或者过期时间的键,返回这个键的剩余生存时间。
127.0.0.1:6379> EXPIRE key 1000
(integer) 1
127.0.0.1:6379> TTL key
(integer) 996
EXPIREAT key timestamp 命令可以设置键key的过期时间为timestamp指定的秒数时间戳
PEXPIREAT key timestamp 命令可以设置键key的过期时间为timestamp指定的毫秒数时间戳
虽然有多种不同的单位以及不同形式的设置命令,但实际上EXPIRE PEXPIRE EXPIREAT 都是基于PEXPIREAT 来实现的,所以客户端中无论执行的是上面四个指令中的哪一个,经过转换后,执行的效果与PEXPIREAT 指令效果相同。
保存设置时间
RedisDB结构的expires 字典保存了数据库中所有键的过期时间,称之为过期字典
- 过期字典的键是一个指针,这个指针指向键空间的某个键对象,即某个数据库的键
- 过期字典的值是一个
long long 类型的整数,这个整数保存了键指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳
移除过期时间
PERSIST 命令可以移除一个键的过期时间
127.0.0.1:6379> EXPIRE key 1989
(integer) 1
127.0.0.1:6379> TTL key
(integer) 1987
127.0.0.1:6379> PERSIST key
(integer) 1
127.0.0.1:6379> ttl key
(integer) -1
PERSIST 命令就是PEXPIREAT 命令的反操作,PERSIST 命令在过期字典中查找给定的键,并接触键和值(过期时间)在过期字典中的关联。
过期数据的删除策略
如果一个键过期了,那么它什么时候会被删除呢?
这个问题有三种可能的答案,分别代表了三种不同的过期删除策略:
- 定时删除:在设置键的过期时间时,创建一个定时器(timer),让定时器在键过期时间来临时,立刻执行对键的删除
- 惰性删除:放任键过期不管,但是每次从键空间中获取键,都会检查取得的键是否过期,如果过期,就删除该键,否则就返回该键
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,检查多少个数据库,如何检查(因为数据量庞大时,不可能每个数据都进行检查,要设计算法随机检查),则由算法决定。
以上三种删除策略中,第一、三种属于主动删除,第二种则属于被动删除。
定时删除
定时删除策略对内存是最友好的,属于用时间换空间的一种策略,通过使用定时器,定时删除键会保证过期键尽快被删除,并释放过期键占用的内存。
另一方面,定时删除策略的缺点是,它对CPU时间是最不友好的,在过期键数量庞大时,删除过期键会占用一部分CPU处理时间,内存不紧张但是CPU压力大时,采用这种策略无疑会对服务器响应时间和吞吐量产生相当明显的负面影响。
如果有大量的请求命令在等待服务器处理,而此时服务器当前并不缺少内存, 那么服务器应该优先将CPU占用分配给请求处理,而不是用在删除过期键上。
除此之外,创建一个定时器需要用到Redis服务器中的时间事件,当前时间事件的实现方式——无序链表,查找一个事件的时间复杂度为O(N),并不能高效地处理时间事件。
因此要让服务器创建大量的定时器来执行定时删除策略,在现阶段是不现实的。
惰性删除
惰性删除策略对CPU时间来说是最友好的:程序只会在取出键的时候才会对键进行过期检查,这可以保证删除键的操作只会在非做不可的情况下才进行,并且删除的目标仅限于当前键,并不是大量地删除,这个策略不会让CPU在删除无关的键上花费过多时间。
但是这种策略的确定是,它对内存是最不友好的,如果数据库中有相当庞大数量的过期键并且占着大量的内存,而这些键如果不被访问到的话,它可能永远不会被删除,它所占据的内存就永远不会被释放,服务器的内存空间会被这些垃圾数据一直占据吞噬,服务器内存也会越来越紧张。
举个例子,对于一些和事件有关的数据,比如日志,在某个时间点之后,它的访问量就会大大减少,甚至不再访问,如果这类过期数据一直堆积在数据库中,那么造成的影响肯定是非常严重的。
定期删除
从上述对两种删除策略的讨论后得知,这两种策略都是相当极端的,在同样极端的情况下会造成严重的后果。
而定期删除策略是这两种策略的一种整合和折中:
- 定期删除策略每隔一段时间执行一次过期键删除,并通过限制删除操作执行的时间和频率来减少操作对CPU的影响
- 除此之外,定期删除过期键,有效地减少了过期键带来的内存浪费,避免了内存泄漏的危险
定期删除策略的难点是确定删除操作执行时长和频率:
- 如果删除得太频繁或执行时间过长,定期删除策略会退化成定时删除策略
- 如果删除操作执行的太少或者执行时间过少,又会跟惰性删除策略一样出现内存浪费的情况
因此,如果采用定期删除策略,服务器必须根据情况,合理设置删除操作的时间和执行频率。
五、持久化
Redis作为一个缓存组件为什么需要持久化?
Redis是一个基于内存的数据库,如果服务出现宕机的情况,数据将全部丢失,通常的解决方案是通过后端数据库恢复,但是后端数据库如常见的Mysql数据库有性能瓶颈,如果是大量丢失数据的恢复,会对数据库造成相当大的压力,开销大效率低下,所以对于Redis实现数据持久化是相当重要的,在出现数据丢失灾难中可以避免从后端数据库恢复数据。
Redis提供了多种持久化方式
- RDB持久化可以在指定的时间间隔里生成数据集的快照(point-in-time-snapshot)并保存到磁盘上,由于是某一时刻的快照,所以快照中的数据要早于或等于内存中的数据
- AOF持久化记录服务器执行的所有写操作命令并以aof格式保存在磁盘上,并在服务器启动时,通过执行这些命令来还原数据集。AOF文件中的命令全部以Redis协议的格式来保存,新命令会追加到文件的末尾。Redis还可以使用
BGREWRITEAOF 命令来重写文件,去除一些不影响最终数据结果的命令,这样保证AOF文件保存的数据集占用内存不会过大 - Redis还可以同时使用AOF和RDB来实现持久化,这是Redis4.0版本推出的,官方也支持在实际开发中使用这种用法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。在这种情况下,快照不用很频繁地执行,避免了频繁的fork对主线程的影响,避免主线程阻塞,也不需要记录所有操作了,因此不会出现文件过大的问题,也可以避免重写开销过大。
- 虚拟内存(VM)方式存储,从Redis Version2.4开始,官方就明确表示不再使用,Version 3.2版本中更找不到关于虚拟内存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo还专门写了一篇论文,来反思Redis对虚拟内存(VM)存储技术的支持问题。
RDB持久化
RDB 就是 Redis DataBase 的缩写,中文名为快照/内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程
触发方式
触发RDB持久化的方式有两种,分别是手动触发和自动触发
RDB的优缺点
- 优点
- RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远小于于内存大小,适合备份、全量复制等场景。
- Redis加载RDB文件恢复数据远远快于AOF,因此RDB非常适用于灾难恢复(Disaster Recovery)。
- 缺点
- RDB方式实时性不够,无法做到秒级的数据持久化,虽然Redis允许设置不同的保存条件来控制保存RDB文件的频率,但是RDB文件需要做的是全量复制需要保存整个数据集,所以它并不是一个轻松的过程,当服务器故障发生宕机,可能因此丢失相当多的数据。
- 每次进行快照时,主进程需要
fork() 出一个子进程,由子进程来完成持久化工作。在数据集比较庞大时,fork() 过程都可能会非常耗时,造成服务器某时刻停止对服务器的请求处理。
AOF持久化
Redis是写后日志,先执行命令,将数据写入内存,再记录日志,日志里记录的是Redis收到的每一条命令
为什么采用写后日志?
- 避免额外的检查开销:Redis在想AOF里面记录日志时,并不会先去执行对命令的语法检测,所以先记录日志再执行命令的话,日志中有可能记录下错误的命令,当Redis利用日志恢复数据时,可能就会出错。
- 不会阻塞当前的写操作
当也同样存在风险:
- 如果命令执行完即将写入日志时,服务器宕机,这些数据操作未能写入日志,则这些数据将丢失。
实现AOF
AOF日志记录Redis的每个写操作,因此不需要触发,具体步骤分为命令追加append ,文件写入write 和文件同步sync 以及文件重写rewrite
-
命令追加append 当AOF持久化功能开启后,服务器在执行完一个写命令之后,会将写命令追加到服务器的aof_buf 缓冲区,而不是直接写入文件,主要是避免了每次写命令直接写入磁盘,导致磁盘IO操作成为Redis负载的瓶颈。 命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,有兼容性好,可读性强,容易处理等优点。 -
文件写入write 和文件同步sync Redis提供了多种AOF缓存区的文件同步策略,策略涉及到操作系统的write 函数和fsync 函数,为了提高文件写入效率,现代操作系统中,当用户调用write 函数将数据写入文件时,操作系统常会将数据暂存到一个内存缓冲区中,当缓冲区被存满或者达到一定时限后才会写入磁盘中,这样的操作虽然提高了效率但是也存在安全隐患,如果服务器宕机,内存缓冲区的数据将全部丢失。因此系统里也提供了fsync 函数等同步函数,可以强制将缓冲区的数据写入磁盘,以此保证数据的安全性。 AOF持久化中aof_buf 缓存区的文件同步策略由appendfsync 参数控制,各个参数含义如下:
always :命令写入aof_buf 后立刻调用系统的fsync 函数同步到AOF文件,fsync 操作完成后线程返回。这种情况下,aof_buf 缓冲区已然失去作用,每次写命令都要同步到AOF文件中,磁盘IO成为性能瓶颈,严重降低了Redis的性能;即便是用固态硬盘,每秒也只能处理几万个请求,而且会大大降低硬盘的寿命。no :命令写入aof_buf 后调用系统write 操作,不对AOF文件进行fsync 同步,同步操作由操作系统负责,通常同步周期为30s。这种情况下,同步操作完全由系统控制,文件的同步时间变得不可控,而且缓冲区堆积的数据会很多,安全性无法得到保证。everysec :命令写入aof_buf 后调用系统的write ,write 完成后线程返回;fsync 同步文件操作由专门的线程每秒调用一次。everysec 是前述两种策略的折中,是性能和数据安全性的平衡,实际开发中我们会优先选择这种策略。 -
文件重写rewrite Redis执行的写命令越来越大,AOF文件也会越来越大,过大的文件会影响服务器的运行,也会使文件恢复时用时过长。 文件重写是指定期对AOF文件进行重写,减少AOF文件的体积。Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。 文件重写之所以可以压缩AOF文件原因在于:
-
过期的数据不再写入文件 -
无效的命令不再写入文件:如有些数据被重复设值,有些数据被删除了等待 -
多条命令可以合并为一个:如sadd key value1 ,sadd key value2 ,sadd key value3 可以合并为sadd key value1 value2 value3 ,不过为了防止单条命令过大造成客户端缓冲区溢出,对于list ,set ,hash ,zset 类型的key,并不一定只使用一条命令,而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义,不可更改,3.0版本中值是64。 #define REDIS_AOF_REWRITE_ITEMS_PRE_CMD 64
配置文件
appendfilename "appendonly-6379.aof"
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-rewrite-incremental-fsync yes
要特别注意:no-appendfsync-on-rewrite :always和everysec的设置会使真正的I/O操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的Redis是没法解决的。为了尽量缓解这个情况,Redis提供了这个设置项,保证在完成fsync函数调用时,不会将这段时间内发生的命令操作放入操作系统的Page Cache(这段时间Redis还在接受客户端的各种写操作命令)。
重写机制
-
AOF重写会阻塞吗? AOF重写过程是由后台进程bgrewriteaof 完成的,主线程需要fork 出子进程,这个过程会占用主进程内存,所以如果频繁进行重写是会造成主线程阻塞的。 -
AOF日志何时会重写? auto-aof-rewrite-min-size :表示运行AOF重写文件的最小大小,默认为64MB auto-aof-rewrite-percentage :这个值的计算方式是,当前aof文件大小比上次重写后的aof文件大小的差值与上次重写后aof文件大小的比值;percentage = (last - now) / last -
重写日志时,有新的数据写入怎么做? 关于文件重写,要特别注意两点:
- 重写由子进程进行
- 重写阶段Redis执行的写命令需要追加到新的AOF文件中,为此Redis引入了
aof_rewrite_buf 缓存区 ? ? 对比上图,文件重写的流程如下:
-
1>执行AOF请求 如果当前进程正在执行bgrewriteaof ,则返回请求,请求不执行 如果当前进程正在执行bgsave ,则重写命令延迟到bgsave 完成之后进行 -
2>父进程fork 创建子进程,开销相当于bgsave 创建子进程的开销 -
3.1>主进程fork 操作完成后继续相应其他命令 所有修改命令依然写入aof_buf 缓冲区根据appendfsync 参数采取策略同步到磁盘,保证原有数据同步 -
3.2>fork 操作运用写时复制基数,子进程只能共享fork 操作时的内存数据 由于父进程依然响应命令,会有新的数据写入,Redis使用aof_rewrite_buf 重写缓冲区来保存这部分数据,防止新生成的文件生成期间丢失这部分数据。 -
4>子进程按照命令合并规则写入到新的AOF文件 每次批量写入的硬盘数据量由aof-rewrite-incremental-fsync 参数控制,默认为32MB,防止单次写入数据造成磁盘IO阻塞 -
5.1>新的AOF文件写入完成后,发送信号给主进程,通知主进程更新统计信息 -
5.2>父进程将aof_rewrite_buf 重写缓冲区新写入的数据更新到新的AOF文件中 -
5.3>使用新的AOF文件代替旧的AOF文件 总结: 1.父进程fork 子进程完成AOF文件重写 2.父进程将新写入的数据保存到aof_buf 和aof_rewrite_buf 缓冲区,子进程重写完毕后从aof_rewrite_buf 缓存区将新的数据写入新的AOF文件 3.新的AOF文件替代旧的AOF文件 -
为什么AOF重写不复用旧的AOF文件? 1.父子进程写同一个文件会产生竞争关系,影响了父进程的性能 2.如果AOF重写失败,会污染原本的AOF文件,无法再作为数据恢复的参考
重启加载
AOF和RDB文件都可以用于服务器重启时的数据恢复。下面展示Redis持久化文件加载流程:
流程说明:
1)AOF持久化开启且存在AOF文件时,优先加载AOF文件。
2)AOF关闭或者AOF文件不存在时,加载RDB文件。
3)加载AOF/RDB文件成功后,Redis启动成功。
4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。
六、事务
Redis通过MULTI ,EXEC ,WATCH ,DISCARD 等命令来实现事务Transaction 功能。事务体哦概念股了一种将多个命令请求打包,然后一次性按顺序地执行多个命令的机制,并且事务执行期间,服务器不会中断事务而去改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后 才去执行其他客户端的命令请求。
以下是一个事务执行的过程,该事务从一个MULTI 开始,接着将多个操作命令放入事务中,然后最后EXEC 将事务提交给服务器执行。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set key value
QUEUED
127.0.0.1:6379(TX)> get key
QUEUED
127.0.0.1:6379(TX)> set key otherValue
QUEUED
127.0.0.1:6379(TX)> get key
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "value"
3) OK
4) "otherValue"
特别注意:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化,按顺序地执行,事务执行过程中也不会被其他客户端发来的命令请求打断
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部不执行
EXEC 命令复制触发并执行事务中的所有命令
- 如果客户端使用
MULTI 开启一个事务后,因为断线导致EXEC 没有执行,那么事务中的所有命令都不会被执行 - 而如果
EXEC 成功执行,那么事务中所有命令都会被执行
当使用AOF 持久化时,Redis会使用单个write 命令将事务写入磁盘中,如果Redis服务器宕机,那么只有部分事务命令会成功写入磁盘。
如果Redis重新启动发现了AOF文件有这样的问题,那么它会退出并汇报一个错误。
使用redis-check-aof 可以修复这一问题,它会移除AOF文件中不完整事务信息,以保证服务器顺利启动。
放弃事务
当执行DISCARD 命令时,事务会被放弃,事务队列清空,并且客户端从事务状态退出:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set key value
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> get key
"otherValue"
WATCH 命令
WATCH 命令是一个乐观锁Optimistic Locking ,它可以在EXEC 命令执行之前,监视任意顺序的数据库键,并在执行EXEC 命令时,检查被监视的值是否已经被修改,如果是,服务器将拒绝执行事务,并向客户端代表事务执行已经失败的回复。
redis> set key oldValue
OK
redis> watch key
OK
redis> MULTI
OK
redis(TX)> set key newValue
QUEUED
redis(TX)> EXEC
(nil) //事务失败
为什么上述事务失败了呢?
时间 | 客户端A | 客户端B |
---|
T1 | SET key oldValue | | T2 | WATCH key | | T3 | MULTI | | T4 | SET key newValue | | T5 | | SET key otherValue | T6 | EXEC | |
由上图所知,T5时刻,在客户端A执行EXEC 命令前,key 值已经被客户端B修改,此时服务器发现被监视的键key 的值已经被修改了,所以服务器会拒绝执行客户端A的事务,并向客户端A返回空回复。
上述客户端A的事务是不安全的,服务器会拒绝执行客户端提交的不安全的事务,以保证数据的一致性。
上述这种形式的锁叫做乐观锁,是一种强大的锁机制。
同时可以使用UNWATCH 命令取消对所有键key 的监视,注意!不是取消对单个或着几个键的监视,是取消所有。
事务的ACID性质
-
原子性 事务中具有原子性是指:事务中的多个操作当作一个整体来执行,服务器要么执行所有操作,要么一个操作都不执行。 以下展示的是一个成功执行的事务,所有命令都被执行: 127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> get key
QUEUED
127.0.0.1:6379(TX)> set key value
QUEUED
127.0.0.1:6379(TX)> get key
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (nil)
2) OK
3) "value"
与此相反,以下展示了一个执行错误的事务,这个事务因为命令入队时错误而被服务器拒绝执行,事务中的所有命令都不会被执行: 127.0.0.1:6379> set key value
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set key newValue
QUEUED
127.0.0.1:6379(TX)> gett key
(error) ERR unknown command `gett`, with args beginning with: `key`,
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get key
"value"
Redis的事务与传统的关系型数据库事务的最大区别就在于,Redis不支持事务回滚机制rollback ,即使队列中某个命令执行期间出现了错误,整个事务也不会回滚,而是继续执行下去。 在下面的例子中,SADD 命令执行期间发生了错误,后续的命令也会继续执行下去,而且之前执行的事务也不会受到影响 127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set key newValue
QUEUED
127.0.0.1:6379(TX)> SADD key ERROR
QUEUED
127.0.0.1:6379(TX)> get key
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "newValue"
Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能与Redis追求简单高效的设计初衷不相符,并且他认为Redis事务执行时的错误都是因为程序错误产生的,这种错误通常只会出现在开发环境中,而很少在实际的生成环境中出现,所以他认为没有必要为Redis开发事务回滚功能。
-
一致性 事务具有一致性是指:如果数据库在执行事务之前是一致的,那么事务执行之后无论事务是否执行成功,数据库也应该仍然是一致的。 一致性是指数据库符合数据库本身的定义和要求,没有包含非法或者无效的错误数据 Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。 -
隔离性 事务的隔离性指的是,即使数据库中有多个事务并发执行,各个事务也不会互相影响,并且在并发情况下执行的事务和串行执行的事务产生的结果完全相同。 因为Redis使用单线程的方式来执行事务,并且服务器保证,在事务执行期间,其他客户端不会中断该事务,因此Redis的事务总是以串行的方式执行,并且事务也是具有隔离性的。 -
耐久性 事务的耐久性指定是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质中,即使服务器执行完事务后发生宕机,执行事务的结果也不会丢失。 因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,使之成为一个整体,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务中的耐久性是基于Redis所使用的持久化模式。
- 当服务器在无持久化的内存模式运作时,事务不具有耐久性,一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
- 当服务器在RDB持久化模式运作下,服务器只会在特定的保存条件被满足时,才会执行
bgsave 命令,对数据库进行保存操作,并且异步执行的bgsave 不能保证事务数据被第一时间保存的硬盘里面,因此RDB持久化模式下的事务也不具有耐久性 - 当服务器运行在AOF持久化模式下,并且
appendfsync 参数的值为always 时,程序总会执行命令之后调用同步sync 函数,将数据真正保存到硬盘里面,这种配置下的事务是具有耐久性 - 当服务器运行在AOF持久化模式下,并且
appendfsync 参数的值为everysec 时,程序会每秒同步一次命令数据到硬盘中,如果服务器发生宕机,可能会造成事务数据丢失,这种配置下的事务也不具有耐久性 - 当服务器运行在AOF持久化模式下,并且
appendfsync 参数的值为no 时,程序会交由操作系统为决定何时,将命令数据同步到硬盘中,因为事务数据可能在等待同步中丢失,这种配置下的事务也不具有耐久性
七、主从复制
在Redis中,用户可以通过执行SLAVEOF 命令或者设置slaveof 配置,让一个服务器去复制(replicate)另一个服务器,被复制的的服务器叫主服务器(master),对主服务器进行复制的服务器叫从服务器(slave)。
进行复制的主从服务器双方的数据库都将保存相同的数据,概念上将这种现象称为“数据库状态一致”。
旧版复制功能的实现
Redis2.8版本之前的复制功能分为同步(sync)和命令传播(command propagate)两个操作。
- 同步用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
- 命令传播用于当主服务器数据库的状态被修改,导致主从服务器数据库状态不一致,让主从服务器数据库回到一致状态。
复制
当从服务器复制主服务器时,需要先进行执行同步操作,从服务器需要通过向主服务器发送SYNC 命令来完成,以下是SYNC 命令的执行步骤:
-
从服务器向主服务器发送SYNC 命令 -
收到SYNC 命令的主服务器执行BGSAVE 命令,在后于生成RDB 文件,并使用一个缓冲区记录从现在开始执行的所有写命令。 -
当主服务器的BGSAVE 命令执行完毕时,主服务器会将BGSAVE 生成的RDB 文件发送给从服务器,从服务器接收并载入RDB 文件,将自己数据库状态更新至主服务器执行BGSAVE 命令时的数据库状态。 -
主服务器将记录在缓冲区里面所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。 同步完整过程:
命令传播
在同步操作执行完毕之后,主从服务器两者的数据库达到一致,每当主服务器执行客户端发送的写入命令时,数据库就会被修改,主从服务器数据库状态不一致。
为了让主从服务器回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,发送给从服务器执行相同的写命令之后,主从服务器再次回到一致状态。
旧版复制的缺陷:
断线后重新复制:处于命令传播阶段的主从服务器因为网络问题断开了连接终止了复制,但从服务器重写连接主服务器后会重新进行复制,但是这种复制是全量复制,开销相当大。主从服务器断线期间,主服务器执行的写命令可能多或少,但是服务器为了弥补这一小部分缺失的数据,就要主从服务器重写执行一次SYNC ,这种做法无疑是低效的。
新版复制功能的实现
为了解决旧版复制在处理断线重复值情况的低效率问题,Redis从2.8版本之后推出了PSYNC 命令来代替SYNC 命令执行复制操作。
PSYNC 命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
- 完整重同步用于处理初次复制情况,完整重同步的执行步骤和
SYNC 命令的执行步骤基本一样,通过主服务器创建并发送RDB 文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步 - 部分重同步则用于处理断线后重复值的情况:当从服务器断线后重新连接主服务器,只复制断开期间主服务器写入的数据即可,不需要再做一次完全重同步。
部分重同步
部分重同步功能由以下三个部分构成:
- 主从服务器的复制偏移量(replication offset)
- 主服务器的复制积压缓冲区(replication backlog)
- 服务器的运行ID(run ID)
复制偏移量
执行复制的主从服务器都会分别维护一个复制偏移量:
- 主服务器每次向从服务器传播N个字节的数据时,就会将自己的复制偏移量加入N
- 从服务器收到主服务器传播来的N个字节的数据时,也会将自己的复制偏移量加入N
通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列组成,默认存储大小为1MB。
当主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区。
因此,主服务器的复制积压缓冲区里面会保存着一部分最近播放的写命令,并且复制缓冲区会为队列中的每个字节记录相应的复制偏移量。当从服务器重写连接上主服务器之后,从服务器会通过PSYNC 将自己的复制偏移量offset 发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
- 如果
offset 偏移量之后的数据仍然存在于复制积压缓冲区中,那么主服务器将对从服务器进行部分重同步操作。 - 相反,如果
offset 偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。
根据需要调整复制积压缓冲区的大小
Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量的写操作,或者从服务器断线后重连接的时间较长,那么这个值可能并不合适,这个值设置不得当,可能会让从服务器重新连接主服务器后,让主服务器判定从服务器需要进行完整重同步,那么PSYNC 命令的部分重同步模式就不能正常发挥作用。
复制积压缓冲区的最小大小可以根据公式reconnect_second *write_size_per_second 来估算,reconnect_second 为重新连接所需要时间,write_size_per_second 为主服务器平均每秒写入的命令数据量,然后在此基础上将这个大小翻倍,即可满足大部分断线情况下重连后都能用部分重同步。
可参考:repl-backlog-size = 2*reconnect_second *write_size_per_second
服务器运行ID
-
每个Redis服务器无论主从都会自己的运行ID -
运行ID在服务器启动时,自动生成,由40个随机的十六进制字符组成。 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器也会将这个运行ID保存起来。 当从服务器断线重连上一个主服务器时,会向主服务器将之前保存的运行ID:
- 如果从服务器保存的运行ID与当前连接的主服务器运行ID相同,则说明之前连接的就是这个服务器,主服务器可以尝试执行部分重同步。
- 反之,则说明之前连接的主服务器不是当前连接的服务器,主服务器会对从服务器执行完整同步操作。
PSYNC 命令的实现
PSYNC 命令的调用有两种方式:
- 如果从服务器以前没有复制过任何服务器,或者之前执行过
SLAVEOF no one 命令,那么从服务器在开始依次新的复制时将对主服务器发送PSYNC ? -1 命令,主动请求主服务器进行完整重同步。 - 相反,从服务器已经复制过某个主服务器的数据,那么从服务器在开始一次新的复制前,会向主服务器发送
PSYNC <runid> <offset> 命令,runid 是上次复制的主服务器的运行ID,而offset 是当前从服务器的复制偏移量,接受到这个命令的主服务器会根据这两个参数来决定对从服务器执行哪种同步操作。
根据情况,收到PSYNC 的主服务器回向从服务器返回以下的三种回复中的其中一种:
- 如果主服务器返回
+FULLRESYNC <runid> <offset> ,表示主服务器将与从服务器执行完整重同步操作,runid 是这个主服务器的运行ID,而从服务器会将这个运行ID保存起来,在下次发送PSYNC 命令时使用,而offset 是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量 - 如果主服务器返回
+CONTINUE ,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只需要等候主服务器将自己缺少的那部分数据发送过来完成同步即可。 - 如果主服务器返回
-ERR ,那么表示主服务器的版本低于2.8识别不了PSYNC 。
复制的完整流程
通过向从服务器发送SLAVEOF 命令,可以让一个从服务器去复制主服务器:SLAVEOF <master_ip> <master_port>
步骤一:设置主服务器的地址和端口
从服务器将客户端给定的主服务器IO地址以及端口保存到服务器状态里的masterhost 属性和masterport 属性里面。
需要注意的是:SLAVEOF 命令是一个异步任务,在完成masterhost 属性和masterport 属性的设置工作之后,从服务器将发送SLAVEOF 命令的客户端返回OK,表示复制指令已经被接受,但是真正的复制工作是在OK返回之后才开始真正执行的。
步骤二:建立套接字连接
在SLAVEOF 命令执行后,从服务器会根据命令所设置的IP地址以及端口号,创建连向主服务器的套接字(socket)连接。
如果连接成功,那么从服务器将会为这个套接字关联一个专门用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作,比如接受RDB文件以及接受主服务器传播过来的写命令。
主服务器在接受从服务器的套接字连接之后,将会该套接字创建相应的客户端状态,并将从服务器看作是一个连接到主服务器的客户端来对待,此时从服务器同时具有服务器和客户端两个身份,而接下来的复制工作都会以从服务器向主服务器发送命令请求的形式来执行,因此理解“从服务器是主服务器的客户端”这点相当重要。
步骤三:发送PING命令
从服务器成为主服务器的客户端之后,做的第一件事情就是向主服务器发送一个PING 命令。
PING 命令有以下作用:
- 因为主从服务器创建了套接字连接之后未进行过任何通信,所以先要检查套接字的读写状态是否正常。
- 检查主服务器是否能正常处理命令请求。
从服务器发送PING 命令之后会遇到以下三种情况:
- 主服务器向从服务器返回一个命令回复,但从服务器未能在限定时间里读取命令回复的内容,则表示主从服务器之间的网络连接状态不佳,此时需要从服务器断开连接并重新建立连向主服务器的套接字
- 主服务器返回一个错误,表示主服务器暂时无法处理从服务器的处理请求,不能执行之后的复制工作,此时需要从服务器断开连接并重新建立连向主服务器的套接字。
- 如果从服务器成功读取到
PONG 回复,那么表示主从服务器的连接状态正常,可以继续执行以下的复制操作。
步骤四:身份验证
从服务器收到主服务器返回的PONG 之后,下一步要进行的就是是否需要进行身份验证:
- 如果从服务器设置了
masterauth ,那么需要进行 - 反之,则不需要进行
需要进行身份验证的情况下,从服务器向主服务器发送一条AUTH 命令,命令的参数为从服务器中masterauth 参数的值。
从服务器身份验证阶段可能遇到的情况有以下几种:
- 主服务器没有设置
requirepass 选项,而且从服务器也没有设置masterauth ,那么主服务器将继续从服务器发送的命令,复制操作可以继续执行 - 如果从服务器通过
AUTH 命令发送的密码与主服务器requirepass 所设置的密码相同,则继续执行复制操作,反之,主服务器将返回一个invalid password 错误 - 如果主服务器设置了
requirepass 但是从服务器没有设置masterauth ,那么主服务器会返回NOAUTH 错误 - 如果主服务器没有设置
requirepass ,但是从服务器设置了masterauth ,那么主服务器将返回no password is set 错误
所有错误都会让从服务器终止当前的复制工作,并重新创建套接字开始重新进行验证,直至身份验证通过或者从服务器放弃复制为止。
步骤五:发送端口信息
身份验证过后,从服务器将执行REPLCONF listening-port <port-number> ,向主服务器发送从服务器的监听端口号。
主服务器接受该命令之后会将端口号记录在slave_listening_port 属性中。目前该属性的唯一作用就是主服务器执行INFO replication 命令时打印出从服务器的端口号信息。
步骤六:同步
这一步中,从服务器将向主服务器发送PSYNC 命令,执行同步操作,将自己的数据更新至与主服务器数据库当前所处状态
步骤七:命令传播
完成同步之后,主从服务器会进入命令传播阶段,主服务器会一直将执行的写命令发送给从服务器,从服务器会一直接受并执行主服务器发送来的命令,以此保证主从服务器数据库的一致性。
心跳检测
在命令传播阶段,从服务器会默认以每秒一次的频率向主服务器发送命令:REPLCONF ACK <replication_offset>
其中replication_offset 是当前从服务器的复制偏移量,发送REPLCONF ACK <replication_offset> 命令对主从复制有以下三个作用:
- 检测主从服务器的网络连接状态
- 辅助实现
min-slaves 参数 - 检测命令丢失
以下将介绍这三个作用的具体实现
检测主从服务器的网络连接状态
主从服务器通过发送和接收REPLCONF ACK <replication_offset> 命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有接收到REPLCONF ACK <replication_offset> 命令,那么主服务器就知道从服务器的连接状态出现问题了。
通过向主服务器发送INFO replication 命令,在列出的从服务器列表中的lag 一栏中,会看到对应的从服务器上一次向主服务器发送REPLCONF ACK <replication_offset> 命令距离现在过了多少秒:
在一般情况下,lag 值应该在0秒和1秒之间跳动,如果超过1秒,则说明主从服务器可能出现问题。
辅助实现min-slaves 选项
Redis的min-slaves-to-write 和min-slaves-max-lag 两个选项可以防止主服务器在不安全的情况下执行写命令。
min-slaves-to-write 3
min-slaves-max-lag 10
以上配置中,如果从服务器数量少于3个或者三个从服务器的延迟(lag)值都大于10秒时,则主服务器将拒绝执行写命令。
检测命令丢失
如果因为网络故障,主服务器传播给从服务器的写命令在中途丢失,那么从服务器向主服务器发送REPLCONF ACK <replication_offset> 命令,主服务器发现从服务器当前复制偏移量少于自己的偏移量,然后就会根据从服务器提交的偏移量在复制积压缓冲区里找到从服务器缺失的数据,并将这些数据重新发送给从服务器。
主服务器向从服务器补发缺失数据这一操作的原理与部分重同步操作的原理相似,两者的区别在于,补发缺失数据是在主从服务器没有断开连接的情况下执行的,而部分重同步是主从服务器断线后重连执行的,
八、哨兵
哨兵(Sentinel)是Redis高可用性(High Availability)解决方案:由一个或多个Sentinel实例组成的哨兵系统可以监视多个主服务器以及这些主服务器属下的从服务器,并在被监视的主服务器进行下线状态后,进行故障转移,让某个从服务器升级为主服务器,代替已下线的主服务器继续处理请求命令。
哨兵实现的功能:
- 监控(Monitoring):哨兵会不断检查主节点和从节点是否正常运作
- 自动故障转移(Automaitc Failover):当主节点不能正常工作时,哨兵会开始自动转移故障,将失效主节点的其中一个从节点升级为主节点,并让其他从节点复制新的主节点
- 配置提供者(Configuration Provider):客户端初始化时,通过连接哨兵来获得当前Redis服务的主节点地址
- 通知(Notification):哨兵可以将故障转移的结果发送给客户端
以下是一个哨兵系统监视服务器的例子:
其中,双环的server1是当前的主服务器,单环的表示主服务器的三个从服务器,哨兵系统监视着四个服务器。
此时server1出现故障,进入下线状态,三个从服务器的复制操作将被终止,并且哨兵系统会监察到server1已下线。
当server1的下线时长超过了用户设定的下线时长上限时,哨兵系统就会对server1进行故障转移
- 首先,哨兵系统会挑选server1的下属的其中之一从服务器,并将这个从服务器升级为主服务器。
- 哨兵系统会向其他的服务器发送新的复制指令,让他们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。
- 另外,哨兵系统还会监视已下线的server1,并在它重新上线时,将它设置为新的主服务器的从服务器。
启动并初始化Sentinel
启动一个Sentinel:
redis-sentinel sentinel.conf
当一个Sentinel启动时,它需要执行以下步骤:
- 初始化服务器
- 将普通Redis服务器使用的代码替换成Sentinel专用代码
- 初始化Sentinel状态
- 根据给定的配置文件,初始化Sentinel的监视主服务器列表
- 创建连向主服务器的网络连接
初始化服务器
Sentinel本质上就是一个运行在特殊模式下的Redis服务器,这个模式叫做哨兵模式。启动Sentinel的第一步就是初始化一个普通的Redis服务器。不过Sentinel执行的工作跟普通的Redis服务器执行的工作不一样,所以两者的初始化过程也不完全相同。
例如,Sentinel服务器并不使用数据库,所以初始化Sentinel不会载入RDB文件或者AOF文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2QS5iXy-1631271000165)(C:/Users/Supreme%20honor/Desktop/NoteBook/redis21.jpg)]
使用Sentinel专用代码
启动Sentinel的第二个步骤就是将一部分普通Redis服务器使用的代码进行替换。
例如普通Redis服务器使用redis.h/REDIS_SERVERPORT 常量值作为服务器端口
# define REDIS_SERVERPORT 6379
而Sentinel服务器使用sentinel.c/REDIS_SENTINEL_PORT 常量作为服务器端口
# define REDIS_SENTINEL_PORT 26379
并且Sentinel会使用sentinel.c/sentinelcmds 作为服务器的专用命令表,sentinelcmds 命令表也解释了为什么Sentinel模式下,Redis服务器不能执行诸如SET ,DEL ,DBSIZE 等命令,因为服务器载入的时候就没有载入这些命令。
Sentinel模式下客户端可以执行的全部命令有:PING SENTINEL INFO SUBCRIBE UNSUBSCRIBE PSUBSCRIBE PUNSUBSCRIBE
初始化Sentinel状态
服务器 初始化一个sentinel.c/sentinelState 结构(简称:Sentinel状态),这个结构中保存了服务器中所有和Sentinel有关的状态信息。
struct sentinelState
{
//当前纪元,用于实现故障转移
uint64_t current_epoch;
//保存了所有被sentinel监视的主服务器
//字典的键是主服务器的名字,值是指向主服务器的指针
dict *masters;
//是否进入TILT模式
int tilt;
//正在执行的脚本数量
int running_scripts;
//最后一次执行时间处理器的时间
mstime_t previous_time;
//进入TILT模式的时间
mastime_t tilt_start_time;
//一个FIFO队列。包含了所有需要执行的用户脚本
list *scripts_queue;
}
初始化Sentinel状态的masters属性
如上述代码可知,sentinelState 结构体中的masters 字典记录了所有被Sentinel监视的主服务器的相关信息。
其中:
-
字典的键是被监视的主服务器的名称 -
字典的值是被监视的主服务器对应的sentinel.c/sentinelRedisInstance 结构。 每一个sentinel.c/sentinelRedisInstance 都代表一个被Sentinel监视的Redis服务器实例,这个实例可以是主服务器,从服务器或另外一个哨兵Sentinel。 typedef struct sentinelRedisInstance
{
//标识值,记录了实例的类型,以及该实例的状态
int flags;
//实例的名称
//主服务器的名称在配置文件中设置
//从服务器以及Sentinel的名称由Sentinel自动设置
//格式为 ip:port
char *name;
//运行ID
char *runid;
//配置纪元,实现故障转移
uint64_t config_epoch;
//实例的地址
sentinelAddr * addr;
//SENTINEL down-after-milliseconds选项设定的值
//实例无响应多少秒之后才会被判定为主观下线
mstime_t down_after_period;
//SENTINEL monitor <master-name> <ip> <port> <quorum>
//判断该实例客观下线需要的的支持投票数量
int quorum;
//SENTINEL parallel-synuc <master-name> <number>选项的值
//在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
//SENTINEL failover-timeout <master-name> <ms>选项的值
//刷新故障迁移状态的最大时限
mstime_t failover_timeout;
//...
}sentinelRedisInstance;
typedef struct sentinelAddr
{
char *ip;
int port;
}sentinelAddr;
创建连向主服务器的网络连接 初始化Sentinel的最后一步是创建连向被监视的主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。 对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:
- 一个是命令连接,这个连接用于向主服务器发送命令,并接受命令回复。
- 另一个是订阅连接,这个连接用于订阅主服务器的
__sentinel__:hello 频道
为什么要有两个连接?
- Redis目前的发布与订阅功能中,被发送的信息都不会在Redis服务器中保存,如果这条信息发送时,要接受信息的客户端不在线或者掉线,那么这个客户端就会丢失该信息,因此为了不丢失
__sentinel__:hello 频道的信息,Sentinel必须专门用另外一个订阅连接来接受该频道的信息。 - 除了订阅频道之外,Sentinel还必须向被它监视的主服务器发送命令,以此来与主服务器进行通讯,所有Sentinel还必须向主服务器创建命令连接。
- 因为Sentinel需要和多个实例创建多个网络连接,所以Sentinel使用的是异步连接。
获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过连接向被监视的主服务器发送INFO 命令,并通过分析INFO 命令的回复来获取主服务器的当前信息。
通过分析主服务器返回的INFO 命令回复,Sentinel可以获得以下两方面信息:
- 一方面是关于主服务器 本身的信息,包括
run_id 域记录的服务器运行ID,以及role 域记录的服务器角色。 - 另一方面是关于主服务器属下的所有从服务器的信息,每一个从服务器由一个
slave 字符串开头的行记录,每行中会显示从服务器的IP 地址和port 端口号,根据这些信息,Sentinel无需用户提供从服务器的地址信息就可以自动发现从服务器。
根据上述信息,Sentinel对主服务器的实例结构进行更新,例如,主服务器重启后的运行ID与之前保存的运行ID不同,Sentinel会检测到该情况,对实例结构的运行ID进行更新。
获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建新的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接。
创建命令连接之后,Sentinel会以十秒一次的频率通过向从服务器发送INFO 命令,并获得以下内容的回复:
- 从服务器的运行
ID - 从服务器的角色
role - 主服务器的IP地址
master_host ,以及主服务器的端口号master_port - 主从服务器的连接状态
master_link_status - 从服务器的优先级
slave_priority - 从服务器的复制偏移量
slave_repl_offset
根据这些信息,Sentinel会对从服务器的实例结构进行更新。
向主从服务器发送信息
默认情况下,Sentinel会以每两秒一次的频率通过命令连接向所有被监视的主服务器和从服务器发送以下格式命令:
PUBLISH _sentinel_:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
分别记录Sentinel和其监视的主服务器的IP地址,端口号,运行ID以及当前的配置纪元。
接收来自主从服务器的的频道信息
当Sentinel与一个主服务器或从服务器建立起订阅连接之后,Sentinel就会通过订阅连接向服务器发送以下命令:
SUBCRIBE _sentinel_:hello
Sentinel对_sentinel_:hello 频道的订阅会一直持续到Sentinel与服务器的连接断开为止。
每个与Sentinel连接到服务器,Sentinel既通过命令连接向服务器的_sentinel_:hello 频道发送信息,又通过订阅连接从服务器的_sentinel_:hello 频道接收信息。
当一个Sentinel从_sentinel_:hello 频道接收到信息之后,会对该信息进行分析,提取出Sentinel IP Sentinel post Sentinel runID 等八个参数信息,并进行以下检查:
- 如果信息中的运行ID与接收信息的运行ID相同,则说明是自己发送的,丢弃该信息,不予处理。
- 反之,说明这条信息是由监视同一个服务器的其他Sentinel发来的,接收信息的Sentinel会根据信息重点各种参数,而对主服务器的实例结构进行调整更新。
更新sentinels 字典
Sentinel为主服务器创建的实例结构中的sentinels 字典,不仅保存Sentinel本身,还有所有同样监视这个主服务器的其他Sentienl资料。
创建连向其他Sentinel的命令连接
当Sentinel通过频道信息发现了一个新的Sentinel时,它不仅为会新Sentinel在sentinels 字典中创建对应的实例结构,还会创建一个连向新Sentinel的命令连接,而新的Sentinel也会创建连接到这个Sentinel的命令连接,从而让哨兵系统中的多个Sentinel形成相互连接的网络。
使用命令连接的各个Sentinel通过命令请求来进行信息交换。
Sentinel之间不会创建订阅连接
Sentinel在连接主从服务器时会创建命令连接和订阅连接,但是在连接Sentinel时只会创建命令连接,这是因为Sentinel需要通过接收主从服务器发来的频道信息发现未知的Sentinel,所以才需要创建订阅连接,而互相已知的Sentinel则只需要通过命令连接进行通讯即可。
检测主观下线状态
在默认情况下,Sentinel会以每秒一次的频率向其他创建了命令连接的实例(主从服务器、其他Sentinel在内)发送PING 命令,通过实例返回的回复的来判断实例是否在线。
服务器对PING 命令的有效回复是以下三种的其中一种:
+PONG -LOADING 错误-MASTERDOWN 错误
如果服务器返回了除以上三种之外的其他回复,又或者在指定时间内没有回复PING 命令,则Sentinel认为服务器返回的回复无效。
一个服务器在master-down-after-milliseconds 毫秒内一直返回无效信息则会被Sentinel判定为主观下线。
检查客观下线状态
当Sentinel将一个主服务器判定为主观下线之后,为了确认这个主服务器是否真的下线,它会向其他监视这一主服务器的其他Sentinel进行询问,当Sentinel从其他Sentinel接收到足够的已下线判断时,Sentinel就会将这个主服务器判定为客观下线,并进行故障转移。
发送SENTINEL is-master-down-by-addr <ip> <port> <current-epoch> <runid> 命令向其他Sentinel询问意见。
参数 | 意义 |
---|
ip | 被Sentinel判定为主观下线的主服务器的IP地址 | port | 被Sentinel判定为主观下线的主服务器的端口号 | current_epoch | Sentinel当前的配置纪元,用于选举领头Sentienl | runid | 可以是* 符号或者是Sentinel的运行ID:* 符号表示命令仅仅用于主服务器的客观下线状态,而Sentinel的运行ID用于选举领头Sentinel |
目标Sentinel接收SENTINEL is-master-down-addr 命令
当一个Sentinel(目标Sentinel)接收到另一个Sentinel(源Sentinel)发来的SENTINEL is-master-down-addr 命令时,目标Sentinel会分析并取出命令请求中的各个参数,并根据主服务器的IP和端口号,判断主服务器是否已经下线,然后向源Sentinel返回SENTINEL is-master-down-by <down_state> <leader_runid> <leader_epoch> 命令
参数 | 意义 |
---|
down_state | 返回目标Sentinel对服务器的检查结果,1表示主服务器已经下线,0表示主服务器未下线 | leader_runid | 可以是* 符号或者目标Sentinel的局部领头Sentinel的运行ID,* 符号表示主服务器的下线状态,而局部领头Sentinel 的运行ID则用于选举领头Sentinel | leader_epoch | 目标Sentinel的局部领头Sentinel的配置纪元,用于选举领头Sentinel,仅在leader_runid 不为* 时有效,如果leader_runid 的值为* ,那么leader_epoch 的值为0 |
举例:一个目标Sentinel返回SENTINEL is-master-down-by <1> <*> <0> 命令给源Sentinel,则说明目标Sentinel同意主服务器已经下线。
源Sentinel接收SENTINEL is-master-down-by 命令
源Sentinel根据其他目标Sentinel发回的SENTINEL is-master-down-by 命令,Sentinel统计其他Sentinel同意主服务器下线的数量,当这一数量达到配置指定的判断客观下线所需数量时,Sentinel就会将主服务器实例结构的flags 属性的SRI_O_DOWN 标识打开,表示该服务器已经下线。
客观下线的判断条件
Sentinel配置文件中写入了sentinel monitor mymaster 127.0.0.1 6379 2 ——配置的含义是:该哨兵节点监控192.168.92.128:6379这个主节点,该主节点的名称是mymaster,最后的2的含义与主节点的故障判定有关:至少需要2个哨兵节点同意,才能判定主节点故障并进行故障转移。
选举领头Sentinel
当一个主服务器被判断为客观下线,监视这个下线的主服务器的各个Sentinel回进行协商,选出一个领头Sentinel,并由领头Sentinel对下线的主服务器进行故障转移操作。
以下是Redis选举领头Sentinel的规则和方法:
故障转移
在选举出领头Sentinel之后,领头Sentinel将对已下线的主服务器进行故障转移操作:
- 从已下线的主服务器属性的从服务器中挑选一个转换为主服务器
- 让已下线的主服务器属性的其他从服务器改为复制新的主服务器
- 将已下线的主服务器设置为新的主服务器的从服务器,当它重新连接上来时就会成为新的主服务器的从服务器
挑选新的主服务器
在已下线的主服务器属下的所有从服务器中,挑选一个状态良好,数据完整的从服务器,然后向它发送SLAVEOF no one 命令,将这个从服务器转换为主服务器。
新的主服务器是如何挑选的呢?
领头Sentinel回将已下线的主服务器的所有从服务器保存到一个列表中,如何进行一项一项地筛选:
-
删除列表中所有处于下线或者断线的从服务器,保证列表中的服务器都是在线状态良好的 -
删除列表中所有最近五秒内没有回复过领头Sentinel的INFO 命令的从服务器,保证列表中都是最近进行成功通讯的服务器 -
删除所有与已下线主服务器断开连接超过down-after-millisecond * 10 毫秒的从服务器,保证列表中的从服务器都没有过早地与主服务器断开连接,以此保证数据完整。 -
从以上淘汰中存留下来的服务器,会根据复制偏移量来继续进行筛选,(复制偏移量最大的从服务就是保存着最新数据的服务器); 如果复制偏移量不可用,则会根据服务器的runID 来进行选择,选择runID 小的服务器成为主服务器。
修改从服务器的复制目标
当新的主服务器出现之后,领头Sentinel下一步做的就是,让其他从服务器去复制新的主服务器,可以通过向从服务器发送SLAVEOF 实现。
将旧的主服务器变成从服务器
故障转移操作最后要做的就是将已下线的主服务器设置为新的主服务器的从服务器。
当已下线的主服务器重新连接后Sentinel就会向其发送SLAVEOF 命令,使其成为新的主服务器的从服务器,如下图所示:
Sentinel自动故障转移的一致性特质
Sentinel自动故障转移使用Raft算法来选举领头Sentinel,从而确保在一个给定的纪元里面,只有一个领头产生。
这表示同一个纪元中,不会有两个Sentinel同时被选为领头,并且各个Sentinel在同一个纪元中,只会对一个领头进行投票。
更高的配置纪元总是优于较低的纪元,因此每个Sentinel都会主动使用更新的纪元来代替自己的配置。
可以这样说,我们将Sentinel配置看作一个带有版本号的状态,一个状态会以最后写入者的方式保留下来,当一个有着比较旧的配置的Sentinel接收到其他Sentinel发来的版本更新的配置时,就会将自己的配置进行更新。
Sentinel状态的持久化
Sentinel 的状态会被持久化在 Sentinel 配置文件里面。
每当 Sentinel 接收到一个新的配置, 或者当领头 Sentinel 为主服务器创建一个新的配置时, 这个配置会与配置纪元一起被保存到磁盘里面。
这意味着停止和重启 Sentinel 进程都是安全的。
九、集群
上述的高可用方案:持久化,主从复制和哨兵,但这些方案仍然存在不足,其中主要的问题就是存储能力受单机限制,以及无法实现写操作的负载均衡。
集群的作用
集群,即Redis Cluster ,是Redis3.0开始引入的分布式存储方案。
集群有多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:主节点负责读写请求和集群信息的维护,从节点进行主节点数据和状态信息的复制。
集群的作用归纳为以下两点:
集群的搭建
集群的搭建有两种方式:(1)手动执行Redis命令,一步步完成搭建;(2)使用Ruby脚本搭建。两者原理相同,后者对前者使用到的Redis命令进行封装打包。
执行Redis命令搭建集群
集群的搭建分为四步:
- 启动节点:将节点以集群模式启动,此时节点是独立的,没有建立与其他节点的连接
- 节点握手:让各个独立的节点连接成一个网络
- 分配槽:将16384个槽分配给各个主节点
- 指定主从关系
启动节点
集群节点的启动依然是使用redis-server 命令,但需要以集群模式启动,以下是节点的配置文件
dbfilename "dump-6379.rdb"
port 6379
daemonize no
rdbcompression yes
rdbchecksum yes
save 10 2
appendonly yes
appendfsync always
appendfilename "appendonly-6379.aof"
bind 127.0.0.1
databases 16
cluster-enabled yes
cluster-config-file "nodes-6379.conf"
cluster-node-timeout 10000
cluster-enabled yes :Redis实例可以分为单机模式standAlone 和集群模式cluster ,这个设置可以开启节点的集群模式
集群模式下的节点,其redis-mode 为cluster ,如下图所示:
cluster-config-file :指定了集群配置文件的位置,每个节点运行过程中会维护一份集群配置文件;当集群信息发生变化,集群中的所有节点会将最新信息更新到该配置文件中;当节点重启时会读取该配置文件,获取集群信息。Redis节点以集群模式启动时,会首先寻找是否有集群信息文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。
编辑好配置文件后,通过redis-server 命令启动节点:
redis-server redis-6379.conf
节点启动以后,通过cluster nodes命令可以查看节点的情况,如下图所示。
其中返回值第一项表示节点id,由40个16进制字符串组成,集群模式下的节点的run_id 与单机模式下的节点run_id 有所不同,Redis每次启动都会重新创建run_id ,但是集群模式下只会在初始时创建一次,然后保存到集群配置文件中,之后节点重启会从配置文件从读取,而不再重新创建。
需要注意的是:启动节点阶段,节点之间是没有主从关系的,因此节点中不需要添加slaveof 配置。
节点握手
节点启动后是互相独立的,并不知道其他节点存在,因此集群模式中需要进行节点握手,将独立的节点组成一个网络。
节点握手使用cluster meet {ip} {port} 命令实现。
分配槽
在Redis集群中,借助槽实现数据分区,集群有16384个槽,槽是数据管理和迁移的基本单位,当数据库中的16384个槽分配了节点,集群处于上线状态(ok),如果有一个槽没有分配节点,则集群处于下线状态(fail)。
redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}
此时查看集群状态,显示所有槽分配完毕,集群进入上线状态:
指定主从关系
集群中指定关系不再使用slaveof 命令,而是使用cluster replicate run_id
例如:
redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1
至此,集群搭建完毕。
使用Ruby脚本搭建集群
在{REDIS_HOME}/src 目录下有一个redis-trib.rb 文件,这是一个Ruby脚本,可以实现集群的自动搭建。
安装Ruby环境
输入以下命令
apt-get install ruby
gem install redis
启动节点
redis-server redis-6379.conf
搭建集群
redis-trib.rb 脚本提供了众多命令,其中create 用于搭建集群:
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1
-replicas 1 :表示每个主节点有一个从节点;多个{id:port}表示节点地址,前面的做主节点,后面的做从节点。
注意:使用redis-trib.rb 脚本搭建集群时,要求节点不能包含任何槽和数据,否则会报以下错误:
执行创建命令之后,脚本会给出创建集群的计划,如下图所示,计划包括哪些节点是主节点,哪些是从节点,以及如何分配槽。
Can I set the above configuration?(type 'yes' to accept):yes
输入yes执行计划,至此,集群搭建完毕。
集群设计
设计集群方案时,需要考虑以下因素:
- 高可用要求:根据故障自动转移原理,至少需要
3 个主节点才能完成故障转移,且三个主节点应在不同的物理机上,每个主节点至少需要一个从节点,主从节点应在不同的物理机上,因此高可用集群至少需要6 个节点来支持。 - 数据量和访问量:估算应用需要的数据量和总访问量,结合每个主节点的容量和能承受的访问量(可以通过
benchmark 估算),计算所需的主节点个数。 - 节点数量限制:Redis官方给出的节点数量限制是
1000 ,主要是考虑节点间通信带来的消耗。实际应用中需要避免大量集群,如果节点数量不足以满足应用对Redis数据量和访问量的要求,可以考虑:(1)业务分割,大集群划分为多个小集群;(2)减少不必要的数据;(3)调整过期数据删除策略。 - 适度冗余:Redis可以在不影响集群服务的情况下适度增加节点,保证数据容冗余。
数据结构
节点需要专门的数据结构来存储集群的状态。所谓集群的状态,是一个很大的概念,包括:集群是否处于上线状态,集群中有哪些节点,节点的主从状态,槽指派的分布等。
节点为了存储集群状态而提供的数据结构中,最关键的是clusterNode 和clusterState 结构,前者记录集群中一个节点的状态,后者记录了集群作为一个整体的状态。
每个节点都会使用一个clusterNode 结构来记录自己的状态,并为集群中所有的节点创建一个clusterNode 结构,以此记录其他节点的状态:
struct clusterNode
{
//节点创建时间
mstime_t ctime;
//节点名称
char name[REDIS_CLUSTER_NAMELEN];
//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;
//节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等
int flags;
//配置纪元:故障转移时起作用,类似于哨兵的配置纪元
uint64_t configEpoch;
//槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsigned char slots[16384/8];
//节点中槽的数量
int numslots;
//...
};
除了上述字段,clusterNode 还包含了节点连接、主从复制、故障发现和转移需要的信息等。
clusterState
typedef struct clusterState
{
//自身节点
clusterNode *myself;
//配置纪元
uint64_t currentEpoch;
//集群状态:在线还是下线
int state;
//集群中至少包含一个槽的节点数量
int size;
//哈希表,节点名称->clusterNode节点指针
dict *nodes;
//槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL
clusterNode *slots[16384];
};
集群命令的实现
cluster meet
通过向节点A发送cluster meet 命令,客户端可以让接收命令的节点A将另一个节点B添加到节点A 所在的集群中。
CLUSTER MEET <ip> <port>
收到命令的节点A将与节点B进行握手,以此确定彼此的存在,并为将来进一步的通信打好基础。具体步骤:
- 节点A为节点B创建一个
clusterNode 结构来存储节点B的信息,并将该结构添加到自己的clusterState.nodes 字典里。 - 之后,节点A根据
CLUSTER MEET <ip> <port> 命令中指定的IP地址和端口号,向节点B发送一条MEET 消息。 - 节点B接收到节点A发送的
MEET 消息,节点B为节点A创建一个clusterNode 结构,并将该结构添加到自己的clusterState.nodes 字典里。 - 之后节点B向节点A返回一条
PONG 消息。 - 节点A将接收到节点B返回的
PONG 消息,通过这条消息,节点A可以得知节点B已经成功地接收自己的MEET 消息。 - 之后节点A将向节点B返回一条
PING 消息。 - 节点B将接收到节点A返回的
PING 消息,通过这条消息,节点B可以得知节点A已经成功接收了自己返回的PONG 消息,至此,握手完成。
cluster addslots
集群中槽的分配信息,存储在clusterNode 的slots 数组中和clusterState 的slots 数组中,两个数组之间的区别是,前者存储的该节点中分配了哪些槽,而后者存储的每个槽所指向的节点,即集群中所有槽分别分布在哪个节点。
cluster addslots 命令接收一个或多个槽作为参数,例如在A节点上执行cluster addslots {0,1989} 命令,是将编号为0-1989 的槽分配给A节点,具体执行步骤如下:
- 遍历槽,检查它们
0-1989 是否都没有分配节点,如果有一个槽已经分配,则命令执行失败;检查方法是遍历槽在clusterState.slots[] 中对应的值是否为NULL 值。 - 遍历槽,将其分配给节点A,将
clusterNode.slots[] 中对应的比特修改为1,以及clusterState.slots[] 中对应的指针指向节点A。 - 执行完毕后,通过节点通信机制通知其他节点,所有节点都会知道
0-1989 的槽分配给了节点A。
实践须知
集群伸缩
实际场景中常常需要对集群进行伸缩,如果访问量增大时,集群的扩容操作。Redis集群可以在不影响对外服务的情况下对集群进行伸缩;其核心是槽迁移:修改槽与节点之间的关系,实现槽在节点中的迁移。例如,如果槽均匀分配在三个节点中,现需要新增一个节点,则需要从3个节点中取出一部分槽分配给新的节点,从而实现槽的重新分配。
新增节点
- 启动节点
- 节点握手
- 迁移槽,使用
redis-trib.rb 的reshard(重新分区) 工具实现,reshard 自动化程度很高,只需要输入redis-trib.rb reshard ip:port 即可自动实现槽迁移。 - 指定主从关系
减少节点
- 迁移槽,使用
reshard 将需要删除的节点的槽均匀迁移到其他节点上 - 下线节点:使用
redis-trib.rb del-node 工具,先下线从节点再下线主节点。
ASK错误
当客户端向源节点发送一个与数据库有关的命令,并且命令要处理的数据库键刚好就属于正在被迁移的槽时:
- 源节点会现在自己的数据库中查找指定的键,如果找到就执行客户端发送的命令。
- 相反,如果源节点没有在数据库中找到指定的键,则这个键有可能已经被迁移到了其他节点,此时源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并且再次发送之前要执行的命令。
客户端收到ASK错误后,从中读取目标节点的地址信息,并向目标节点重新发送请求,就像收到MOVED错误时一样。但是二者有很大区别:ASK错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART客户端不会刷新slots缓存;MOVED错误重定向则是(相对)永久的,SMART客户端会刷新slots缓存。
参考文献:
《Redis设计与实现》 《Redis开发与运维》
以上。
创作不易,如果文章对你有帮助,留个三连再走吧。
如果不足或错误欢迎评论指正。
|