一、Redis特点
单线程(6.0版本后支持网络连接多线程)
操作基于内存,读写数据不需要磁盘I/O,速度快
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis的所有操作都是原子性的
Redis还支持 publish/subscribe, 通知, key 过期等等特性。
二、数据结构
1.String
适用于简单的key-value场景,如储存一条配置信息
命令 | 描述 |
---|---|
SET key value | 设置指定key的值 |
GET key | 获取指定key的值 |
GETRANGE key start end | 返回key中字符串的子字符串 |
SETEX key seconds value | 设值并添加过期时间 |
SETNX key value | 只有key不存在时设定key的值 |
STRLEN key | 返回字符串值长度 |
INCR key | 将key储存的数值增1 |
INCRBY key increment | 将key储存的数值增加指定值 |
DECR key | 将key所储存的值减1 |
DECRBY key decrement | 将key所储存的数值减少指定值 |
APPEND key value | 如果将指定值追加到原本值的末尾 |
2.Hash
Redis hash 是一个string类型的 field(字段)和value(值)的映射表,hash特别适合用于储存对象数据
key本身相当于一个哈希表名,字段和值都放在这个哈希表中。
命令 | 描述 |
---|---|
HMSET key field1 value1 [field2 value2 ] | 同时将多个字段写入到key中(此时key相当于对象名,字段相当于对象属性) |
HSET key field value | 设置指定key的指定字段的值 |
HMGET key field1 [field2] | 获取指定key所有给定字段的值 |
HGET key field | 获取指定key中指定字段的值。 |
HGETALL key | 获取在哈希表中指定 key 的所有字段和值 |
HEXISTS key field | 查看哈希表 key 中,指定的字段是否存在。 |
HDEL key field1 [field2] | 删除一个或多个哈希表字段 |
HLEN key | 获取指定key中字段的数量 |
3.List
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
key相当于一个List名
适合存储一些有序且数据相对固定的数据,
命令 | 描述 |
---|---|
RPUSH key value1 [value2] | 在列表尾部添加一个或多个值 |
LPUSH key value1 [value2] | 在列表头部添加一个或者多个值 |
LSET key index value | 通过索引设置列表指定位置的值 |
LRANGE key start stop | 获取列表指定范围内的元素 |
LLEN key | 获取列表长度 |
LPOP key | 移出并返回列表的第一个元素 |
RPOP key | 移出并返回列表的最后一个元素 |
LREM key count value | 根据count值,移除列表中与value相等的元素。 count > 0 : 从表头开始向表尾搜索,删除数量为count; count < 0 : 从表尾开始向表头搜索,删除数量为count的绝对值; count = 0 :移除表中所有与value相等的值 |
LTRIM key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
4.Set
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
set提供了诸如交集、并集、差集操作,可实现查找两个人共同的好友等功能
命令 | 描述 |
---|---|
SADD key member1 [ member2 ] | 向集合添加一个或多个成员 |
SCARD key | 获取集合的成员数 |
SDIFF key1 [ key2 ] | 返回第一个集合与第二个集合的差异 |
SISMEMBER key member | 判断 member 元素是否是集合 key 的成员 |
SMEMBERS key | 返回集合中的所有成员 |
SINTER key1 [key2] | 返回给定所有集合的交集(intersection) |
SUNION key1 [key2] | 返回所有给定集合的并集(union) |
SPOP key | 移除并返回集合中的一个随机元素 |
SREM key member1 [member2] | 移除集合中一个或多个成员 |
SMOVE source destination member | 将 member 元素从 source 集合移动到 destination 集合 |
SDIFFSTORE destination key1 [key2] | 返回给定所有集合的差集并存储在 destination 中 |
SINTERSTORE destination key1 [key2] | 返回给定所有集合的交集并存储在 destination 中 |
SUNIONSTORE destination key1 [key2] | 所有给定集合的并集存储在 destination 集合中 |
5.Sorted Set(ZSet)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
底层实现是skiplist (跳跃表)
每个member不仅对应一个指定的score,还会根据先后顺序自动对应一个从0开始的索引。-1 可以代表最后一个成员。该索引是实时更新的,于分数值有关,
它相当于一个分数排名!!!只不过这个排名是从0开始的。
命令 | 描述 |
---|---|
ZADD key score1 member1 [score2 member2] | 将一个或多个成员元素及其分数值加入到有序集当中。如果某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上,分数值可以是整数值或双精度浮点数;如果有序集合 key 不存在,则创建一个空的有序集并执行 ZADD 操作;当 key 存在但不是有序集类型时,返回一个错误。 |
ZCARD key | 获取有序集合的成员数 |
ZCOUNT key min max | 计算在有序集合中指定区间分数的成员数 |
ZINCRBY key increment member | 有序集合中对指定成员的分数加上增量 increment |
ZRANGE key startindex stopindex [WITHSCORES] [REV] | 返回指定索引区间有序集成员。默认返回成员按照分数值从小到大排列。添加withscores 后会在返回值中添加对应分数值。添加 rev会反向排列 |
ZRANGEBYLEX key min max | 返回在指定字典区间有序集成员。返回值排列顺序是按照分数值从小到大排列。min 字典中排序位置较小的成员,必须以[ 开头,或者以( 开头,可使用- 代替;max 字典中排序位置较大的成员,必须以[ 开头,或者以( 开头,可使用+ 代替 | |
ZRANGEBYSCORE key min max [WITHSCORES] | 返回指定分数区间的有序集集成员。排列顺序为分数值从小到大。min 较小的分数值,默认为大于等于该值,可以通过添加前缀( 使之变成大于 。max 较大的分数值,默认为小于等于该值,可以通过添加前缀( 使之变成小于。 |
ZRANK key member | 返回有序集合中指定成员的索引 |
ZREM key member [member ...] | 移除有序集合中的一个或多个成员 |
ZREMRANGEBYSCORE key min max | 移除有序集合中给定的分数区间的所有成员 |
三、 Redis高可用
Redis 通过复制、哨兵、集群,三种技术实现高可用:
- 基于主从复制实现数据的备份,可用于实现读写分离;
- 基于哨兵实现主从切换,主服务器不可用时执行故障转移,解决主服务器的单点问题;
- 数据量大且单类数据可能超过单 redis 服务能力时,可用集群解决,最多可组建拥有2048*8个master节点的redis 集群;
主从 master-slave
redis主从模式(master-slave,为保政治正确,已改名master-replica),提供了除持久化外另一种数据的热备功能,也为读写分离提供了途径;
redis主从模式通过复制功能实现,redis提供了SLAVEOF(REPLICAOF),让一个服务器(slave)去复制另一个服务器(master);
复制功能的开启: 通过客户端向服务器发送指令:slaveof(replicaof) host port ,或者在slave配置文件中配置 replicaof 选项;
注: slaveof(replicaof) host port 是异步命令,当服务器收到该命令后,会先将host/port保存到服务器状态(redisServer)的masterhost及masterport属性里,然后回复 OK,再开始执行真正的复制操作;
redis的复制功能的实现
包括 同步(SYNC/PSYNC) 和 命令传播(command propagate) 两个操作:
- 在刚开启主从同步功能或者断线重连时,使用同步命令让slave的数据状态跟master保持一致;
- 同步后,若master执行写命令,状态又将不一致,通过命令传播让slave执行同样的命令,使状态保持一致;
同步:
- SYNC命令:redis2.8之前使用,slave向master发送sync命令,master会执行bgsave命令生成rdb文件,然后把rdb文件发送给slave,slave通过rdb恢复数据,达到与master一致的状态;
- PSYNC命令:因为SYNC命令只能执行全量同步,对于slave断线重连后只需要执行部分重同步就可以达到一致的情况,仍使用SYNC命令有很大的浪费,因为在redis2.8中,提供了 PSYNC 命令来取代 SYNC 命令
心跳检测
在命令传播阶段,slave 会定时向 master 发送心跳信息,默认每秒一次,命令格式: REPLCONF ACK
作用:
- 检测网络通信状态;
- 辅助实现 min-slave 选项;
- 检测命令丢失: 心跳信息会带有slave的offset,master收到心跳后,可以与自己保存的offset对比,大与收到的offset,说明有命令传播失败;
步骤:
- slave 收到 SLAVEOF
命令,设置 master 的地址端口信息,然后回复OK; - 向 master 建立套接字连接;
- 发送 PING 命令;
- 身份验证,如果 slave 有 masterauth 配置;
- slave 发送自身端口信息;
- 进行同步;
- 命令传播;
注:master 和 slave 需要互为对方的客户端,因为彼此都要向对方发送命令;
哨兵 Sentinel
master-slave方案解决了数据的复制问题,但是 当 master 宕机时,slave 并不会自动切换为新的 master,以继续提供服务,于是,Sentinel System 有了用武之地;
由一个或多个 Sentinel 实例组成的 Sentinel System,可以监视任意多个 master 及其 slave,并行使以下职责:
- 监视各服务器运行状态;
- 发现异常进行通知;
- master 下线后从其 slave 中选举新 master,进行故障转移;
- 下线的 master 上线后,将其降级成新 master 的 slave;
哨兵功能的总结:
- 每个 Sentinel 可监视任意多个服务器,每个服务器也可被任意多个 Sentinel 监视;
- 多个监视同一主服务器的 Sentinel 视为一个集群,在被监视主服务器下线后,该集群将选举出一个 Sentinel Leader,由该 leader 对其进行故障转移;
故障转移:
- 选举 Sentinel leader,用于执行故障转移;
- 由 Sentinel leader 从故障主服务器的所有从服务器中选一个做新的主服务器;
- 向选出的新 master 发送 slaveof no one命令,然后一次次的发送INFO命令查看服务器的role角色,当变成 master 说明升级成功;
- 向其它slave服务器发送slaveof命令,让它们复制新的服务器;
- 若下线的主服务器上线,则发送slaveof命令,让其降级为从服务器,复制新的主服务器;
Sentinel节点感知:
- Sentinel 会向被监视服务器发起两条连接,一条命令连接,一条订阅连接;
- 建立订阅频道后,会在被监视服务器上订阅 —sentinel—:hello频道,并以 2S 一次的频率,向该频道发送消息,以向其它监视该服务器的Sentinel宣示自己的存在;
- Sentinel 通过接收订阅消息并分析,获知与自己监视同一服务器的其它 Sentinel,并在被监视主服务器下线时,与其它 Sentinel 进行选举选出leader,进行故障转移;
判断下线(主观下线、客观下线):
- Sentinel 以每秒一次的频率向其它实例(包括主、从、其它Sentinel)发送PING,并根* 据回复判断服务器是否在线,当一个实例在指定的次数中不能返回有效回复时,会将这个服务器判断为 主观下线
- 判断一个主服务器进入主观下线后,向同样监视这个主服务器的其它Sentinel询问,看是否同意这个主服务器进入主观下线状态;
- 当足够多的 Sentinel 判断主服务器进入主观下线后,将这个主服务器判断为 客观下线
发现主服务器进入客观下线状态后,发起一次故障转移操作;
选举哨兵头领 Sentinel Leader:
- 所有节点都会广播一条设置信息,要求所有节点调自己为局部leader;
- 收到广播的节点,若尚未设置自己的局部leader,则按广播设置其为自己的局部leader,并回复OK,否则回复失败;
- 广播的节点按收到的回复,比对是否同一纪元,统计回复OK的数量,若超过半数,则成为全局Leader;
- 若一纪元没有一个节点获得半数以上,则休眠一个随机时间,纪元加一,再次选举,直到选出全局leader;
选出新的主服务器:
- 排除下线或者断线的从服务器;
- 排除最近5秒没有回复过 leader Sentinel 的 INFO 命令的从服务器;
- 排除与已下线服务器连接断开超过 down-after-millinseconds * 10的从服务器;
- 根据从服务器优先级,对剩余从服务器排序,选出优先级最高的;
- 若优先级最高的有多个,选出其中复制偏移量最大的;
- 若优先级最高的、偏移量最大的,仍有多个,则按 runid ,选最小的;
集群 cluster
通过主从与哨兵,redis即可实现高可用,然额,仍然存在一个问题,单台服务器的内存是有限的,不够用怎么办?redis有缓存淘汰机制,可以解决一部分问题,但业务需求是无限的,当不能过期与淘汰的数据大到一台主机不够用时,怎么办呢?SO,跟所有其它分布式系统一样,还需要横向扩展能力,幸好,自redis3.0开始,开始提供集群功能;
启动集群: 当redis 服务器以集群模式启动时,即成为一个 节点 ,默认运行在一个只包含自己的集群中,使用cluster meet
- A 为B 创建一个clusterNode结构,并添加到自己的clusterState.nodes字典中;
- A 根据 cluster meet 命令指定的地址和端口,向节点B发送meet消息;
- B 节点收到 A 的消息,为节点A创建一个clusterNode结构,并添加到自己的clusterState.nodes结构中,然后向 A 回复一条 PONG 消息;
- A 收到 PONG消息可知 B 已收到自己的握手消息,再次发一条确认消息 PING;
- B 收到 A 发送的 PING消息,握手完成;
- 完成握手后,A 会通过Gossip协议向集群内的其它节点发送节点B的消息,其它服务器也会与B进行握手,最终,集群内所有服务器都将感知其它服务器,保存有其它服务的clusterNode结构,并与之建立通信;
槽指派: redis 集群内部,通过分片的方式来保存键值数据,每个分片称之为一个槽(slot),共有0-16383共计16384个槽(2048 * 8); 集群建立后,处于未上线状态,需要进行槽指派后,才上线并开始提供服务;
集群命令执行: 集群中因为数据分片存储,执行命令的过程稍微有一点差异:收到命令后,先对key进行hash映射取得该Key的槽号,然后判断该槽是否归自己处理,若是则执行命令,否则取得负责该槽的节点,返回一个MOVED错误,并把该节点信息返回,引导客户端向正确的节点请求服务;
- 该槽归自己处理,进行处理;
- 该槽不归自己处理,返回一个MOVED错误,并把负责处理该槽的节点信息返回,引导客户端向正确的节点请求服务;
该槽数据目前正在迁移:
- 首先在自己的库中查找该键,若存在,处理;
- 若不存在,返回ASK错误,并给出新节点信息,引导客户端向新节点请求服务;客户端需要先向新节点发送asking命令,再发送正式的命令,否则将会收到一个moved错误
注:正在被导入的槽和数据,不算归自己管,只有导入完成后,才会集群内广播
关于集群的内部结构:
- clusterNode: 表示一个节点
- clusterLink: 表示一个节点的连接信息
clusterState: 每个节点都保存着一个clusterState,记录了以当前节点为视角,集群目前所处的状态,如是否在线、包含多少节点、当前配置纪元等等;其中两个属性记录了所有槽指派信息:
- clusterState.slots: char数组,长度2048(16384/8),用每一个二进制位表示一个槽是否归当前节点处理,为0表示不归我管;
- clusterState.numslots:当前节点负责处理的槽的数量,即slots数组中位的值为1的数量;
- clusterState.nodes:clusterNode结构数组,长度16384
为什么是16384个槽 :由记录槽指派信息的结构可知,其实是2048,redis节点维护一个长度2048的char数组,用数组元素中的每一位表示一个槽,而char的长度为8位,于是共可以存储2048*8=16384个槽;