Redis使用详解
Redis使用详解
一、NoSQL简介
NoSQL分类
- 键值(Key-Value)存储数据库:这一类数据库主要会使用到一个hash表,如Redis、Oracle BDB
- 列存储数据库:通常是用来应对分布式存储的海量数据,键仍然存在但是他们指向了多个列,如HBase、Riak
- 文档型数据库:该类型的数据模型是版本化的文档,比如JSON,允许之间进行嵌套,如MongoDB
非关系型数据库特点
-
数据模型比较简单
-
对于数据库性能要求较高
-
不需要高度的数据一致性
二、Redis简介
以key-value形式存储,不一定遵循传统数据库的一些基本要求(非关系的、分布式的、开源的、水平可扩展的)
优点:对数据高并发读写
对海量数据的高效率存储和访问
对数据的可扩展性和高可用性
缺点:无法做太复杂的关系模型
Redis单线程:指处理我们的网络请求的时候只有一个线程来处理【文件刷盘等用的是多线程】
Redis单线程的好处:
-
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽,单线程实现比较简单
-
单线程避免了不必要的上下文切换和竞争条件以及加锁释放锁操作
-
使用多路I/O复用模型,非阻塞IO
三、Redis的安装
第一步:准备工作【解压tar包,创建Redis相关目录】
1 | tar -zxvf redis-5.0.2.tar.gz |
第二步:编译redis
1 | #进入解压后的tar包执行 |
第三步:移动配置文件到conf目录
1 | cp redis.conf /opt/redis/conf |
额外配置:
- redis开机自启
1 | vim /etc/rc.local |
- 配置数据保存目录
1 | vim /opt/redis/conf/redis.conf |
四、Redis基本通用命令
命令 | 说明 |
---|---|
KEYS [pattern] | 查找出匹配的key【生成环境禁止使用,数据量太大阻塞生成环境】 |
DBSIZE | 统计key总数【使用的是redis的内部计数,并不是全部扫描,生产可用】 |
EXISTS key [key …] | 检查key是否存在【存在返回1,不存在返回0】 |
DEL key [key…] | 删除key【成功删除返回1,不存在此key返回0】 |
EXPIRE key seconds | 设置过期时间 |
TTL key | 查看剩余的过期时间【-2代表已不存在,-1代表永不过期】 |
PERSIST key | 取消key的过期设置 |
TYPE key | 查询key的类型 |
五、Redis数据类型及其使用
注意:redis操作下标都是闭区间的
字符串【String】
String的值类型可以为字符类型、数字类型、bit类型
String类型是包含很多中类型的特殊类型,并且是二进制安全的。比如序列化的对象进行存储,比如一张图片进行二进制存储,比如一个简单地字符串,数值等等。
使用场景:缓存、计数器、分布式锁等
常用命令格式 | 描述 |
---|---|
{ GET key },{ MGET key [key …] } | 得到String |
{ SET key value },{ MSET key value [key value …] } | 设置String |
{ SETNX key value },{ MSETNX key value [key value …] } | 如果不存在则设置 |
SET key value XX | 如果存在则设置 |
SETEX key second value | 设置过期时间 |
{ INCR key },{ INCRBY key increment } | 自增操作 |
{ DECR key },{ DECRBY key decrement } | 自减操作 |
GETSET key value | 得到就值设置新值 |
APPEND key value | 追加字符串 |
STRLEN key | 得到字符串长度,内部存有计数 |
GETRANGE key start end | 得到指定长度的value |
SETRANGE key offset value | 设置指定偏移量字符串内容 |
哈希【Hash】
使用场景:存储具有一定结构化的数据
常用命令格式 | 描述 |
---|---|
HGET key field | 得到key对应field的value |
HSET key field value | 设置key对应field的value |
HDEL key field | 删除key对应field的value |
HEXISTS key field | 判断key的field是否存在 |
HLEN key | 获取指定key的filed数量【内部计数,生成环境可用】 |
HMGET key field [field …] | 批量获得hash的field对应的value |
HMSET key field value [field value …] | 批量设置hash的field和value |
HINCRBY key field increment | 增加指定increment的对应key的field |
HGETALL key | 获取key对应所有field和value【生成环境慎用】 |
HVALS key | 返回key对应的所有value【生成环境慎用】 |
HKEYS key | 返回key对应的所有field【生成环境慎用】 |
HSETNX key field value | 不存在此key对应的field则设置 |
列表【List】
列表为有序、可重复结构。可指定位置插入和删除、也可从左右插入和弹出(模拟栈结构)
常用命令格式 | 描述 |
---|---|
**LPUSH | RPUSH** key value [value …] |
LINSERT key BEFORE | AFTER value newValue |
**LPOP | RPOP** key |
LREM key count value | 根据count值,从列表删除所有等于value的值【时间复杂度O(N)】 |
【count>0,从左到右删除count个】 | |
【count<0,从右到坐删除count个】 | |
【count=0,删除所有value相等的值】 | |
LTRIM key start end | 按照索引范围保留list,删除大链表有用【时间复杂度O(N)】 |
LRANGR key start end | 获取列表指定索引范围内的值,数值为负则从右往左取值【时间复杂度O(N)】 |
LINDEX key index | 获取列表指定索引的值,数值为负则从右取值【时间复杂度O(N)】 |
LLEN key | 获取list长度【内部计数,生成环境可用】 |
LSET key index newValue | 设置指定位置的值【时间复杂度O(N)】 |
**BLPOP | RLPOP** key timeout |
使用技巧:
- LPUSH + LPOP = Stack
- LPUSH + RPOP = Queue
- LPUSH + LTRIM = Capped Collection【固定容量集合】
- LPUSH + BRPOP = Block Queue
集合【Set】
Set无序、无重复、有集合间操作。
常用命令格式 | 描述 |
---|---|
SADD key element [member …] | 向集合key添加元素 |
SREM key element | 删除集合key中的element元素 |
SCARD key | 查询集合元素的个数【内部计数,生成环境可用】 |
SISMEMBER key element | 集合中是否存在element元素 |
SRANDMEMBER key | 随机得到一个元素 |
SMEMBERS key | 获取集合所有元素【慎用】 |
SPOP key | 随机弹出一个元素 |
{ SDIFF key [key …] }、{ SDIFFSTORE destination key [key …] } | 返回/存储一个集合的全部成员,该集合是所有给定集合之间的差集 |
{ SINTER key [key …] }、{ SINTERSTORE destination key [key …] } | 返回/存储一个集合的全部成员,该集合是所有给定集合的交集 |
{ SUNION key [key …] }、{ SUNIONSTORE destination key [key …] } | 返回/存储一个集合的全部成员,该集合是所有给定集合的并集 |
使用技巧
-
SADD = Tagging
-
SPOP/SRANDMEMBER = Random Item
-
SADD + SINTER = Social Graph【共同关注、有着相同兴趣等】
有序集合【ZSet】
ZSet有序、无重复、包含分值与元素,有集合间操作。
使用场景:排行榜
常用命令格式 | 描述 |
---|---|
ZADD key score member [score member …] | 向集合添加元素 |
ZREM key member [member …] | 移除集合中的元素 |
ZSCORE key member | 得到元素的分数 |
ZINCRBY key increment member | 增长元素的分数 |
ZCARD key | 获得集合中元素的个数 |
ZRANK key member | 成员按分值递减(从小到大)排列的排名 |
ZRANGE key start stop [WITHSCORES] | 按score值递增(从小到大)排序,WITHSCORES返回分数 |
ZRANGEBYSCORE key max min [WITHSCORES] | 返回分数范围内数据,按score值递增(从小到大)排序,WITHSCORES返回分数 |
ZCOUNT key min max | 统计得到分数在min和max之间的元素个数 |
{ ZREMRANGEBYRANK key start stop }、{ ZREMRANGEBYSCORE key min max} | 按照排名/分数范围删除元素 |
ZREVRANK key member | 成员按分值递减(从大到小)排列的排名 |
ZREVRANGE key start stop [WITHSCORES] | 按score值递增(从大到小)排序,WITHSCORES返回分数 |
ZREVRANGEBYSCORE key max min [WITHSCORES] | 返回分数范围内数据,按score值递增(从大到小)排序,WITHSCORES返回分数 |
六、Redis高级功能
慢查询日志
- 慢查询发生在第三阶段
- 客户端超时不一定是慢查询导致,但慢查询可导致客户端超时
慢查询队列简介
- 先进先出的队列
- 固定长度
- 长度不够时,丢弃最早记录
- 保存在内存中
慢查询设置
-
slow-max-len【慢查询队列长度,默认128】
-
slowlog-log-slower-than【慢查询的阙值(微秒,1000000 微秒=1 秒),默认10000(10毫秒),建议为1000(1毫秒)】
更改慢查询参数建议使用
CONFIG SET parameter value
方式,而不是更改redis.conf文件重启redis
慢查询命令
常用命令格式 | 描述 |
---|---|
slowlog get n | 获取慢查询队列一条记录 |
slowlog len | 获取慢查询队列长度 |
slowlog reset | 清空慢查询队列 |
查询结果示例
Pipeline
一般情况下客户端发送一条命令到Redis,Redis处理结束返回结果,即 N条命令 = N次网络时间 + N次计算时间
PipeLine将命令打包发送给客户端,Redis处理完返回结果,PipLine是一个异步处理方式,并不等待Redis返回。
Pipeline(N条命令) = 1次网络时间 + N次计算时间
1 | /* |
发布订阅
常用命令格式 | 描述 |
---|---|
PUBLISH channel message | 将信息 message 发送到指定的频道 channel |
SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息 |
PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定pattern的频道 |
UNSUBSCRIBE [channel [channel …]] | 取消订阅 |
Bitmap
常用命令格式 | 描述 |
---|---|
SETBIT key offset value | 对 key所储存的字符串值,设置或清除指定偏移量上的位(bit) |
GETBIT key offset | 对 key所储存的字符串值,获取指定偏移量上的位(bit) |
BITCOUNT key [start] [end] | 计算给定字符串中,被设置为 1的比特位的数量 |
BITOP operation destkey key [key …] | 对一个或多个保存二进制位的字符串 key进行位元操作,并将结果保存到 destkey上 |
operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种 |
|
BITPOS key bit [start] [end] | 返回位图中第一个值为 bit 的二进制位的位置 |
HyperLogLog
实质:用String类型实现,不能取出具体值,有错误率
作用:极小的空间实现独立数量统计
常用命令格式 | 描述 |
---|---|
PFADD key element [element …] | 将任意数量的元素添加到指定的HyperLogLog |
PFCOUNT key [key …] | 计算HyperLogLog有多少值 |
PFMERGE destkey sourcekey [sourcekey …] | 将多个HyperLogLog合并为一个HyperLogLog |
Geo
Geo(地理信息定位):存储经纬度,计算两地距离,范围计算等
Geo是使用ZSet实现
常用命令格式 | 描述 |
---|---|
GEOADD key longitude latitude member [longitude latitude member …] | longitude:经度,latitude:维度,member:标识 |
GEOPOS key member [member …] | 返回经纬度 |
GEODIST key member1 member2 [unit] | 返回两个给定位置之间的距离 |
GEORADIUS key longitude latitude radius m | km |
ZREM key member | 删除成员 |
七、Redis持久化
RDB【Redis Database】
RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。
触发机制
-
save
手动触发,同步命令,会阻塞线程
-
bgsave
手动触发,fork出一个子进程,异步命令,不会阻塞线程【阻塞仅仅会发生在fork出子进程的阶段】
-
自动
1
2
3save 900 1 #900秒改变1个就生成rdb文件
save 300 10 #300秒改变10个就生成rdb文件
save 60 10000 #60秒改变10000个就生成rdb文件一般情况下建议关闭自动策略
-
全量复制
从节点执行全量复制操作的时候,主节点会自动触发bgsave命令生存rdb文件并发送给从节点
-
debug reload
在执行debug reload重新加载redis的时候,也会自动触发bgsave
-
shutdown
默认情况下执行shutdown命令,如果没有开启AOF持久化功能,就会自动执行bgsave
RDB配置参数
命令格式 | 描述 |
---|---|
rdbcompression | 压缩RDB文件,默认yes |
rdbchecksum | RDB文件是否进行校验,默认yes |
dbfilename dump.rdb | RDB文件名【可使用dump-端口号.rdb区分不同的redis实例】 |
dir ./ | RDB文件存储的目录 |
stop-writes-on-bgsave-error | bgsave出现错误时是否停止写入,默认yes |
AOF【Append-Only File】
AOF是一个追加写入的日志文件从而实现持久化的方式,生成的AOF文件是可识别的纯文本文件。Redis默认使用RDB持久,开启AOF持久化需要设置appendonly
为yes
AOF文件生成策略
-
always 不丢失数据,每次更新记录数据就进行io操作
-
everysec 可能会丢失1s数据,但io小
-
no 不启用AOF
AOF重写
AOF支持AOF文件重写(从内存中读取的数据,并非读取上次的AOF文件进行重写)。AOF重写可以减少硬盘占用、加速恢复速度。
AOF配置参数
命令格式 | 描述 |
---|---|
appendonly | 是否开启AOF,默认no |
appendfilename | 生成AOF文件明【可使用appendonly-端口号.aof区分不同的redis实例】 |
appendfsync | 刷盘策略,默认everysecond |
dir | 保存文件的目录,默认./ |
no-appendfsync-on-rewrite | AOF重写过程中是否禁止append操作,默认no允许append |
auto-aof-rewrite-min-size | 进行AOF时文件最小尺寸,默认64mb |
auto-aof-rewrite-percentage | 下次进行AOF操作时的增量,默认100 |
aof-load-truncated | AOF文件结尾不完整,Redis重启忽略不完整记录,默认yes |
命令
- bgrewriteaof 手动进行AOF重写操作
- aof_current_size 查看AOF当前尺寸
redis-cli info Persistence
可以查看统计信息aof_current_size
【当前AOF文件大小】和aof_base_size
【上次重写AOF文件大小】
RDB与AOF比较
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
混合持久化
Redis 4.0 开始支持 rdb 和 aof 的混合持久化(默认开启),这样做的好处是可以结合 rdb 和 aof 的优点, 快速加载同时避免丢失过多的数据。缺点 aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。
-
配置开启混合持久化
aof-use-rdb-preamble yes
-
命令开启混合持久化
config set aof-use-rdb-preamble yes
开启混合持久化时,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。aof 文件内容会变成如下
1 | +------------------------+ |
持久化相关优化
fork操作优化
-
控制redis实例最大可用内存:maxmemory
-
合理配置Linux内存分配策略:vm.overcommit_momory=1(默认0,当内存少时fork阻塞不进行)
-
降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制
子进程
-
cpu
开销:RDB和AOF文件生成,属于CPU密集型
优化:不做CPU绑定,不和CPU密集型服务部署
-
内存
开销:fork内存开销,使用了linux的copy-on-write【父进程未发生改变的内存页,不进行copy-write】
优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled【不分配大内存页】
-
硬盘
开销:AOF和RDB文件写入,可以结合iostat,iotop分析
优化:不和高硬盘负载服务部署一起,
no-appendfsync-on-rewrite
设置为yes
八、Redis主从复制原理和优化
- 一个master可以有多个slave
- 一个slave只能有一个master
- 数据流向是单向的,master到slave
主从实现两种方式
-
在从机上执行
slaveof masterIp masterPort
,此命令是异步。slaveof no one
结束从属关系。 -
修改redis配置文件
1
2slaveof masterIp masterPort #配置主从
slave-read-only yes #从节点只读
主从状态查看
info replication
全量复制和部分复制
redis4后使用psync2实现复制使redis重启也可使用部分同步,还为解决在主库故障时候从库切换为主库时候使用部分同步机制。redis从库默认开启复制积压缓冲区功能,以便从库故障切换变化master后,其他落后该从库可以从缓冲区中获取缺少的命令。该过程的实现通过两组replid、offset替换原来的master runid和offset变量实现:
-
第一组:master_replid和master_repl_offset
如果redis是主实例,则表示为自己的replid和复制偏移量; 如果redis是从实例,则表示为自己主实例的replid1和同步主实例的复制偏移量。
-
第二组:master_replid2和second_repl_offset
无论主从,都表示自己上次主实例repid1和复制偏移量;用于兄弟实例或级联复制,主库故障切换psync
判断是否使用部分复制条件:如果从库提供的master_replid与master的replid不同,且与master的replid2不同,或同步速度快于master; 就必须进行全量复制,否则执行部分复制。
以下常见的主从切换都可以使用部分复制:
- 一主一从发生切换,A->B 切换变成 B->A
- 一主多从发生切换,兄弟节点变成父子节点时
- 级别复制发生切换, A->B->C 切换变成 B->C->A
全量复制开销
- bgsave时间
- RDB文件网络时间
- 从节点清空数据时间
- 从节点加载RDB时间
- 可能的AOF重写时间
当从库与主库断开时间过长导致自己的偏移量不在master_repl_offset
允许的范围之内,会触发全量复制
主从相关参数配置
1 | ##############从库############## |
主从配置问题
maxmomory不一致导致丢失数据
数据结构参数优化只有优化了主机,从机未配置导致内存不一致,数据错误或丢失
九、Redis Sentinel
- 主观下线:Sentinel根据配置条件,发现redis节点达到故障标准,则此Sentinel认为此redis节点下线
- 客观下线:当Sentinel中认为此redis客观下线的总数达到配置阙值,则认为此节点客观下线
安装Sentinel
配置sentinel.conf文件
1 | #没有开启bind和密码的情况下,保护模式默认被开启。只接受来自环回IPv4和IPv6地址的连接。拒绝外部连接 |
Sentinel三个定时任务
- 每10秒每个sentinel对master和slave执行info
- 发现slave
- 确认主从关系
- 每2秒每个sentinel通过master节点的channel交换信息(pub/sub模式)
- 通过
__sentinel__:hello
频道交互 - 交互各节点的“看法”及自身信息
- 通过
- 每1秒每个sentinel对其他sentinel和redis节点执行ping
- 心跳检查、失败依据
Master选举过程
第一步:Sentinel选举出leader
原因:只需要一个Sentinel完成故障转移
选举:通过sentinel is-master-down-by-addr
命令都希望自己成为领导者
- 每个做主观下线的Sentinel节点向其它Sentinel节点发送命令,要求它给自己投票
- 收到命令的Sentinel节点如果没有同意其它Sentinel节点发送的命令,则同意投票否则拒绝
- 如果该Sentinel节点发现自己的票数已经超过Sentinel半数,那么它将成为leader
- 如果此过程未选出leader则等待一段时间继续选举
第二步:故障转移选举Master
-
从slave节点中选出一个“合适”的节点作为新的master节点
- 选择
slave-prority
最高的slave节点,如果存在则返回【一般不修改】 - 选择复制偏移量最大的slave节点,如果存在则返回
- 选择runId最小的slave节点
- 选择
-
对上面的slave节点执行
slaveof no one
命令让其成为master节点 -
向剩余的slave节点发送
slaveof
命令,让它们成为master节点的slave节点,复制规则和paraller-sync
参数有关 -
更新对原来master节点配置为slave,并保存对其“关注”,当其恢复后命令它去复制新的master节点
手动下线master机器
sentinel failover <masterName>
十、Redis Cluster
数据分布概论
分布方式 | 特点 | 典型产品 |
---|---|---|
哈希分布 | 数据分散度高 | |
Memcache |
Redis Cluster
|
| 键值分布业务无关 |
| 无法顺序访问 |
| 支持批量操作 |
| 顺序分布 | 数据分散度易倾斜 |
BigTable
HBase
|
| 键值分布业务相关 |
| 可顺序访问 |
| 支持批量操作 |
哈希分布方式
节点取余分区
进行取模运算,将余数相等的放入同一节点,简单易操作,增加节点时数据偏移,导致数据的前移达到80%,翻倍扩容可以使数据迁移从80%降到50%
一致性hash分区
为系统中每个节点分配一个token,范围一般在0~2的32次方,这些token构成哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点,往往一个节点会对应多个token。加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这些数据,常用于缓存场景
虚拟哈希分区
虚拟分槽使用哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。槽数范围远远大于节点数(redisCluster槽的范围是0~16383),每一个节点负责维护一部分槽以及所映射的键值数据
基本架构
-
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
-
节点的fail是通过集群中超过半数的master节点检测失效时才生效
-
客户端与redis节点直连,不需要中间proxy层.客户端连接集群中任何一个可用节点即可
-
redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->key
安装Cluster
修改redis配置文件
1 | cluster-enable yes |
原生安装
-
启动所有节点
-
执行
redis-cli -p ${port} cluster meet ${ip} ${port}
使节点相遇 -
执行
cluster addslots slot [slot...]
分配槽1
2
3
4
5
6
7
8start=$1
end=$2
port=$3
for slot in `seq ${start} ${end}`
do
echo "slot:${slot}"
/opt/redis/bin/redis-cli -p ${port} cluster addslots ${slot}
done -
执行
redis-cli -p ${port} cluster replicate ${nodeId}
执行主从分配
集群命令安装
-
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
–cluster-replicas代表集群的每个主节点的从节点个数
ruby安装已废弃
cluster配置参数
1 | cluster-enable yes |
集群伸缩
扩展集群
扩展步骤原理
- 对目标节点发送cluster setslot {slot} importing {targetNodeId}命令,让目标节点准备导入槽的数据
- 对源节点发送cluster setslot {slot} migrating {sourceNodeId}命令,让源节点准备槽数据的导出
- 源节点上循环执行cluster getkeyinslot {slot} count 命令,每次获取属于这个槽中键的个数
- 在源节点上执行migrate {sourceIp} {sourcePort} key 0 {timeout}命令,迁移指定的key
- 重复执行步骤3~4直到槽下所有数据完成迁移
- 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点
扩展执行步骤
-
加入集群
执行
redis-cli -p ${port} cluster meet ${ip} ${port}
将节点加入集群 -
设置主从关系
redis-cli -p ${port} cluster replicate ${nodeId}
设置主从关系 -
任意节点执行迁移槽命令,后续过程根据提示进行
redis-cli --cluster reshard {ip}:{port}
-
查看节点分配情况
redis-cli -p {prot} cluster nodes | grep master
集群缩容
迁移槽命令和扩展集群的迁移命令相同,迁移完成之后使用redis-cli cluster forget {downNodeId}
下线节点
-
迁移数据
redis-cli --cluster reshard {ip}:{port} --cluster-from {sourceNodeId} --cluster-to {targetNodeId} --cluster-slots {slotsNum}
-
下线节点
redis-cli --cluster del-node {ip}:{port} {shutdownNodeId}
客户端路由
使用cluster keyslot ${key}可查看key对应的hash值
moved重定向
ask重定向
smart客户端
实现原理:追求性能
- 从集群中选一个可运行节点,使用
cluster slots
初始化槽和节点映射 - 将cluster slots的结果映射到本地缓存,为每个节点创建JedisPool
- 准备执行命令(使用CRC16计算key对应的槽,找到映射节点执行)
1 | public static void main(String[] args) { |
批量操作
Redis主要提供了以下几种批量操作方式:
- 批量get/set(multi get/set)
- 管道(pipelining)
- 事务(transaction)
- 基于事务的管道(transaction in pipelining)
批量操作必须key在同一个槽,导致以上用法异常苛刻
方案一:传统的串行IO操作,也就说n个key,分n次串行操作来获取key,复杂度是o(n)
方案二:将mget操作(n个key),利用已知的hash函数算出key对应的节点,这样就可以得到一个这样的关系:Map<node, somekeys>,也就是每个节点对应的一些keys,这样将之前的o(n)的效率降低到o(node.size())
方案三:在方案二的基础上将串行取数据改为并行取数据,进一步提高效率
方案四:通过redis自带的hashtag功能,强制一批key分配到某台机器上【不建议,大量数据会造成数据倾斜】
1 | //使用{user}作为key,使key统一 |
故障转移
故障发现
- 通过ping/pong信息实现故障发现,当半数以上持有槽的主节点都标记某节点主观下线则为客观下线【向集群广播下线节点的fail消息】
- 客观下线发送通知故障节点的从节点触发故障转义流程
故障恢复
- 资格审查
- 每个从节点检查与故障主节点的断线时间
- 超过
cluster-node-timeout * cluster-slave-validity-factor
取消资格 cluster-slave-validity-factor
默认是10
- 准备选举时间
- 最接近主节点的偏移量的从节点率先发起选举,稍后其他从节点发起选举
- 选举投票
- 收集票数大于N/2+1即为选举成功
- 替换主节点
- 当前从节点取消复制变为主节点(slave no one)
- 执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己
- 向集群广播自己的pong消息,表明已经替换了故障从节点
集群运维问题
集群完整性
cluster-require-full-coverage yes
默认为yes
- 集群中16384个槽全部可用:保证集群完整性
- 节点故障或者正在故障转移,集群不可使用
大多数情况下业务无法容忍,建议cluster-require-full-coverage
设置为no
PubSub广播
任意节点发布消息所有节点都会订阅到消息,消耗带宽较多。JedisCluster只会订阅任意一个节点
数据倾斜
造成的原因:
- 节点槽分配不均
- 不同槽对应的键值数量差异较大【可能存在hashTag】
- 包含bigkey
- 内存相关配置不一致
使用
redis-cli --cluster info {ip}:{port}
可以查看key、slot分布情况使用
redis-cli --cluster rebalance {ip}:{port}
进行数据平衡【慎用】
从机读写问题
在集群模式下从节点不接受任何读写请求
- 命令会重定向到负责槽的主节点
- readonly命令可以读取【连接级别】
数据迁移
官方工具不只能从单机向集群迁移,不支持断点续传,不支持在线迁移,单线程影响速度,不建议使用官方工具
十一、缓存设计与优化
缓存使用的成本
- 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关
- 代码维护成本:多了一层缓存逻辑
- 运维成本:例如Redis Cluster
缓存更新策略
redis里面存储的过期时间,都是绝对时间点,所以如果两台机器时钟不同步,那么超过的数据会全部删除。
- slaves不会独立删除数据,而是等待master给它发送删除指令的时候,再删除数据
- 如果slave当选为master的时候,会先淘汰keys,然后再成为master
-
设置
maxmemory-policy
值指定算法更新策略 解释 volatile-lru 过期的键使用LRU策略剔除,没有可删除对象则退回到noeviction allkeys-lru 所有键均使用LRU策略剔除,直到腾出足够空间 volatile-lfu 过期的键使用LFU策略剔除,没有可删除对象则退回到noeviction allkeys-lfu 所有键均使用LFU策略剔除,直到腾出足够空间 volatile-random 过期的键使用随机策略剔除,没有可删除对象则退回到noeviction allkeys-random 所有键均使用随机策略剔除,直到腾出足够空间 volatile-ttl 剔除TTL最小的键,没有可删除对象则退回到noeviction noeviction 默认策略,不做任何事,返回写错误 LRU【Least Recently Used】最近最少被使用
LFU【Least Frequently Used】最不常用
-
被动更新
当客户端方位key的时候,主动检测这些key是否过期,过期就删除
-
主动更新
每秒检测10次以下操作,测试随机的20个keys进行相关过期检测,删除所有的过期的keys,如果有多于25%的keys过期,重复此操作
策略 | 一致性 | 维护成本 |
---|---|---|
算法剔除 | 最差 | 低 |
被动更新 | 较差 | 低 |
主动更新 | 强 | 高 |
低一致性:最大内存和淘汰策略
高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底
缓存穿透&缓存雪崩&无底洞
缓存穿透
特点:
当缓存和数据库中都没有数据的时候,当查询Redis没有数据的时候,会继续查询数据库,数据库也没有数据,当大量查询请求发生或遭到恶意攻击时,这些访问全部透过Redis,并且数据库也没有数据,这种现象称为“缓存穿透”。
解决方案:
- 缓存空对象,storage返回一个空对象,将键存储在缓存层,下次请求此键之间返回空对象
- 需要更多的键,建议设置过期时间
- 缓存层和存储层数据“短期”不一致
- 布隆过滤器
缓存击穿
特点:
当Redis的热点数据key失效时,大量并发查询直接打到数据库,此时数据库负载压力骤增,这种现象称为“缓存击穿”
解决方案:
1.设置key值永不过期
2.使用互斥锁,查到后就回填缓存
缓存雪崩
特点:
缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。
解决方案:
- 随机设置key过期时间
- 随机延时,让一部分查询先将数据缓存起来
- 设置key值永不过期
无底洞问题
现象:增加节点机器性能没提升反而下降
解决方案参考批量操作
热点key的重建优化
现象:热点key缓存重建过程过长导致浪费了不必要的资源
解决方案:
-
互斥锁【使用redis构建锁机制】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15String get(String key) {
String value = redis.get(key);
if(value == null) {
String mutexKey = "mutex:key:"+key;
if(redis.SetParams.setParams().ex(180).nx()) {
value = db.get(key);
redis.set(key,value);
redis.delete(mutexKey);
}else {
Thread.sleep(1000);
get(key);
}
}
return value;
} -
永不过期
- 缓存层面:没有设置过期时间
- 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程取构建缓存
方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 思路简单 | 代码复杂度增加 |
保证一致性 | 存在死锁的风险 | |
永不过期 | 基本杜绝热点key重建问题 | 不保证一致性 |
逻辑过期时间增加维护成本和内存成本 |
十二、Redis布隆过滤器
布隆过滤器的原理
- 首先需要k个hash函数,每个函数可以把key散列成为1个整数
- 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
- 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
- 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中
优点:不需要存储key,节省空间
缺点:算法判断key在集合中时,有一定的概率key其实不在集合中,且无法删除
google-guava库实现了java版的布隆过滤器
Redis布隆过滤器
使用插件的方式部署
redis4.0之后支持使用插件的方式使用Bloom filters和cuckoo filters,redis4.0之前需手动使用代码的方式编写布隆过滤器,安装步骤参考下列链接
建议在conf配置文件中配置,不使用redis-server --loadmodule /path/to/rebloom.so
启动
1 | loadmodule /path/to/rebloom.so |
布隆过滤器命令
命令 | 说明 |
---|---|
BF.RESERVE {key} {error_rate} {size} | 创建布隆过滤器,error_rate为错误率,size为预期数据大小 |
BF.ADD {key} {item} | 添加item到指定布隆过滤器 |
BF.MADD {key} {item} [item …] | 批量添加item到指定布隆过滤器 |
BF.EXISTS {key} {item} | 判断item是否存在与指定布隆过滤器 |
BF.MEXISTS {key} {item} [item …] | 批量判断item是否存在与指定布隆过滤器 |
JavaAPI
导入maven依赖
1 | <dependency> |
Api使用
1 | public static void main(String[] args) { |
十三、Redis开发规范
BigKey处理
发现BigKey
debug object {key}
查看指定key的详细信息redis-cli --bigkeys
扫描出BigKey【全表扫描,阻塞,建议从节点本地执行】
BigKey删除
场景:当key非常大时,delete命令执行十分缓慢,会发生阻塞【过期bigkey也是进行删除操作也会阻塞】
redis4.0之后:可以使用unlink
命令进行后台删除,不阻塞前台
生命周期管理
-
使用
OBJECT IDLETIME {key}
查看key的闲置时间 -
过期时间不易集中
命令优化
有遍历需求可以使用hscan
、sscan
、zscan
代替【这些扫描命令在field较少时COUNT参数不会生效】
必要情况下使用monitor
命令监控,注意时间不要过长
Java客户端优化
参数名 | 含义 | 默认值 | 建议 |
---|---|---|---|
testWhileIdle | 是否开启空闲资源检测 | false | true |
timeBetweenEvictionRunsMillis | 空闲资源检测周期 | -1 | 自选,也可使用JedisPoolConfig中的默认值 |
minEvictiableIdleTimeMillis | 资源池中资源最小空闲时间 | 30分钟 | 自选,也可使用JedisPoolConfig中的默认值 |
numTestsPerEvictionRun | 做空闲资源检测每次的采样数 | 3 | 自选,如果设置为-1则为全部做空闲检测 |
maxIdle需要设置为接近maxTotal
预估maxTotal方法的例子:
一次命令时间平均耗时1ms,一个连接QPS大约1000,业务期忘的QPS时50000,理论上maxTotal=50000/1000=50
十四、内存管理
内存消耗
内存统计
执行info memory
命令可以查看内存信息
主要属性名 | 属性说明 |
---|---|
used_memory | 实际存储数据的内存总量 |
used_memory_rss | redis进程占用的总物理内存 |
maxmemory | 最大内存 |
maxmemory_policy | 内存剔除策略 |
mem_fragmentation_ratio | used_memory_rss/used_memory比值,表示内存碎片率 |
内存消耗划分
-
客户端缓冲区设置规则
client-output-buffer-limit
:客户端类型,分为三种 (a)normal:普通客户端
(b)slave:用从节点用于复制,伪裝成客户端
©pubsub:发布订阅客户端
:如客户使用的输出冲区大于hardlimit客户端会被立即关闭 和 :如果客户端使用的输出缓冲区超过了 并且持续了 ,客户会被立即关闭 - 普通客户端默认:client-output-buffer-limit normal 0 0 0
- salve客户端默认:client-output-buffer-limit slave 256mb 64mb 60
- pubsub客户端默认:client-output-buffer-limit slave 32mb 8mb 60
-
复制缓冲区:用于slave和master断开重连时不进行全量复制保存偏移数据使用
默认为
repl-backlog-size 1mb
-
AOF缓冲区:AOF重写期间,AOF缓冲区,没有容量限制
内存回收策略
删除过期值
- 惰性删除:访问key->expired dict->del key【先在过期表中找,发现过期删除key,返回null】
- 定时删除:每秒运行10次,采样删除
超过maxmemory使用maxmemory-policy进行控制,参见缓存更新策略
十五、开发运维事项
Linux内核优化
vm.overcommit_memory
Redis建议vm.overcommit_memory = 1(影响fork操作)
立即生效:
永久生效:vm.overcommit_memory = 1写入到/etc/sysctl.conf文件中
值 | 含义 |
---|---|
0 | 表示内核将检查是否有足够可用的内存。如果有足够可用的内存,内存申请通过,否则内存申请失败,并返回错误给进程 |
1 | 表示内核允许超量使用内存直到用完为止 |
2 | 表示内存绝不过量使用,即整个系统内存不能超过swap+50%的RAM值 |
swappiness
值 | 策略 |
---|---|
0 | Linux3.5及以上:宁愿OOM Killer也不用swap |
Linux3.5及以下:宁愿swap也不用OOM Killer | |
1 | Linux3.5及以上:宁愿swap也不用OOM Killer |
60 | 默认值 |
100 | 操作系统会主动使用swap |
建议:Linux3.5以上vm.swappiness = 1,否则vm.swappiness = 0
立即生效:echo {bestValue} > /proc/sys/vm/swappiness
永久生效:vm.swappiness = {bestValue} 写入到/etc/sysctl.conf
THP(Transparent huge page)
建议禁用,Centos7在/sys/kernel/mm/transparent_hugepage/enabled
下设置为never即可
THP为大内存页时fork子线程时Copy-On-Write可能造成延迟
ulimit
建议将Redis启动用户的文件句柄限制调成10032,限制文件etc/security/limits
TCP backlog
建议将/proc/sys/net/core/somaxconn
系统TCP backlog的限制设置为与Redis一样默认511
Redis安全问题
在配置文件中配置
1 | #将命令更改为另一个字符串,原命令失效。如果字符串为空则表示禁用此命令 |
附各种数据类型的内部编码:
附Redis云平台CacheCloud【一键部署、监控、运维、数据迁移工具等】 GitHub项目地址