本文详细讲解了关于Redis的知识点。
概述
什么是Redis
Redis是基于C语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
主要特性
整理了Redis的7种特性,有:
速度快
:
处理速度非常快,每秒能执行约11万集合,每秒约81000+记录。速度快的原因是:
- 基于C语言实现,效率高;
数据存储在内存中,读取速度快
;(主要)- 单线程模型,避免线程上下文切换和竞态消耗;
- 使用了
多路I/O复用模型
;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。多路 I/O 复用模型
是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
注意:
单线程:
Redis内部使用文件事件处理器FileEventHandler,这个处理器是单线程的,所以Redis也被叫做单线程的模型
。它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来进行处理。非单线程:
Redis内部也有许多多线程操作,如fysnc file descriptor和close file descritor操作时会有独立的线程来操作。
持久化使用RDB时,手动使用bgsave命令触发时,会调用系统的fork函数创建子进程后台处理。即 Redis 使用操作系统的fork多进程 COW(Copy On Write) 机制来实现快照持久化。
持久化AOF时的瘦身操作,fork子线程进行命令合并;
可持久化
:
支持持久化,即使机器宕机或断电也不会丢失数据。
因为数据保存在内存中,对数据的更新将会异步的保存到磁盘上。主要有三种持久化方式:
- 快照:一种半持久模式,不时的将数据集以异步的方式从内存以RDB格式写入硬盘;
- AOF可追加文件:将数据集的修改操作追加记录;
- 快照和AOF混合使用:
多种数据结构
:
5种常见的数据结构:字符串(String)、散列哈希(Hash)、列表(List)、集合(Set)和有序集合(Sort Set)。
其他的还有位图(BitMaps)、HyperLogLog(超小内存的唯一值计数)、GEO(地理信息定位)。功能丰富
:
支持发布订阅、Lua脚本(原子性的操作)、事务、pipeline管道操作;主从复制
:
支持主从同步,确保Master和Slave之间的数据同步。可以将数据复制到任意数量的从服务器,而从服务器也是可以关联其他从服务器的主服务器。
由于完全实现了发布订阅机制,使得从Slave在任何地方同步数据时,就可以订阅一个频道并接收Master完整的发布记录。
高可用、分布式、集群模式
:
支持集群模式,Sentinel哨兵机制支持高可用。支持多种编程语言,使用简单
:
如Java、Python等热门语言,都提供了API可以使用。
应用场景
缓存系统
如JetCache中Local使用Caffeine,Remote使用Redis。
为什么用Redis做缓存?
主要是基于高性能
和高并发
两个方面考虑,才使用缓存。
缓存分为本地缓存和远程缓存。本地缓存
的特点是轻量且快速,生命周期随着JVM的销毁而结束,如果多实例或多机器的情况下,每个实例或机器都需要保存一份本地缓存,浪费空间而且不具备一致性。远程缓存
也可称为分布式缓存,如Redis或MemCached等,使用远程缓存多个实例或机器可共用一份缓存,能够保证一致性,但需要Redis自身保持高可用。
而Redis是单线程的,基于内存的数据库,支持持久化和高可用,数据结构丰富,多用于缓存系统。
Redis与MemCached的区别
- Redis支持丰富的数据结构,而MemCached支持简单的String类型(新增了二进制类型);
- Redis支持数据持久化,而MemCached是数据全部存在内存中;
- Redis支持集群模式,而MemCached没有原生集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis使用单线程的IO多路复用模型。MemCached是多线程的,非阻塞IO复用的网络模型。
消息队列系统
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
还可以基于List结构的消息队列:lpush + brpop = message queue 阻塞式先进先出
计数器
increment方法实现。
- 1、计数器:记录用户个人主页的访问量
可以使用1
2
3
4
5
6incr userId:pageView
````
单线程无竞争的,来记录每个用户每个页面的访问量
- 2、计数器:记录网站每个用户某页的访问量
```redis
hincrby user pageView count
排行榜功能
有序集合Zset里面的元素是唯一的,有序的,按分数从小到大排序。
如:
1 | zadd key score1 element1 score2 element2 ... |
score可以为:时间戳、销量、关注人数等
社交网络
如社交网络应用中的点赞数、粉丝数、关注数等。
可以将点赞用户存在set集合中,scard获取其大小。
实时系统过滤器
实现过滤功能,如布隆过滤器。
安装部署
四种安装方式
具体安装部署方式可参考官网或其他文章,比较简单。
- 单机模式:
- 主从模式:
- Sentinel哨兵模式;
- Cluster集群模式:
可执行文件
基于Redis 5.0.5版本:
- redis-server 服务器
- redis-cli 命令行客户端,连接服务端
- redis-benchmark 基准和性能测试
- redis-check-aof AOF文件修复工具
- redis-check-rdb RDB文件修复工具
- redis-sentinel 启动哨兵节点
启动
三种启动方式
简单启动:
直接执行redis-server (默认ip为127.0.0.1/localhost,port为6379)动态参数启动:
1
redis-server --port 6380
配置文件启动(推荐):
1
redis-server redis.conf
验证
1 | ps -ef | grep redis 查看pid进程; |
开机启动
1 | systemctl enable redis.service |
配置参数
线上修改配置
如果已经启动服务,修改配置需重启,影响线上服务;
可以使用客户端命令修改(不会修改配置文件,临时生效,重启后恢复原样)。
如下案例:
1 | 127.0.0.1:6379> CONFIG GET appendonly |
配置文件详解
基于Redis 5.0.5版本:
网络 Network
1 | ################################## NETWORK ##################################### |
通用配置 Gennral
1 | ################################# GENERAL ##################################### |
快照 SnapShotting
1 | ################################ SNAPSHOTTING ################################ |
主从复制 Replication
1 | ################################# REPLICATION ################################# |
安全 Security
1 | ################################## SECURITY ################################### |
客户端 Clients
1 | ################################### CLIENTS #################################### |
内存管理 Memory Management
1 | ############################## MEMORY MANAGEMENT ################################ |
惰性/异步删除 LazyFree
1 | ############################# LAZY FREEING #################################### |
AOF持久化 Append Only Mode
1 | ############################## APPEND ONLY MODE ############################### |
Lua脚本 LUA SCRIPTING
1 | ################################ LUA SCRIPTING ############################### |
集群 Redis Cluster
1 | ################################ REDIS CLUSTER ############################### |
慢查询 SlowLog
1 | ################################## SLOW LOG ################################### |
延迟监控 Latency Monitor
1 | ################################ LATENCY MONITOR ############################## |
订阅通知 Event Notification
1 | ############################# EVENT NOTIFICATION ############################## |
高级配置 Advance Config
1 | ############################# EVENT NOTIFICATION ############################## |
API相关
通用命令
除了Keys命令的时间复杂度为O(n)
,其他都为O(1)
。
keys pattern
列出符合pattern的key
因为它是一个O(n)的操作且是单线程操作会阻塞其他命令。一般不在生产环境使用。建议使用方案:
- 可以在热本从节点时,在从节点执行比较重的命令;
- 用
scan命令
:以非阻塞的方式实现key值的查找,绝大多数情况下是可以替代keys命令的,可选性更强
SCAN cursor [MATCH pattern] [COUNT count]
是一个基于游标的迭代器,count默认为10。
这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程。scan 参数提供了三个参数:
- 第一个是 cursor 整数值;
- 第二个是 key 的正则模式;
- 第三个是遍历的 limit hint。
第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。
dbsize
返回当前数据库的 key 的数量
可以线上使用,因为redis有计数器会实时记录key的总数,时间复杂度为O(1)exists key
是否存在key,存在1 不存在0del key...
删除key,可以多个expire key seconds
过期时间设置,key在n秒后过期ttl key
查看key剩余的过期时间,返回负数-2说明key已经被删除persist key
去掉key的过期时间,这时在执行ttl会返回-1,说明key存在,并没有过期时间type key
返回 key 所储存的值的类型
一般为string、hash、list、set、zset、none(key不存在)select db
选择db,共16个db,默认0MOVE key db
将当前数据库的 key 移动到给定的数据库 db 当中。RENAME key newkey
修改 key 的名称RENAMENX key newkey
仅当 newkey 不存在时,将 key 改名为 newkey 。
数据结构
5种常见类型
String:字符串类型
结构:key value。
Value的类型可以为字符、数值型、二进制、Json串。
注意:字符串类型的Value的大小不能大于512M;
内部编码
int
:
8个字节的长整型。
如果一个字符串的内容可以转换为long,那么该字符串就会被转换成long类型,对象的ptr就会指向该long,并且对象类型也用int类型表示;
embstr
:
小于等于39个字节的字符串。
如果字符串对象的长度小于39字节,就用embstr对象,否则使用传统的raw对象。
raw
:
大于39个字节的字符串。
常见命令
get key
O(1)操作del key
O(1)操作set操作:O(1)操作
set key value
不管key是否存在都设置setnx key value
当key不存在才设置set key value XX
key存在才设置SETEX key seconds value
将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。如果 key 已经存在, SETEX命令将覆写旧值。PSETEX key milliseconds value
这个命令和 SETEX命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX命令那样,以秒为单位。set key value [expiration EX seconds|PX milliseconds] [NX|XX]
- EX seconds : 将键的过期时间设置为 seconds 秒。
执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value。 - PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。
执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。 - NX : 只在键不存在时, 才对键进行设置操作。
执行 SET key value NX 的效果等同于执行 SETNX key value - XX : 只在键已经存在时, 才对键进行设置操作。
- EX seconds : 将键的过期时间设置为 seconds 秒。
批量操作:O(n)操作
mget key1 key2 ...
批量获取key,原子操作。n次get = n次网络时间 + n次命令时间
(一般是网络时间比较耗时)
mset key1 v1 key2 v2 ...
批量设置key。
1次mget = 1次网络时间 + n次命令时间
MSETNX key value [key value ...]
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
即使只有一个给定 key 已存在, MSETNX也会拒绝执行所有给定 key 的设置操作。
- O(N)操作, N 为要设置的 key 的数量。
- 当所有 key 都成功设置,返回 1 。
如果所有给定 key 都设置失败(至少有一个 key 已经存在),那么返回 0 。
自增/减命令
incr key
O(1)操作。计数,key自增1,如key不存在,自增后get(key)=1decr key
O(1)操作。key自减1,如key不存在,自减后get(key)=-1incrby key k
O(1)操作。key自增k,如key不存在,自增后get(key)=kdecr key k
O(1)操作。key自减k,如key不存在,自减后get(key)=-k其他操作:
getset key newValue
O(1)操作,设置key的新值,返回旧值。append key value
O(1)操作,将value追加到旧的value。strlen key
O(1)操作,返回字符串的长度。incrbyfloat key 3.5
O(1)操作,增加key对应的值为3.5getrange key start end
O(1)操作,获取字符串下指定下标的值setrange key index value
O(1)操作,设置指定下标所对应的值。
应用
1、计数器:记录用户个人主页的访问量
可以使用
incr userId:pageView
(单线程无竞争的),来记录每个用户每个页面的访问量
Hash:哈希类型
结构:key field value。
即类似于Map
内部编码
ziplist压缩链表
:
当元素个数小于512 , 并且值的大小小于64个字节时 , 采用ziplist。- 当哈希类型中元素个数小于 hash-max-ziplist-entries 配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,
Redis 会使用 ziplist 作为哈希的内部实现。 ziplist
是一种压缩链表,它的好处是更能节省内存空间。
因为它所存储的内容都是在连续的内存区域当中的。
当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。
但当数据量过大时就ziplist就不是那么好用了。
因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。- ziplist最大的优势就是存储的时候是连续的内存,可以极大的提升cpu的缓存命中率。
- 当哈希类型中元素个数小于 hash-max-ziplist-entries 配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,
hashtable哈希表
:
当元素个数小于512 , 并且值的大小小于64个字节时 , 采用ziplist , 大于的时候采用hashtable。
常见命令
常用命令:O(1)
1
2
3
4
5
6
7
8
9hget key field 获取hashkey对应的field的value
hset key field value
hdel key field
hexists key field
hlen key 获取hash key的field的数量
hsetnx key field value 不存在设置,否则失败
hincrby key field intCounter
hincrbyfloat key field floatCounter批量操作:O(n)
1
2hmget key1 field1 field2...
hmset key field1 value1 field2 value2...其他:O(n)
1
2
3hgetall key 返回key下所有的field和value。由于单线程,要小心使用此命令,尽量用hmget代替
hkeys key 返回key对应的所有field的field
hvals key 返回key对应的所有field的value
应用
- 1、计数器:记录网站每个用户某页的访问量
1
hincrby user:info pageView count
- 2、缓存:缓存视频的基本信息
List:列表类型
结构:key elements。
有序的(插入顺序)、可重复的、可以左右两边插入弹出的。
内部编码
linklist双向链表
:linkedlist是一种双向链表。
它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。
当每增加一个node的时候,就需要重新malloc一块内存。ziplist 压缩列表
:
常见命令
增:
1
2
3
4
5
6
7
8rpush key value1 value2 ...
O(1~n),从列表的右端插入
lpush key value1 value2 ...
O(1~n),从列表的左端插入
linsert key before|after value newValue
O(n),在list指定的value之前或后插入newValue删:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17lpop key
O(1),从左弹出list中的一个元素
rpop key
O(1),从右弹出list中的一个元素
lrem key count value
O(n),根据count值,从list中删除所有等于value的元素
count > 0:从左到右,删除最多count个。
count < 0:从右到左,删除最多Math.abs(count)个。
count = 0:删除所有满足条件的元素。
ltrim key start end
O(n),按照索引位置修剪list。保留范围內的元素
blpop key timeout 和brpop key timeout
O(1),阻塞删除,timeout是阻塞超时时间,为0表示永远不阻塞改:
1
2lset key index newValue
O(n),修改指定位置的值为newValue查:
1
2
3
4
5
6
7
8
9
10
11lrange key start end (包含end)
O(n),获取列表指定索引范围的元素。
如:list有6个元素。
索引从左:0~5;
索引从右:-1~-6;
lindex key index
O(n),获取列表指定索引的元素
llen key
O(1),获取list长度
应用
1、微博时间轴TimeLine:将关注用户的微博由新到旧排列。
关注的人更新微博,使用lpush左侧即头部插入;
使用lrang可以分页查询;2、实现栈:lpush + lpop = stack 先进后出
3、实现队列:lpush + rpop = queue 先进先出
4、实现有固定数量的列表:lpush + ltrim = capped collection
5、消息队列:lpush + brpop = message queue 阻塞式先进先出
Set:集合类型
结构:key values。
元素是无序的、无重复元素、支持集合间的操作,如交/并/差集,
内部编码
intset
:
intset是一个整数集合,里面存的为某种同一类型的整数。1
2
3#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))当集合中的元素都是整数,并且集合中的元素个数小于 512 个时,Redis 会选用 intset 作为底层内部实现。
intset是一个有序集合,查找元素的复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。
比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,
那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。但是intset不支持降级操作。hashtable 哈希表
:
常见命令
- 集合内操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21sadd element
O(1),添加元素,如存在则失败
srem key element
O(1) 删除
scard key
计算集合大小
sismember key value
判断vlaue是否在key中
srandmember key count
随机挑count个元素。不会破坏集合的数据
spop key
从集合中随机弹出一个元素
smembers key
获取集合所有元素,返回结果无序
会造成阻塞,需注意使用,建议使用游标scan - 集合间操作:
1
2
3
4
5sdiff key1 key2 求差集
sinter key1 key2 求交集
sunion key1 key2 求并集
sdiff/sinter/suion + store destkey
将差/交/并集结果保存到destkey中
应用
- 1、微博抽奖系统:使用spop或srandmember随机选择一个或多个用户
- 2、微博点赞、转发等:将点赞用户存在集合中,scard获取其大小
- 3、标签:给用户添加标签/给标签添加用户
- 4、共同关注/共同好友等功能:求交集
ZSet:有序集合类型
结构:key score value。
无重复元素、有序的、有元素+分值构成。
时间复杂度比集合类型有所增大。
内部编码
ziplist 压缩列表
:skiplist 跳表
:它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。
但它的实现比较简单,可以作为平衡树的替代品。
可参考:
随机数据结构:跳表(SkipList)
常见命令
基本操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17zadd key score1 element1 score2 element2 ...
O(logN),添加score和element
zrem key element
O(1) 删除
zscore key element
O(1) 返回元素score
zincrby key incrScore element
O(1),增加或减少元素的score
zcard key
O(1) 返回key中元素个数
zrank key element
获取某元素的排名(升序 从小到大)范围操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20zrange key start end [withscores]
返回指定索引范围的升序元素,是否打印分数可选
复杂度为O(log(n) + m) :
n指有序集合中元素的个数;
m指获取范围内的元素个数
zrangebyscore key minScore maxCore [withscores]
O(log(n) + m) ,指定分数范围,其余和上面zrange一样
zcount key minScore maxScore
O(log(n) + m) ,指定分数范围的个数
zremrangebyrank key start end
O(log(n) + m) ,删除指定排名內的升序元素
zremrangebyscore key start end
zremrangebyscore key minScore maxScore
zrevrank/zrevrange/zrevrangebyscore
排名从高到低集合操作:
1
2zinterstore/zunionstore
集合间操作,交集/并集
应用
1、排行榜实现:zadd key score1 element1 score2 element2 ...
score可以为:时间戳、销量、关注人数等
高级功能
慢查询 SlowLog
概念
慢查询顾名思义是将redis执行命令较慢的命令记录到慢查询队列中。慢查询是一个先进先出的队列,且队列是固定长度的,保存在内存中的
。
生命周期
Redis命令执行的完整生命周期:
1 | client发送命令 -> Redis队列命令排队(单线程) -> 执行命令 -> 返回结果到client |
慢查询发生在第3个阶段(执行命令);
客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素。
两个配置和三个命令
两个配置
slowlog-log-slower-than 10000
sloglog是用来记录redis运行中执行比较慢的命令耗时。
当命令的执行超过了指定时间,就记录在slowlog中(单位是微秒,所以1000000就是1秒),slowlog保存在内存中,所以没有IO操作。注意:负数时间会禁用慢查询日志,而0则会强制记录所有命令。
slowlog-max-len 128
慢查询队列长度(记录多少条慢查询,默认128)
一个新的命令满足慢查询条件时被插入到这个列表中。
当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出。
三个命令
slowlog get [n]
获取慢查询日志,参数n可以指定条数
返回结果有6个部分组成:
1、慢查询日志的唯一ID
2、发生的时间戳
3、命令耗时,单位微秒
4、执行的命令和参数
5、客户端网络套接字(ip: port)
6、“”slowlog len
查询当前慢查询记录数slowlog reset
重置慢查询日志 (实际是对列表做清理操作)
运维经验
slowlog-max-len不要设置太小,通常1000左右
。
线上建议调大。因为记录慢查询时Redis会对长命令做阶段操作,并不会占用大量内存,增大慢查询列表可以减缓慢查询被剔除的可能。slowlog-log-slower-than不要设置太大,通常1000微秒(即1ms),根据时间QPS设置
。
默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间超过1毫秒以上,那么Redis最多可支撑QPS不到1000因此对于高QPS场景下的Redis建议设置为1毫秒。
如:qps为10000的话,平均每个时间就为0.1ms,如超过1ms就会对qps造成影响,这样调小阈值慢查询才会被记录下来。注意Redis命令的生命周期。
慢查询只记录命令的执行时间,并不包括命令排队和网络传输时间。
因此客户端执行命令的时间会大于命令的实际执行时间,因为命令执行排队机制,慢查询会导致其他命令级联阻塞。
因此客户端出现请求超时时,需要检查该时间点是否有对应的慢查询,从而分析是否为慢查询导致的命令级联阻塞。定期持久化慢查询
。
由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多,队列满的情况下,可能会丢失部分慢查询命令。
为了防止这种情况发生,可以定期执行slowlog get命令将慢查询日志持久化到其他存储中(例如:MySQL、ElasticSearch等),然后可以通过可视化工具进行查询。
管道 Pipeline
pipeline,即流水线。
1次pipeline(N条命令)= 1次网络时间 + N次命令时间。
对于多个命令执行,不再同步等待每个命令的返回结果。我们会在一个统一的时间点来获取结果。
1 | public static void main(String[] args) { |
优点
- 提高redis的读写能力。
Redis其实是一个基于TCP协议的CS架构的内存数据库,所有的操作都是一个request一个response的同步操作。
redis每接收到一个命令就会处理一个命令,并同步返回结果。
这样带来的问题就是,一个命令就会产生一次RTT(Round Time Trip,往返时间),这样的话必然会消耗大量的网络IO。
redis客户端执行一条命令分4个过程:
发送命令-〉命令排队-〉命令执行-〉返回结果
。
这个过程称为RTT
。
mget和mset批量操作,有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题
需注意
- Redis命令时间是微秒级别的,无瓶颈,也就是pipeline解决了Redis网络的瓶颈。
- pipeline中每条命令要注意网络消耗
- 使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。
- pipeline每次只能作用在一个Redis节点;
与M操作(批处理)对比:
原生批命令操作是原子的
(一批命令 要么成功要么失败)。pipeline是非原子的
,会将其中命令进行拆分的,但返回的结果是顺序的。- 原生批命令一命令多个key, 但pipeline支持多命令(存在事务),非原子性;
- 原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成。
发布订阅 Pub/Sub
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
角色
- 发布者:Publisher
基于客户端实现。注意:
无法做消息堆积,即获取历史信息
。
如Pub发布了一条消息到Channel,新的Sub去订阅该Channel,是收不到之前的消息的。
订阅者:Subscribe
基于客户端实现,可订阅多个频道。频道:Channel
基于Server段实现。
API
1 | publish channel message |
对其他消息队列发布订阅的对比:
- 其他MQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;
- 其他MQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
- 其他MQ支持多种消息协议。
位图 BitMap
概念
8bit = 1b = 0.001kb
,
bitmap就是通过最小的单位 bit来进行0或者1的设置,表示某个元素对应的值或者状态。
一个bit的值,只能是0或1;也就是说一个bit能存储的最多信息是2。
位数组是自动扩展的,如设置在某个offset超出来现有范围,就会自动将位数组进行0扩充。
位图并不是一种特殊的数据结构,其实本质上是二进制字符串,也就是byte数组。
可以使用普通的 get/set 直接获取和设置整个位图的内容;
也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
Bitmaps 并不是实际的数据类型,而是定义在String类型上的一个面向字节操作的集合。
因为字符串是二进制安全的块,他们的最大长度是512M,最适合设置成2^32个不同字节。
优势
- 基于最小的单位bit进行存储,所以非常省空间。
- 设置时候时间复杂度O(1)、读取时候时间复杂度O(n),操作是非常快的
- 二进制数据的存储,进行相关计算的时候非常快
- byte二进制数组方便扩容
限制
redis中BitMap被限制在512MB之内,所以最大是2^32位。
建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。
常用命令API
getbit key offset
获取位图指定索引的值。
长度超过补为0。1
2
3
4
5
6
7
8
9
10
11
12
13127.0.0.1:6379> set hello redis
OK
127.0.0.1:6379>
127.0.0.1:6379> getbit hello 0
(integer) 0
127.0.0.1:6379> getbit hello 1
(integer) 1
127.0.0.1:6379> getbit hello 2
(integer) 1
127.0.0.1:6379> getbit hello 3
(integer) 1
127.0.0.1:6379> getbit hello 4
(integer) 0setbit key offset value
给位图指定索引设置值,返回该索引位置的原始值1
2
3
4
5
6
7
8
9127.0.0.1:6379> get hello
"redis"
127.0.0.1:6379> getbit hello 3
(integer) 1
127.0.0.1:6379> setbit hello 3 0
(integer) 1
127.0.0.1:6379> get hello
"bedis"
127.0.0.1:6379>bitcount key [start end]
获取位图指定范围(start到end,单位为字节,如果不指定就是获取全部)位值为1的个数。1
2
3
4127.0.0.1:6379> get hello
"bedis"
127.0.0.1:6379> bitcount hello
(integer) 19bitpos key targetBit [start] [end]
计算位图指定范围(start到end,单位为字节,如果不指定就是获取全部)第一个偏移量对应的值等于targetBit的位置。
查找指定范围内出现的第一个0或1。bitop and|or|not|xor destkey key [key...]
做多个bitmap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存到destkey中。1
2
3
4
5
6
7
8127.0.0.1:6379> set hello big
OK
127.0.0.1:6379> set world big
OK
127.0.0.1:6379> bitop and destkey hello world
(integer) 3
127.0.0.1:6379> get destkey
"big"bitfield命令
已支持的命令列表:1
2
3
4
5- 支持的命令:GET <type> <offset> – 返回指定的位域
- SET <type> <offset> <value> – 设置指定位域的值并返回它的原值
- INCRBY <type> <offset> <increment> – 自增或自减(如果increment为负数)指定位域的值并返回它的新值
还有一个命令通过设置溢出行为来改变调用INCRBY指令的后序操作:
- OVERFLOW [WRAP|SAT|FAIL]示例:
当需要一个整型时,有符号整型需在位数前加i,无符号在位数前加u。1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> get w
"hello"
127.0.0.1:6379>
127.0.0.1:6379> BITFIELD w get u4 0
1) (integer) 6
127.0.0.1:6379> BITFIELD w get u3 2
1) (integer) 5
127.0.0.1:6379> BITFIELD w get i4 0
1) (integer) 6
127.0.0.1:6379> BITFIELD w get i3 2
1) (integer) -3setbit和getbit指定的值都是单个位的,如果指定多个位,就需要pipeline来处理。但使用bitfield可以对指定位片段进行读写,但最多只能处理64个连续但位。
如超过需使用多个子指令,bitfield可以一次执行多个子指令。1
2
3
4
5
6一次执行多个子指令:
127.0.0.1:6379> BITFIELD w get u4 0 get u3 2 get i4 0 get i3 2
1) (integer) 6
2) (integer) 5
3) (integer) 6
4) (integer) -3incrby,它用来对指定范围的位进行自增操作。
既然提到自增,就有可能出现溢出。
如果增加了正数,会出现上溢,如果增加的是负数,就会出现下溢出。
Redis 默认的处理是折返。如果出现了溢出,就将溢出的符号位丢掉。
如果是 8 位无符号数 255, 加 1 后就会溢出,会全部变零。如果是 8 位有符号数 127,加 1 后就会溢出变成 -128。溢出策略:
bitfield 指令提供了溢出策略子指令 overflow,用户可以选择溢出行为。
默认是折返 (wrap),还可以选择失败 (fail) 报错不执行,以及饱和截断 (sat),超过了范围就停留在最大 最小值。
overflow 指令只影响接下来的第一条指令,这条指令执行完后溢出策略会变成默认 值折返 (wrap)。
应用
- 1、
UV 独立用户统计
:
使用set和Bitmap(前提是用户的ID必须是整型)
场景一:总共1亿用户,五千万独立用户。使用Set:
- 每个userId占用空间:32位(假设userId用的是integer) ;
- 需要存储的用户量:50,000,000;
- 内存使用总量:32位 * 50,000,000=200MB
使用BItMap: - 每个userId占用空间:1位;
- 需要存储的用户量:100,000,000;
- 内存使用总量:1位 * 100,000,000=12.5MB
场景二:总共1亿用户,若只有10万独立用户
使用Set
- 每个userId占用空间:32位(假设userId用的是integer) ;
- 需要存储的用户量:100,000;
- 内存使用总量:32位 * 100,000=4MB
使用BItMap:- 每个userId占用空间:1位;
- 需要存储的用户量:100,000,000;
- 内存使用总量:1位 * 100,000,000=12.5MB
2、用户签到数据记录:
1
setbit key offset value
key主要由用户id组成,设定一个初始时间,每加一天即对应用户的offset的加1。value=1为已签到
取数据时,只需要计算时间段差的天数,然后1
bitcount key [start end]
3、用户在线状态:判断某用户是否在线?
1
setbit key offset value
只需要一个key,然后用户id为偏移量offset,如果在线就设置为1,不在线就设置为0。
3亿用户只需要大约36.6MB的空间:1位 * 3亿=3亿位/8/1000/1024=36.6M4、统计活跃用户:
setbit key offset value 使用时间作为key,用户id为offset,如果当日活跃过就设置为1。
通过bitop and|or|not|xor destkey key [key...]进行二进制计算,就可以算出在某段时间内用户的活跃情况。
注意
- string类型最大长度为512M。
- 注意setbit时的偏移量,当偏移量很大时,可能会有较大耗时。
- 位图不是绝对的好,有时可能更浪费空间。(如UV统计时的第二种情况)。
HyperLogLog 基数统计
概念
HyperLogLog 是用来做基数统计的算法
。
基数统计即统计一个数据集中不重复元素的个数。
提供不精确的去重计数方案,标准误差(即均方根误差,RMSE,Root mean squared error)为0.81%
API命令
1 | PFADD key element [element ...] |
优缺点
优点:
- 在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
- 每个HyperLogLog 键只需要花费12KB内存,就可以计算接近 2^64 个不同元素的基数。
这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
缺点:
- HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
- HLL这个数据结构需要占据一定12k的存储空间,不适合统计单个用户相关的数据。
为什么占用12K?
因为Redis的HLL实现中用到了16384(2^14)个桶,每个桶的maxBits需要6个bits来存储,最大可以表示为maxBits=63。
于是总内存为:16384 * 6 / 8 / 1024 = 12k字节
优化
Redis对HLL做了优化:
- 在计数比较小时,存储空间采用稀疏矩阵存储,占用空间小;
- 计数变大时,稀疏矩阵占用空间超过阈值时,会一次性转变为稠密矩阵,才会占用12K的空间。
稀疏矩阵
:
在矩阵中,若数值为0的元素数目远远多于非0元素的数目时,则称该矩阵为稀疏矩阵。只存储在矩阵中极少数的非零元素,为此必须对每一个非零元素,保存它的下标和值。
可以采用一个三元Trituple<row,column,value>来唯一地确定一个矩阵元素
。
因此,稀疏矩阵需要使用一个三元组数组(亦称为三元组表)来表示。
稠密矩阵
:
与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。
应用
- 1、用户的UV独立访问统计
1
2
3
4
5
6
7
8
9
10
11
12
13127.0.0.1:6379> PFADD uv 1 2 3 4 3 4 2 1
(integer) 1
127.0.0.1:6379> PFCOUNT uv
(integer) 4
127.0.0.1:6379> PFADD uv_2 7 8 9 8 7 9
(integer) 1
127.0.0.1:6379> PFCOUNT uv_2
(integer) 3
127.0.0.1:6379> PFMERGE result uv uv_2
OK
127.0.0.1:6379> PFCOUNT result
(integer) 7
127.0.0.1:6379>
GEO 地理位置信息
GEO 地理信息定位:存储经纬度、计算两地距离、范围计算等。
原理
业界比较通用的地理位置距离排序算法是 GeoHash 算法
GeoHash算法
:
GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。
当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行 了。映射算法实现
:- 1、它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘,所有的地图元素坐标都将放置于唯一的方格中。方格越小,坐标越精确。
- 2、然后对这些方格进行整数编码,越是靠近的方格编码越是接近。
最简单的方案就是
切蛋糕法
:
设想一个正方形的蛋糕摆在你面前,二刀下去均分 分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数予以表示。然后继续切去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。 - 3、编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。
- 4、GeoHash算法会继续对这个整数做一次 base32编码 (0-9,a-z去掉 a,i,l,o 四个字母) 变成一个字符串。
在Redis里面,经纬度使用52位的整数进行编码,放进了zset里面,zset的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。
- 5、在使用 Redis 进行 Geo 查询时,它的内部结构实际上只是一个zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 ,通过将 score 还原成坐标值就可以得到元素的原始坐标。
API命令
geoadd key longitude latitude member [longitude latitude member...]
将给定的空间元素(纬度、经度、名字)添加到指定的键里面。这些数据会以有序集合的形式被储存在键里面,
从而使得georadius和georadiusbymember这样的命令可以在之后通过位置查询取得这些元素。
geoadd命令以标准的x,y格式接受参数,所以用户必须先输入经度,然后再输入纬度。
geoadd能够记录的坐标是有限的:非常接近两极的区域无法被索引的精确的坐标限制由EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义, 具体如下- 有效的经度介于-180-180度之间
- 有效的纬度介于-85.05112878 度至 85.05112878 度之间。
当用户尝试输入一个超出范围的经度或者纬度时,geoadd命令将返回一个错误。
1 | 127.0.0.1:6379> GEOADD cities:locations 116.28 39.55 beijing |
geopos key member [member...]
从键里面返回所有给定位置元素的位置(经度和纬度)geopos命令返回一个数组。
数组中的每个项都由两个元素组成:
第一个元素为给定位置元素的经度,
而第二个元素则为给定位置元素的纬度。
当给定的位置元素不存在时,对应的数组项为空值.
1 | 127.0.0.1:6379> geopos cities:locations tianjin shijiazhuang |
geodist key member1 member2 [unit]
计算出的距离会以双精度浮点数的形式被返回。如果给定的位置元素不存在,那么命令返回空值。指定单位的参数unit必须是以下单位的其中一个:
m表示单位为米
km表示单位为千米
mi表示单位为英里
ft表示单位为英尺
如果用户没有显式地指定单位参数,那么geodist默认使用米作为单位。
geodist命令在计算距离时会假设地球为完美的球形,在极限情况下,这一假设最大会造成0.5%的误差。
1 | 127.0.0.1:6379> geodist cities:locations tianjin shijiazhuang |
georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。范围可以使用以下其中一个单位:
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
withdist:在返回位置元素的同时,将位置元素与中心之间的距离也一并返回.距离的单位和用户给定的范围单位保持一致。
withcoord:将位置元素的经度和纬度也一并返回。
withhash:以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用不大。
命令默认返回未排序的位置元素。
通过以下两个参数,用户可以指定被返回位置元素的排序方式:asc:根据中心的位置,按照从近到远的方式返回位置元素
desc:根据中心的位置,按照从远到近的方式返回位置元素。
在默认情况下,georadius命令会返回所有匹配的位置元。虽然用户可以使用count选项去获取N个匹配元素,但是因为命令在内部可能会需要对所有被匹配的元素进行处理,所以在对一个非常大的区域进行搜索时,即使只使用count选项去获取少量元素,
1 | 127.0.0.1:6379> georadius cities:locations 117 39 200 km withdist |
georadiusbymember key member radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
和georadius命令一样,都可以找出位于指定范围内的元素,
但是georadiusbymember的中心点是由给定的位置元素决定的。
而不是像georadius那样,使用输入的经度和纬度来决定中心点。1
2
3
4
5
6
7127.0.0.1:6379> georadiusbymember cities:locations tianjin 100 km
1) "beijing"
2) "tianjin"
3) "tangshan"
127.0.0.1:6379> georadiusbymember cities:locations beijing 100 km
1) "beijing"
2) "tianjin"geohash key member [member...]
使用geohash将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似表示距离越近。1
2
3
4
5
6127.0.0.1:6379> geohash cities:locations tangshan baoding
1) "wx5bj2um070"
2) "wwcgp6x9580"
127.0.0.1:6379> geohash cities:locations beijing tianjin
1) "wx48ypbe2q0"
2) "wwgq34k1tb0"zrem
GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset
,所以可以借用zrem命令实现对地理位置信息的删除.1
zrem cities:locations tianjin
应用
- 1、查看附近的人、餐厅、公司等
1
2georadiusbymember company huoli 20 km count 3 asc
范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身
事务 Transaction
MULTI
标记一个事务块的开始。
事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC命令原子性(atomic)地执行。
O(1)操作;EXEC
执行所有事务块内的命令。
假如某个(或某些) key 正处于 WATCH命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
时间复杂度:事务块内所有命令的时间复杂度的总和。
返回:事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil
DISCARD
取消事务,放弃执行事务块内的所有命令。
如果正在使用 WATCH命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH
O(1)操作,总是返回okWATCH key [key ...]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
O(1)操作UNWATCH
取消 WATCH命令对所有 key 的监视。
如果在执行 WATCH命令之后, EXEC命令或DISCARD命令先被执行了的话,那么就不需要再执行 UNWATH了。因为 EXEC命令会执行事务,因此 WATCH命令的效果已经产生了;而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH了。
内部原理
持久化
概述
Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制 来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。
持久化就是:redis将所有的数据保存在内存中,对数据的更新它会异步地保存到磁盘上
。
主流数据库的持久化方式:
快照
:
如:MySQL的Dump、Redis的RDB写日志
:
如:MySQL的Binlog、Hbase的HLog、Redis的AOF日志
两者区别:- 1、快照是一次全量备份,AOF 日志是连续的增量备份;
- 2、快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本;
- 3、AOF 日志在长期的运行过程中会 变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
三种方式
RDB快照
RDB快照,也就是定时快照(snapshot),是存储在硬盘中的二进制文件,是一个复制媒介用于一次全量备份
。
RDB快照持久化,也就是是将redis数据库某个时点的数据信息以快照文件的形式保存到磁盘的持久化方法。
主要思想
:
RDB方式实际就是在Redis内部存在一个定时器机制,扫描进程按照配置文件中的要求去检查数据的变化情况,即根据指定时间内的数据变化次数决定是否进行持久化。
当达到持久化的触发条件时,操作系统会单独创建(fork)一个子进程来进行数据持久化的操作,子进程默认与父进程有共同的地址空间,这样子进程就可以遍历整个内存来进行存储操作,主进程此时仍然可以正常提高服务,只是不进行I/O操作,有写入请求时,由操作系统按照内存页(Page)为单位进行写时复制Copy-on-Write,从而保证主进程的高效性能。
子进程数据写入时是将数据先写入一个临时文件中,当整个数据写入完毕后,才会用临时文件覆盖上一个持久化好的快照文件,这样保证了系统可以随时的进行数据备份,数据文件总是可用的。
由上可知:RDB模式的持久化,数据的完整性没有保障
。
触发复制的三种方式:
手动触发:
save一个同步的命令:O(n)操作
该指令会阻塞当前 Redis 服务器,执行 save 指令期间,Redis 不能处理其他命令,直到 RDB 过程完成为止。如存在老的RDB文件,会新建一个临时文件,执行完毕后替换老文件,再删除老文件。bgsave 一个异步命令:O(n)操作
。
执行该命令时,Redis 会在后台异步执行快照操作,此时 Redis 仍然可以相应客户端请求。如存在老的RDB文件,会新建一个临时文件,执行完毕后替换老文件。具体操作是Redis进程执行linux的fork函数 操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。Redis 只会在 fork 期间发生阻塞,但是一般时间都很短。但是如果 Redis 数据量特别大,fork 时间就会变长,而且占用内存会加倍,这一点需要特别注意。
自动触发:(redis.conf 配置)
参考:配置中的持久化模块
可以设置指定时间内key的变化数量来自动触发。其他触发机制:(不可忽视)
全量复制
:涉及到主从复制,在没有触发手动和自动的时候,主从复制时(全量复制),主会生成rdb文件。debug reload操作
:进行debug级别的重启,也会生成rdb文件。命令 shutdown [nosave|save]
:关闭服务指定save时,也会生成Shutdown 命令执行以下操作:
停止所有客户端
如果有至少一个保存点在等待,执行 SAVE 命令
如果 AOF 选项被打开,更新 AOF 文件
关闭 redis 服务器(server)
原理
背景:
单线程同时在服务线上的请求还要进行文件 IO 操作,文件IO操作会严重拖垮服务器请求的性能。Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。
在服务线上请求的同时,Redis还需要进行内存快照,内存快照要求 Redis 必须进行文件IO操作,可文件 IO 操作是不能使用多路复用API。COW 写时复制(copy-on-write)
为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变:比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这要怎么办? 所以,
Redis 使用操作系统的fork多进程 COW(Copy On Write) 机制来实现快照持久化
。
大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率。
实现原理:
- fork是类Unix操作系统上创建进程的主要方法。fork用于创建子进程(等同于当前进程的副本)。
新的进程要通过老的进程复制自身得到,这就是fork!- fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。
当父子进程都只读内存时,相安无事。
当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。
中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
优点:
1、COW技术可减少分配和复制大量资源时带来的瞬间延时。
2、可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
缺点:
1、如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。
fork多进程操作
:- Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
- 子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。
- 进程分离的逻辑:
fork 函数会在父子进程同时返回,在父进程里返回子进程的pid,在子进程里返回零。
如果操作系统内存资源不足,pid 就会是负数,表示 fork 失败。 - 子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。
但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。 - 这个时候就会使用操作系统的
COW机制(写时复制, copy-on-write)
来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的2倍大小。
另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。
每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,
这也是为什么 Redis 的持久化叫「快照」的原因。
接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
AOF追写文件
AOF 日志是连续的增量备份,记录的是内存数据修改的指令记录文本。
就是把执行过的写指令按照执行顺序写在记录文件的尾部,重启时读取文件指令明细按照顺序执行一遍就能完成恢复。
- 主要思想总结:
配置文件redis.conf中的appendonly参数
就是控制AOF功能的启动与关闭,yes表示开启,如果在有写操作,指令就会被追加到记录文件的尾部。
AOF默认的存储策略是每秒钟执行一次,术语称之为fsync
。
即把缓存中的写指令记录到文件中,这是redis持久化与性能的最佳平衡点,此策略既能保证 redis 有很好的性能表现又能保障数据完整性,最多存在 1 秒钟的数据丢失。
在文件追加过程中,如果发生了意外错误,例如系统宕机等意外状况,导致记录文件写入不完整的缺陷,这种情况下,redis提供了一个叫redis-check-aof
的工具
可以对不完整的日志文件进行修复处理。
AOF文件一定会随着数据量变动越来越大,有可能导致日志空间不足等意外风险,为了避免这个低级的错误发生,redis 提供了一个方便实用的自我保护机制-重写(rewrite)机制
,
就是管理员预先设定 AOF 文件大小的阈值,当实际大小超过阈值时,redis 就会启动了文件内容压缩,只保留了能保障数据恢复的最
小可用的指令集。文件重写功能仍然采用了先创建并写入临时文件,当重写过程结束后,才更名及覆盖上一个可用的 aof 文件模式来保障备份文件的随时可用。
AOF 文件本身是可读且可编辑的
。
可读可编辑的好处:假设我们在操作 redis 时,不小心执行了 flushall,内存数据全部清空了,但是开启了AOF功能且AOF文件没有被重写的前提下,我
们可以暂停 redis 并对aof文件进行编辑,删除文件末尾保存的 flushall 指令,重启 redis,内存数据就能恢复。
RDB快照的问题
1、耗时、耗性能。
将内存中的数据dump到硬盘,是一个O(n)过程,比较耗时;
bgsave中的fork():消耗内存,copy-on-write策略
硬盘I/O:IO性能问题2、不可控、容易丢失数据。
如该场景:
T1时间 执行多个写命令;
T2时间 满足RDB自动创建的条件;
T3时间 再次执行多个写命令;
T4时间 宕机,就会出现数据丢失
原理
AOF 日志存储的是 Redis服务器的顺序指令序列,只记录对内存进行修改的指令记录。
- 假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过 对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
- Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。
这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。 - Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。
AOF重写(提高效率)
Redis 提供了 bgrewriteaof 指令
用于对 AOF 日志进行瘦身。
日志瘦身:对执行的命令进行合并,如String类型多次赋值只保留最后一次,多次操作改为批量命令操作等。
其原理就是:对redis内存中的内容进行回溯,回溯成aof文件。
fork开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。
- AOF重写配置:
1
2
3
4
5auto-aof-rewrite-min-size 64mb
AOF文件重写需要的尺寸
auto-aof-rewrite-percentage 100
AOF文件增长率
fsync操作(宕机AOF丢失数据问题)
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了硬盘的缓冲区(内核为文件描述符分配的一个内存缓存中),然后内核会异步将脏数据刷回到磁盘的。
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个 时候就会出现日志丢失。那该怎么办?
- Linux 的 glibc 提供了
fsync(int fd)函数
可以将指定文件的内容强制从内核缓存刷到磁 盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。
但是 fsync 是一个磁盘IO操作,它很慢。
如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。- 所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。
这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。- Redis 同样也提供了另外两种策略:
一个是永不 fsync,让操作系统来决定合适同步磁 盘,很不安全;
另一个是来一个指令就 fsync 一次,非常慢。
- 三种配置策略:
具体参考:aof持久化策略的配置1
2
3no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
always 表示每次写入都执行fsync,以保证数据同步到磁盘。
everysec 表示每秒执行一次fsync,可能会导致丢失这1s数据
混合持久化
重启 Redis 时,我们很少使用rdb 来恢复内存状态,因为会丢失大量数据(rdb的缺点)。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。Redis 4.0 为了解决这个问题,带来了一个新的持久化选项:混合持久化
。
- 生效的两个配置:
1
2appendonly yes
aof-use-rdb-preamble yes - 思想:
将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF全量文件重放,重启效率因此大幅得到提升。
RDB和AOF对比
- rdb持久化:故障数据丢失比aof严重,但是服务重启恢复数据快
- aof持久化:故障数据丢失较rdb少,但是服务启动时恢复数据慢,因为要把aof文件中指令执行一遍。
- RDB 启动优先级低、体积小、恢复速度快、容易丢数据、比较重的操作;
AOF 启动优先级高、体积大、恢复速度慢、丢数据要根据策略决定、比较轻的操作;
运维常见问题
通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行
。
从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。因为:
- 快照(bgsave)是通过开启子进程的方式进行的,它是一个比较耗资源的操作;遍历整个内存,大块写磁盘会加重系统负载;
AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统 IO 负担。 - 但是如果出现网络问题,从节点长期连不上主节点,就会出现数据不一致的问题。
特别是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。 - 还应该再增加一个从节点以降低网络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。
- 快照(bgsave)是通过开启子进程的方式进行的,它是一个比较耗资源的操作;遍历整个内存,大块写磁盘会加重系统负载;
子进程开销和优化:
CPU
:
RDB和AOF文件生成,属于CPU密集型;不做CPU绑定,不和CPU密集型部署。内存
:
fork内存开销,copy-on-write,即父子进程共享只读分段文件时,父进程某个分段文件发生写入,会将其拷贝出一份新的分段文件,造成内存开销。
单机部署时,不允许产生大量重写,硬盘
:
AOF和RDB文件写入,可以集合iostat和iotop分析;
1、不要和高硬盘负载的服务部署在一起,如:存储服务、消息队列等;
2、no-appendfsync-no-rewrite = yes,表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,
3、根据写入量决定磁盘类型:如SSD
4、单机多实例持久化文件目录可以考虑分盘
主从复制
概述
Redis数据库在单机时:机器故障问题、容量瓶颈、QPS的瓶颈。
主从复制就是对主节点进行数据备份、读写分离对读进行分流。
可以为数据提供副本,扩展redis的读的性能。
1、一个Master可以有多个Slave;
2、一个Slave只能有一个Master;
3、数据流向是单向的,Master到Salve;
4、默认情况下,Redis都是主节点
主要思想
:
从服务器启动并与主服务器成功建立连接后,它会主动发起 SYNC请求,Master 接收到同步请求时会调用 BGSAVE 指令来创建一个专用子进程来完成数据持久化处理。
全量数据持久化处理可以分成两个阶段:
首先,将主服务器的存量数据都写入 RDB 文件,Master 在数据持久化期间产生的增量数据会启动
另一个后台存盘进程,把增量的写指令记录全部缓存在内存中,等待存量数据持久化完成后,Master 把 RDB 数据库文件传送给 Slave,Slave 接收到数据库文件后将其存放在磁盘上并逐步完成数据加载,这样存量数据就同步结束了;
然后,要进行同步的是持久化期间的增量数据,Master 将所有缓存在内存中的写指令按照约定的 redis 协议格式发给 Slave,Slave 接收后在本地执行这些数据写命令,从而达到最终的数据完全同步。
不论是那一种逻辑结构,Master只会执行一次持久化动作无论有多少个slave有同步请求,然后把持久化好的 RDB 文件分发下去
。
Redis2.8 的版本提供了数据增量同步策略
:
Master和Slave如果断开连接,之后又重新连接时。在连接成功后,可以尝试进行增量数据同步。
增量数据同步策略是主服务器会在内存中维护一个缓冲区来存放待同步数据,主从连接成功后,Slave会把“申请同步的主服务器ID”和“请求的数据的偏移量(replication offset)”发送给Master,Master 收到增量同步请求时,根据上送的申请同步的主服务器ID去匹配自己的ID信息,匹配成功后检查自己的缓存区数据是否能满足申请的数据偏移量,如果都两个条件都满足,则能完成master-slave 的增量数据同步。
如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。
步骤
- 1、
执行slaveof
后,slave只保存master的地址信息,直接返回; - 2、
主从建立socket连接
;
slave内部通过每秒执行的定时任务维护复制相关逻辑,当发现新的matser时,尝试建立网络连接,slave会建立一个socket套接字,用于接收master发送的复制命令;如slave无法建立连接,定时任务会一直重连到成功或执行slaveof no one取消复制。 - 3、
发送ping命令
:
连接成功后slave发送ping请求首次通信,检测MS之间套接字是否可用、是否可接收处理命令;
如slave没收到master的pong回复或超时,下次定时任务会重连; - 4、
权限验证
:
如Master设置了requirepass参数,需要密码验证。
如验证失败复制将终止,slave重新发起复制流程。 - 5、
同步数据集
:
主从连接正常通信后,如首次建立,master会把持有的数据全部发送给slave。(通过rdb或socket的方式) - 6、
命令持续复制
:
当master把数据同步给slave复制流程建立成功后,后面M会持续的把写命令发送给S,保证主从数据一致性。
两种配置
1、slaveof命令:异步的操作
slave上执行命令slaveof MasterHost 6379
来异步复制,slave机器重启就会丢失,不建议。
取消复制:slaveof no one
- 取消时slave不会清除已经同步的数据;
- slave配置新的Master时,会清除历史数据;
2、配置文件
replicaof <masterip> <masterport>
对Slave机器,替换为对应的主服务器IP和主服务器的端口号。
如果主服务器(redis)有设置密码的话,则需要配置密码masterauth
注意:修改需要重启
全量/部分复制:
涉及知识点
1、
runid
每个Redis服务器都会有一个表明自己身份的ID。
在PSYNC中发送的这个ID是指之前连接的Master的ID,如果没保存这个ID,PSYNC的命令会使用PSYNC ? -1
这种形式发送给Master,表示需要全量复制。2、
复制偏移量offset:
通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。在主从复制的Master和Slave双方都会各自维持一个offset。
Master成功发送N个字节的命令后会将Master的offset加上N,Slave在接收到N个字节命令后同样会将Slave的offset增加N。
Master和Slave如果状态是一致的那么它的的offset也应该是一致的。3、
复制积压缓冲区backlog
:
当从节点在一段时间内断开连接时,主节点会收集数据到backlog这个缓冲区。
因此当一个从节点想要重新连接时,通常不需要完全的重新同步,但是部分的重新同步就足够了,只是通过在断开连接的时候传递数据的一部分。是由Master维护的一个固定长度的FIFO队列,默认大小为1M。
它的作用是缓存已经传播出去的命令。
当Master进行命令传播时,不仅将命令发送给所有Slave,还会将命令写入到复制积压缓冲区里面。
因此当一个slave想要重新连接时,如runingid与M一致且偏移量与M相差没超过缓冲区大小,通常不需要完全的重新同步,增量同步缓冲区的命令就足够了。4、
psync命令
:
从节点使用psync命令完成部分复制和全量复制功能。
PSYNC执行过程中比较重要的概念有3个:runid、offset(复制偏移量)以及复制积压缓冲区。
- 1、客户端向服务器发送SLAVEOF命令,让当前服务器成为Slave;
- 2、 当前服务器根据自己是否保存Master runid来判断是否是第一次复制,如果是第一次同步则跳转到3,否则跳转到4;
- 3、 向Master发送PSYNC ? -1 命令来进行完整同步;
- 4、 向Master发送PSYNC runid offset;
- 5、 Master接收到PSYNC 命令后首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;
- 6、 如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,
那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。
全量复制
一般用于初次复制场景,Redis早期支持的复制功能只有全量复制。它会把主节点全部数据一次性发送给从节点。
当数据量较大时,会对主从节点和网络造成很大的开销。
- 1、发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1;
- 2、主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应;
- 3、从节点接收主节点的响应数据保存运行ID和偏移量offset;
- 4、主节点执行bgsave异步快照命令,保存RDB文件到本地;
- 5、主节点发送RDB给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志;
- 6、对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令。
因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送个从节点,保证主从之间数据一致性;
全量复制注意:如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。
client-output-buffer-limit replica 256mb 64mb 60 如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败;
对于主节点,当发送完所有的数据后就认为全量复制完成;- 7、从节点接收完主节点传送来的全部数据后会清空自身旧数据;
- 8、从节点清空数据后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时,可以通过计算日志之间的时间差来判断加载RDB的总耗时;
- 9、从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof AOF日志瘦身操作,为了保证全量复制后AOF持久化文件立刻可用。
全量复制比较耗时:
- Master进行bgsave快照持久化时间;
- RDB快照文件网络传输时间;
- Slave清空老数据时间;
- 可能存在的AOF重写时间;
部分复制
部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施。使用psync {runId}{offset}命令
实现。用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,
当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。如复制缓冲区 repl-backlog-xx的配置
流程:
- 1、当主节点直接网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接;
- 2、主从连接中断期间主节点依然响应命令,但因复制连接中断,命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB,可以通过into replication 查看;
- 3、当从节点网络恢复后,从节点会再次连上主节点;
- 4、当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。
因此会把它们当做psync参数发送个主节点,要求进行部分复制操作;- 5、主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;
之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+COUTINUE响应,表示可以进行部分复制;- 6、主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。
心跳检测
主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。
主从心跳检测机制:
- 1、主从节点彼此都有心跳检测机制,各自模拟对方的客户端进行通信,主节点的连接状态为flags=M,从节点连接状态为flags=S
- 2、主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可以通过repl-ping-replica-period 10 控制发送频率
- 3、从节点在主线程中每隔一秒发送replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
- 主节点根据replconf命令判断从节点超时时间,体现在info replication 统计中的lag信息中,
lag表示从节点最后一次通信延迟的秒数,正常延迟应该在0到1之间。 - 如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测和继续执行.
故障转移
Master故障
假如主从都没数据持久化,此时千万不要立即重启服务,否则可能会造成数据丢失。应该:
- 在slave上执行
SLAVEOF ON ONE
,来断开主从关系并把slave升级为主库; - 此时重新启动主数据库,执行SLAVEOF,把它设置为从库,自动备份数据。
Slave故障
如满足业务需求,可以将宕机的slave的连接转移到其他slave。
在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据;
如果从数据库实现了持久化,只要重新假如到主从架构中会实现增量同步。
自动故障转移?哨兵机制
Redis提供了sentinel(哨兵)机制通过sentinel模式启动redis后,自动监控master/slave的运行状态
。
基本原理是:心跳机制+投票裁决
。
- 每个sentinel会向其它sentinal、master、slave定时发送消息,以确认对方是否“活”着,
如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机” Subjective Down,简称SDOWN)。 - 若"哨兵群"中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称ODOWN), 通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
sentinel哨兵故障自动转移流程
:
- 1、通过心跳机制,多个sentinel发现并确认master有问题;
- 2、选举出一个sentinel作为领导;
- 3、选出一个slave作为新的master;
- 4、通知其余slave成为新master的slave;
- 5、通知客户端主从发生变化;(客户端连接哨兵即可获取主从的信息)
- 6、等待老的master复活成为新master的slave;
读写分离
默认是读写分离的:replica-read-only yes
对于读占比较高的场景,可以通过把一部分读流量分摊到slave来减轻master压力,同时需要注意永远只对主节点执行写操作。
建议大家在做读写分离之前,可以考虑使用Redis Cluster 等分布式解决方案。
常见问题
1、读写分离:
默认是读写分离的:replica-read-only yes
用于读多写少的场景,将流量分配到Slave节点,减少Master的压力,扩展读的能力。
可能问题:
- 复制数据延迟,出现读写不一致的情况;
- 读到过期数据;
过期策略:
a. 懒惰性策略(操作key时才会看是否过期)
b. 定时采样key,看是否过期;
因为slave只要读操作不能del处理,要靠master将过期删除的命令发送过来再执行,会造成Slave读到过期数据。 - 从节点发送故障;
2、主从配置不一致:
如:
- maxmemory不一致:会出现丢失数据;
- 数据结构优化参数不一致,如hash-max-ziplist-entries:会出现内存不一致问题;
3、规避全量复制:
1、新加slave第一次全量复制不可避免:
可以注意:
- 小主节点,控制内存;
- 低峰时间处理;
2、节点runningid不匹配: - 主节点重启,runningid变化;
- 故障转移,哨兵或集群;
3、复制积压缓冲区不足: - 网络中断,部分复制无法满足;
- 增大缓冲区的配置rel_backlog_size;
4、规避复制风暴:
如一主多从,主挂重启,需要生成rdb并复制到从节点;
- 单主节点复制风暴:
问题:主节点重启,多从节点复制;
解决:更换复制拓扑,slave下挂slave。 - 单机器复制风暴:
机器上节点都是master,机器宕机,会有大量的全量复制;
可以主节点分散多机器。
Sentinel哨兵
概述
因为主从复制的缺陷:
1、手动故障转移;
2、写能力和存储能力受限;
引出哨兵机制:Redis的哨兵机制是官方推荐的一种高可用(HA)方案。
哨兵机制主要三个功能:
- 1、监控:
不停监控Redis主从节点是否安装预期运行; - 2、提醒:
如果Redis运行出现问题可以按照配置文件中的配置项 通知客户端或者集群管理员; - 3、自动故障转移:
当主节点下线之后,哨兵可以从主节点的多个从节点中选出一个为主节点,并更新配置文件和其他从节点的主节点信息。
客户端连接
步骤:
- 1、获取所有的sentinel节点和masterName。
遍历sentinel集合找到可用节点; - 2、在找到的可用sentinel节点上,执行命令
sentinel get-master-addr-by-name masterName
;会获取到master节点真正地址和端口。 - 3、获取master节点信息后去role验证真伪;
- 4、如redis节点发生了变化,client会感知到。
内部基于发布订阅感知:client会订阅sentinel端某个频道,里面有谁是master端信息,有了变化会通知client。
实现原理
客户端高可用案例:(故障转移)
Redis Sentinel Failover故障选举测试:死循环对redis哨兵主从进行读写
故障转移恢复案例:
- 执行该死循环程序;
- 将其中的7000节点进行强制宕机;这时程序会大量的报错:Connection refused。
- 过了n秒后,sentinel自动进行完故障转移后,程序就会正常执行打印;
服务端日志分析:数据节点和sentinel节点
日志中故障转移大致流程:
- 1、发现master不可用,进入主观不可用(SDOWN);
- 2、进行投票,如达到了quorum(配置文件指定),进入客观不可用(ODOWN)
- 3、当前配置版本被更新;
- 4、达到failover条件,正等待其他sentinel的选举:
开始要选择一个slave当选新master;
找到了一个适合的slave来担当新master;
当把选择为新master的slave的身份进行切换; - 5、Failover状态变为reconf-slaves
- 6、sentinel发送SLAVEOF命令把它重新配置,重新配置到新主;将其他slave配置到新master;
- 7、新的master对新的slave进行数据复制同步;
- 8、老的master离开客观不可用(ODOWN),failover成功完成。
- 9、master地址发生改变,变为新的master
- 10、检测slave并添加到slave列表。
三个定时任务:
- 1、每10秒每个sentinel会对master和slave进行info操作:
作用就是发现slave节点,并且确认主从关系,
因为redis-Sentinel节点启动的时候是知道 master节点的,只是没有配置相应的slave节点的信息 - 2、每隔2秒,sentinel都会通过master节点内部的channel来交换信息(pub/sub订阅模式):
作用是通过master节点的频道来交互每个Sentinel对master节点的判断信息 - 3、每隔一秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作:
对于master来说若超过30s没回复,就对该master进行主观下线并询问其他的Sentinel节点是否可以客观下线。
心跳检测的过程,用来判断上下线的依据。
主观下线和客观下线:
1 | sentinel monitor mymaster 127.0.0.1 6379 2 |
- 主观下线:每个Sentinel节点对Redis节点失败的“偏见”。
- 客观下线:所有Sentinel节点对Redis节点失败达成共识。
Sentinel节点领导者选举
:
因为:只有一个sentinel节点完成故障转移即可。
选举:
通过sentinel is-master-down-by-addr
命令都有希望成为leader。
该命令的作用:
a.交换master节点的失败判定;
b.进行领导者选举。
- 1、每个做主观下线的sentinel节点向其他sentinel节点发命令,要求它设为领导者;
- 2、收到命令的sentinel节点如目前还没同意通过其他sentinel节点发的命令,就会同意当前请求,否则拒绝;
- 3、如该sentinel节点的票数超过sentinel集合半数且超过指定的quorum,它就成为领导者;
- 4、如果有多个sentinel节点成为了领导者,就会等一段时间继续重新选举。
故障转移:(sentinel领导者完成)
- 1、从slave节点选择一个合适的节点作为新的master;
- 2、对上面的slave节点执行slaveof on one命令,让它成为master;
- 3、向剩余的slave节点发送slaveof命令,让它们成为新master的slave;
- 4、进行新主从数据的复制同步;
- 5、更新老的master为slave,并一直关注它,当重启生效后就去复制新的master节点的数据。
如何选择合适的slave节点作为新的master?
- 1、选择slave-priority优先级最高的slave节点,如存在返回,否则继续;
- 2、选择复制偏移量最大的slave节点(数据最完整,类似zookeeper),如存在返回,否则继续;
- 3、选择runningid最小的slave节点(也就是最早启动的节点);
注意
- 1、sentinel集群节点大于等于3且为奇数
- 2、redis的sentinel是配置中心不是代理,其中的数据节点和普通数据节点没区别。
客户端初始化连接的是sentinel节点集合,不是具体的redis节点;
它通过3个定时任务实现了sentinel节点对于master和slave和其余sentinel节点的监控;
Cluster集群
为什么需要集群?
- 并发问题:redis最高的ops并发量为10w,如果业务需要ops为100万就无法解决;
- 数据量问题:单机的内存太小,无法满足需求;
集群架构
- 单机架构(主从模式也是单主机架构)
- 分布式架构:
服务端多个节点,每个节点都可读写,节点间都是可以通信的,节点间互相了解各自对应的槽。
client访问任意节点,读key时,如在该节点会直接返回,否则返回key真实的槽所在的节点信息,在做对应跳转获取;
数据分布:
两种分区方式:
顺序分布
:
(上图3个节点,平均每个节点33个数据)
顺序分区的数据量不可确定性会导致倾斜,支持顺序访问,但不支持批量操作。
哈希分布
:
节点取余分区 hash(key)%nodes
数据分散度高,无法顺序访问,支持批量操作;
1、节点取余分区:hash(key)%nodes
- 会有节点伸缩:数据节点关系变化,导致数据迁移;
- 迁移数量和添加节点的数量有关,建议翻倍扩容,迁移数据量会比较小。
如3个节点变为6个节点,这样数据迁移量在50%左右;
如对节点进行扩容时:如3个节点变为4个节点。
问题:
- 如果要增加分区,数据迁移量在80%左右;
- 数据迁移第一次是无法从缓存中取到的,数据库需要回写到新节点;
2、一致性哈希分区:哈希 + 顺时针(优化取余)
将数据作为一个0~2^ 32大小的token环中,有4个node,为每个node分配一个token,每个node负责范围内的数据;
如某key进行hash计算落在node3和4范围内,它会顺时针找离自己最近的node,即node3。
- 节点伸缩:只影响邻近节点,但是还是有数据迁移;
- 翻倍伸缩:保证最小迁移数据和负载均衡;
- 多用在节点非常多的时候。
节点扩容的情况:
如上图,添加节点node5,会进行数据的漂移,但不会影响node3和4。尤其节点非常多的时候效率提高太多;
3、虚拟槽分区:(共享消息模式,集群默认)
- 预设虚拟槽(redis cluster范围0-16383):每个槽映射一个数据子集,一般比节点数大;
- 良好的哈希函数:例如CRC16;
- 服务端管理节点、槽、数据:例如Redis Cluster;
假如有10w数据,有16384个槽,5个节点,对槽进行分配,对key按照一定的hash规则计算后,再对16383进行取余,会把取余对结果发生给cluster中的任意一个节点,而每个节点都知道自己负责的槽,如落在自己槽的范围内,就由它管理。如不在该槽,因为节点间会共享消息,所以就会知道该key对应的真实的槽。
集群伸缩
伸缩原理
集群伸缩 = Slot槽和数据在节点之间的移动。
集群扩容:增加新节点
1、
准备新节点
:
集群模式下,配置和其他节点统一,目前启动后仍是孤儿节点;2、
加入集群
:
作用:为它迁移槽和数据实现扩容;作为slave节点负责故障转移;3、
迁移槽和数据
:迁移数据过程:
- 对目标节点发送:cluster setslot {slot) importing {sourceNodeld)命令,让目标节点准备导入槽的数据。
- 对源节点发送:cluster setslot {slot) migrating {targetNodeld)命令,让源节点准备迁出槽的数据。
- 源节点循环执行cluster getkeysinslot {slot) {count)命令,每次获取count个属于槽的健。
- 在源节点上执行migrate {targetlp} {targetPort} key 0 {timeout}命令把指定key迁移。
- 重复执行步骤3~4直到槽下所有的键数据迁移到目标节点。
- 向集群内所有主节点发送cluster setslot {slot)node {targetNodeld)命令,通知槽分配给目标节点。
集群缩容:下线节点和槽
客户端路由:(实际开发会遇到的问题)
moved重定向
client接收到moved异常后,会拿到正确的目标节点需自己去执行;
注意:client不会自动找到目标节点进行跳转,需要二次写的逻辑进行功能开发;
槽命中,直接返回(可查看key的slot)
槽未命中:返回moved异常:
客户端不会自己找到异常节点,需要自己写逻辑;
集群和非集群环境下:
集群环境下会自动完成:捕获moved异常和重新写的操作;
ask重定向
在进行集群伸缩时,会出现数据slot迁移,出现ask重定向。
ask是槽还在迁移中;
moved是槽已经完成了迁移;
smart(智能)客户端:JedisCluster、追求性能
工作原理:(具体可看源码:redis.clients.jedis.JedisClusterCommand#runWithRetries)
- 1.从集群中选一个可运行节点,使用cluster slots初始化槽和节点映射。
- 2.将cluster slots的结果映射到本地,为每个节点创建JedisPool。
- 3.准备执行命令。
集群原理
批量操作
四种批量操作实现优化(mget,mset必须在一个槽)
串行mget
:相当于for循环遍历keys去对应的集群节点get值,最后将结果汇总;简单效率低,需要n次的网络时间;串行IO
:对串行mget优化,在client本地做內聚合将key的槽计算出,对key根据节点分组,之后通过几次pipeline操作即可;只需要节点个数次网络时间;并行IO
:对串行IO优化,使用多线程,只需1次网络时间;hash_tag
:将key进行hash_tag包装,使所有key都在一个节点,只需要1次网络时间即可;
故障转移
Redis Sentinel的故障转移依赖外部节点sentinel来实现;
而Redis Cluster自身实现了高可用,当前节点出了问题其他节点会监控得知,实现故障转移;
故障发现:
通过节点间的ping/pong消息实现,不需要sentinel。
- 主观下线:
某个节点认为另一个节点不可用。
- 客观下线:
客观下线:半数以上持有槽点主节点标记某节点主观下线。
故障恢复:
资格检查:
准备选举时间:
保证偏移量大的slave有更小的延迟达到选举时间,保证数据一致性更高。选举投票:
替换主节点:
常见问题
集群完整性
cluster-require-full-coverage
默认为yes
表示是否需要所有集群节点都是在线的状态,所有的16384个槽是全部可用的,才会认为集群是完整的,才可以对外提供服务。
注意:如为yes
- 节点故障或者正在故障转移时会有:(error)CLUSTERDOWN The cluster is down
- 大多数业务无法容忍,cluster-require-full-coverage建议设置为no
宽带消耗
- 官方建议节点不超过1000个,因为节点间进行ping/pong操作,过多会带来比较大的带宽消耗;
优化:
- 避免“大”集群:避免多业务使用一个集群,大业务可以多集群。
- cluster-node-timeout:带宽和故障转移速度的均衡。
- 尽量均匀分配到多机器上:保证高可用和带宽
Pub/Sub广播模式的局限性:
问题:发布一条消息,每个节点都会接收到,加重带宽消耗。
解决:单独“走”一套Redis Sentinel。
集群倾斜问题:
数据倾斜和请求倾斜。
数据倾斜:内存不均。
4种原因:节点和槽分配不均;
不同槽对应键值数量差异较大;
包含bigkey;
内存相关配置不一致;
请求倾斜:热点数据(缓存常见问题)
集群的读写分离:
只读连接:集群模式下从节点不接受任何读写请求
。
如进行了读操作:会重定向到负责槽点主节点;
使用readonly命令可以读,是一个连接级别的命令。
上图7000为Master,7003为它的Salve。
集群的读写分离更复杂:
- 同样的问题:复制延迟、读取过期数据、从节点故障
- 修改客户端:cluster slaves {nodeld)
集群VS单机(sentinel/主从/单点):
针对集群
分布式集群redis不一定好。大部分场景下
Redis Sentinel
就足够了。
总结
Redis Cluster数据分区规则采用
虚拟槽方式
(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。搭建集群包括四个步骤:准备节点、节点握手、分配槽、复制。
集群伸缩通过在节点之间移动槽和相关数据实现。
扩容时:根据槽迁移计划,把槽从源节点迁移到新节点;
缩容时:如果下线的节点有负责的槽,就需要迁移到其他节点上,再通过cluster forget
命令让集群内所有节点忘记并下线该节点。使用smart客户端操作集群打到通信效率最大化,客户端内部负责计算维护
键->槽->节点的映射
,用于快速定位到目标节点。集群自动故障转移过程包括:
故障发现
和节点恢复
。
节点下线包括主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时,标记它为客观下线状态。
从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。
经典案例
Redis缓存
缓存的收益和成本?
收益:
- 1、加速读写速度:
- 2、降低后端负载:
后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载等
成本:
- 1、数据不一致:
缓存层和数据层有时间窗口不一致,和更新策略有关; - 2、代码维护成本:
要多加一层缓存逻辑; - 3、运维成本:
如Redis Cluster维护;
缓存更新策略:
常见策略
三种缓存算法:(FIFO/LRU/LFU)
FIFO算法
:
先进先出(FIFO,队列)。
即如果一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。空间满的时候,最先进入的数据会被最早置换(淘汰)掉。LRU算法
:
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,如果空间不足淘汰掉最近最少使用的数据。。
思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
实现可参考:
LRU算法实现测试LFU算法
:
LFU(Least Frequently Used ,最近最不常用算法),也就是淘汰一定时期内被访问次数最少的数据。
LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素。
因此可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表。
超时剔除:
时间过期时间
主动更新:
开发来控制缓存的生命周期;
redis的内存驱逐策略:
配置参数:maxmemory-policy noeviction
驱逐策略:内存容量超过maxmemory后的处理策略。
volatile-lru
:利用LRU算法移除设置过过期时间的key。LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
volatile-random
:随机移除设置过过期时间的key。volatile-ttl
:移除即将过期的key,根据最近过期时间来删除(辅以TTL)allkeys-lru
:利用LRU算法移除任何key。allkeys-random
:随机移除任何key。noeviction
:不移除任何key,只是返回一个写错误。volatile-lfu
:从已经设置过期时间的数据中,挑选最不经常使用的数据淘汰。allkeys-lfu
:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
一致性问题
- 低一致性:最大内存和淘汰策略
- 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底。
常见缓存问题:
缓存穿透
缓存穿透,即大量缓存中不存在的请求key访问直接落到数据库,一般是恶意攻击;
- 解决方案:
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器
。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
把这个空结果进行缓存
,但它的过期时间会很短,最长不超过五分钟。
缓存击穿
缓存击穿,指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key
。
- 解决方案:
添加互斥锁
:
结合上面的击穿的情况,在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,从而保护数据库。
但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。
缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
如:redis服务器挂掉导致请求大量涌至数据库;
而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
案例:
比如双十二活动,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
一般采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。
这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
解决方案:
事前:
使用集群缓存,保证缓存服务的高可用
。这种方案就是在发生雪崩前对缓存集群实现高可用,
如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。事中:
加本地缓存 + Hystrix限流&降级
,避免MySQL被hit死。- 使用本地缓存的目的也是考虑在Redis Cluster 完全不可用的时候,本地缓存还能够支撑一阵。
- 使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。
- 然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
事后:
开启Redis持久化机制,尽快恢复缓存集群
。一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
Redis实现分布式锁
分布式锁一般有三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。
确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性
。
在任意时刻,只有一个客户端能持有锁。不会发生死锁
。
即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。具有容错性
。
只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。解铃还须系铃人
。
加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁
1 | private static final String LOCK_SUCCESS = "OK"; |
加锁代码满足我们可靠性里描述的三个条件。
- 首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足
互斥性
。 - 其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),
不会发生死锁
。 - 最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。
- 由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。
解锁
1 | private static final Long RELEASE_SUCCESS = 1L; |