Redis 分布式实践
一. Redis 初识
1.1 Redis 导学
-
Redis有哪些特点?
- 高性能的key-value服务器
- 多种数据结构
- 丰富的功能
- 高可用分布式支持
-
适合人群:
- 初学者
- 进阶者
- 希望了解企业级开发Redis的同学
-
技术储备:
- 了解Linux命令的基本使用
- 了解常用数据结构
- 了解一门编程语言
-
授课思路:
-
课程目标
- 全面了解Redis单机的相关功能
- 全面了解Redis高可用和分布式方案
- 理解企业级Redis的开发运维
1.2 Redis 初识
-
目录:
- Redis 是什么
- Redis 的特性回顾
- Redis 单机安装
- 使用场景
-
Redis是什么?
1.3 谁在使用Redis
应该问,还有谁没有使用过Redis?它已经深入到我们的方方面面,众多大厂都在使用它。
1.4 redis特性目录
- 速度快、持久化、多种数据结构、支持多种编程语言、功能丰富、简单、主从复制、高可用、分布式
主从复制是高可用的基础。
1.5 特性1-速度快
-
支持10W OPS的读写
-
内存处理
-
不同种类的存储设备的读写区别:
1.6 特性2-持久化(断电不丢数据)
- Redis所有数据保持在内存中,对数据的更新将异步地保存在磁盘上。
1.7 特性3-数据结构
除了上述的几种数据结构外,Redis还支持BitMaps(位图)、HyperLogLog(超小内存唯一值计数)、GEO(地理信息定位)
1.8 特性4 多语言客户端
- 支持Java、PHP、Ruby、Lua、Node、C#…
1.9 特性5 功能丰富
通过它的特性可以实现很多的功能,满足各种不同的场景,使用的好的话,它就像一把瑞士军刀一样短小精悍!
1.10 特性6 简单
- 代码短小精悍,初始时只有2万三千来行代码,有助于我们学习源码和深入理解Redis,并可以进行再封装和修改。
- 不依赖外部库
- 单线程模型
1.11 特性7 主从复制
1.12 特性8-高可用分布式
1.13 redis典型使用场景
- 场景有:
- 缓存系统
- 计数器
- 消息队列系统
- 排行榜
- 限流
- 事务
- 通知
- 等等
1.14 redis 三种启动方式介绍
-
安装教程:
- 下载:
wget http://download.redis.io/releases/redis-3.0.7.tar.gz
- 解压:
tar -xzf redis-3.0.7.tar.gz
- 建立软连接,对于后期升级是非常方便的:
In -s redis-3.0.7 redis
- 进入目录:
cd redis
- 编译和安装:
make && make install
- 使用redis-server相关的命令 可以启动服务器
- 使用redis-cli相关的命令 来连接Redis命令行客户端
- 使用redis-benchmark 相关的命令来做基准测试和性能测试
- 使用redis-check-aof 相关的命令来做AOF文件修复
- 使用redis-check-dump 相关的命令来做RDB文件检查
- redis-sentinel 相关命令来启动Sentinel服务器(2.8以上)
-
Redis安装(windows)
-
三种启动方法:
-
最简启动
- 直接执行
redis-server
- 它使用的是默认配置
-
动态参数启动:
- 命令:
redis-server --port 5380
-
配置文件启动
- 命令:
redis-server configPath
通过配置文件来进行启动,我们将配置都写在这个配置文件中,通过传入配置文件路径来执行配置文件内的内容
-
比较
- 生成环境建议使用配置启动(可能会有多个实例,我们可以使用配置文件,更好维护)
- 单机多实例配置文件可以用端口区分开
-
验证是否启动的命令:
- 方法1:
ps -ef | grep redis
- 方法2:
netstat -antpl | grep redis
- 方法3:
redis-cli -h ip -p port ping
-
Redis客户端连接
-
Redis客户端返回值
1.15 redis 常用配置
- 配置有哪些?
Redis 的守护进程默认是关闭的,建议选为Yes,这样能够打印日志。对外端口号默认是6379;工作目录关系到日志文件和持久化文件存在于哪个目录中;
- 为什么取6379?
- 使用
config get *
命令可以显示有多少个配置,图示如下:
1.16 redis安装启动演示
二. API的理解和使用
2.1 课程目录
- 图示如下:
2.2 通用命令
-
通用命令:
-
keys:
keys 命令一般不在生成环境使用,它是一个重命令,数据量大会非常慢,且会阻塞其他命令,如果真的有这样的需求可以使用其他命令替代;
keys* 怎么使用? 1. 热备从节点 2. scan
-
dbsize
线上可以使用
-
exists
一般线上可以使用,注意一些特殊场景
-
del
-
expire、ttl、persist
ttl可以用来观测过期时间,而persist 命令可以移除掉过期时间限制,它就会没有过期时间,即永不过期。有过期时间且未过期的返回值大于0,没有过期时间永不过期的数据等于-1,如果为-2说明设置了过期时间且数据已经过期。
-
type
-
时间复杂度
通过时间复杂度的统计,我们可以清晰地认识到哪些命令可以在生产环境中使用,哪些命令的大致执行耗时等情况
2.3 数据结构和内部编码
-
数据结构及编码:
以空间换时间的话,我们可以用hash结构,使用更小的空间达到效果;它内部经过了压缩
-
redisObject
2.4 单线程
-
图示:
- 一瞬间redis只会执行一条命令
-
redis为什么这么快?
- 纯内存
- 非阻塞IO
- 避免线程切换和竞态消耗
第一条是主要原因,第二条和第三条是辅助。
-
redis的单线程需要注意什么?
- 一次只运行一条命令
- 拒绝长(慢)命令:
- keys,flushall,flushdb,slow lua script,mutil/exec,operate big value(collection)
- 它使用的是单线程模型,但是它的单线程是相对的。一些其他的东西也会另外使用线程,比如异步的回写磁盘、fysnc file descriptor、close file descriptor
2.5 字符串
2.6 hash
2.7 list
-
列表结构:
-
特点:有序、可以重复、左右两边插入弹出
-
rpush:
-
lpush:
-
linsert
-
lpop:
执行lpop 后,就会弹出a ,然后list内的结果为: bcd
-
rpop:
l是从左边处理(left),r是右边处理(right),所以此处是右边弹出最后一个值,则结果为abc,被弹出的是d
-
lrem:
- 执行全部删除命令
lrem listkey 0 a
此处count 是0 说明删除全部包含a的,所以执行后图示内容只剩下ccbf
- 继续执行删除一条数据的命令
lrem listkey -1 c
因为此处的count 是-1 则只会删除一条符合条件的数据,且从右侧开始,所以最终结果从ccbf => cbf
-
ltrim:
1 和4 代表起始位置和结束的索引位置,所以bcde会被保留,其他则被舍弃,结果为 bcde
在大数据量下性能相对较好,推荐使用,比删除性能更优
-
lrange:
源数据不会发生改变,这里只是获取指定范围的数据,不对数据本身进行操作
-
lindex:
-
llen:
-
lset:
- 执行lset 操作,会修改执行索引处的值。比如此处
lset listkey 2 java
则将图中的c变成了java,最终结果为:abjavadef
-
演示:
-
实战:
通过时间排序存入,然后获取最新的微博可以拿取0~10 索引的数据;
-
blpop/brpop
-
通过List的一些操作可以实现一些数据结构:
- Stack = LRUSH + LPOP
- Queue = LPUSH + RPOP
- Capped Collection = LPUSH + LTRIM
- Message Queue = LPUSH + BRPOP
2.8 set
- 集合结构:
- Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
- 集合对象的编码可以是 intset 或者 hashtable。
- Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
- 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
- 特点:无序、唯一、支持集合间操作(交集、并集等)
- 集合内Api,所有Api都是以S开头:
- sadd srem
- API:
- scard sismember srandmember smembers
- API:
- smembers:(无序、数据量很多的情况下要小心使用)
- srandmember 和 spop 区别: spop 从集合弹出,而srandmember 不会破坏集合
- 演示:
- 实战案例:
- 赞、踩的功能:
- 图示:
- 可以使用其他,当然也可以使用set结构来进行实现
- 为指定事务附上标签(tag)
- 图示:
- 下面为一些集合间的api操作:
- sdiff sinter sunion
- 实战:
- 一些命令对应一些实战案例:
- SADD => Tagging
- SPOP/SRANDMEMBER => Random item
- SADD + SINTER => Social Graph
2.10 zset
三. Redis客户端的使用
3.1 Java 客户端:Jedis
-
获取Jedis
-
Jedis基本使用:
- string:
- hash:
- list:
- set:
- zset:
-
Jedis连接池使用:
-
简单使用:
3.2 Go客户端:redigo简介
-
官网下载:
-
简单使用-连接:
-
简单使用-命令:
做一个简单介绍,具体开发可以参考官方文档
3.3 Jedis配置优化
四. 瑞士军刀Redis其他功能
4.1 慢查询
-
生命周期:
-
两个配置:
- slowlog-max-len
- 先进先出队列
- 固定长度
- 保存在内存中
- 如图所示:
- slowlog-log-slower-than
- 慢查询阀值(单位:微秒)
- slowlog-log-slower-than=0,记录所有命令
- slowlog-log-slower-than < 0, 不记录任何命令
-
配置方法:
- 默认值:
config get slowlog-max-len = 128
config get slowlog-log-slower-than= 10000
- 修改配置文件重启
- 动态配置
config set slowlog-max-len 1000
config set slowlog-log-slower-than 1000
-
慢查询命令
- slowlog get [n] :获取慢查询队列
- slowlog len :获取慢查询队列长度
- slowlog reset :清空慢查询队列
-
运维经验
- slowlog-max-len 不要设置过大,默认10ms,通常设置1ms
- slowlog-log-slower-than 不要设置过小,通常设置1000左右
- 理解命令生命周期
- 定期持久化慢查询
4.2 pipeline
-
1次网络命令通信模型:
-
批量网络命令通信模型:
-
什么是流水线?
- pipeline 是Redis 的一个提高吞吐量的机制,适用于多key读写场景,比如同时读取多个key的value,或者更新多个key的value。
- Redis本身是基于Request/Response协议(等停机制)的,正常情况下,客户端发送一个命令,等待Redis返回结果,Redis接收到命令,处理后响应。在这种情况下,如果同时需要执行大量的命令,那就是等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip), 而且还频繁调用系统IO,发送网络请求。为了提升效率,这时候pipeline出现了,它允许客户端可以一次发送多条命令,而不等待上一条命令执行的结果,这和网络的Nagel算法有点像(TCP_NODELAY选项)。
- pipeline不仅减少了RTT,同时也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)。
-
流水线的作用?
- 两点注意:
- Redis 的命令时间是微秒级别
- pipeline每次的条数要控制(网络)
使用流水线可以类似管道的作用,同时减少了重复连接和网络耗时,在大量命令同时执行的情况下,可以极大地减少执行耗时,提高性能;但应注意大量命令的条数,如果命令过多,导致慢查询,其他的命令因此而陷入等待,可能会引发其他的问题。
-
性能对比:
-
使用建议:
- 注意每次pipeline携带数据量
- pipeline每次只能作用在一个Redis节点上
- M操作与pipeline区别(mset、mget等操作是原子性操作,一次m操作只返回一次结果;pipeline非原子性操作,只是将N次命令打个包传输,最终命令会被逐条执行,客户端接收N次返回结果。)
4.3 发布订阅
- 模型:
Redis 不支持消息堆积,故不支持获取历史消息;只能收取到关注的频道,如果没有关注则不会收到。
- publish(发布命令)
- subscrbe(订阅)
- unsubscribe(取消订阅)
- 其他API:
- psubscribe [pattern…] #订阅模式
- punsubscribe [pattern…] #退订指定的模式
- pubsub channels #列出至少有一个订阅者的频道
- pubsub numsub [channel…] #列出给定频道的订阅者数量
- pubsub numpat #列出被订阅模式的数量
- 与发布订阅不同的是,消息队列是一个抢的模型,就类似于抢红包,只有一个人能够抢到。而发布订阅更像是长辈发红包,每个小孩子都能收到。
4.4 bitmap
-
位图:
-
什么是位图?
- 先举出一个问题:如果要简单做一个签到功能,可能最简单粗暴的方式就是,插入数据库,签到一次插入一条数据。那么问题来了,一个人一年会有365条 或者366条数据,而一个小部门一年就会有成千上万条数据。同时数据插入的时候,需要用upsert操作,也是需要消耗资源的。因此这种做法是不可取的,如果先从数据库里面查询今天签到没有,再进行签到呢?那这个操作不是原子性的,可能会导致线程安全问题,一个人可能最后可以签到多次,另外需要查询,也需要插入,分为俩次操作。
- 此外还有一个问题,查询一个一年签到多少次,怎么办?数据库里面做一个聚合查询,也可以做到这个,但是数据量本身很大的情况下,会导致并不快。每个人的某一年的数据,从字面上来看,需要根据人以及年份做聚合,数据量小的时候确实无关紧要,但是数据量一大,数据库的索引本身很大的情况下,就显得效率不怎么高了。
- 那么怎么样做这个操作比较好呢?
- 计算机里面数据结构的最小单位就是bit了,如果一个人一天的签到,占一个位的话,那么一年的签到,会占365或者366个位,一条记录大概是46个字节,就可以满足了。当然这里是以年为单位的,若以月为单位会更小。
- 另外签到其实就将该位的数据改为1,不存在线程安全问题,哪怕重复签到。同时,当统计一个人一年签到多少次的时候,直接使用redis的bitCount指令即可。不用像数据库一样通过聚合数据来查询。
-
位图定义?
- 位图并不是一种数据结构,其实就是一种普通的字符串,也可以说是byte数组。基本语法是setbit/getbit,刚才说了是一个byte数组,所以也可以用set/get设置或获取。
- setBit语法: Setbit KEY_NAME OFFSET
- getBit语法: Getbit KEY_NAME OFFSET
-
直接去操作位
-
setbit:
- 执行完上图中的命令后,就类似达成下图中的效果,索引0 是1 ,索引5是1…
-
getbit:
-
bitcount:
-
bitop:
-
bitpos:
-
实战:
- 独立用户统计:
- 使用set 和 Bitmap:
- 只有10万独立用户数量呢?
- 使用经验:
- type = string, 最大512MB
- 注意setbit时的偏移量,可能有较大耗时
- 位图不是绝对好,注意其优缺点,在合适的场景使用
4.5 hyperloglog
-
hyperloglog是什么?
- 基于HyperLogLog算法:极小控件完成独立数量统计
- 本质还是字符串:
- 执行命令
type hyperloglog_key
后,返回结果为:string
说明它的类型本质是String
-
API:
- 向hyperloglog添加元素:
pfadd key element [element...]
- 计算hyperloglog的独立总数:
pfcount key [key...]
- 合并多个hyperloglog:
pfmerge destkey sourcekey [sourcekey ...]
-
演示:
-
内存消耗(百万独立用户)
-
使用经验:
- 是否能容忍错误?(错误率:0.81%)
- 是否需要单条数据?
- 是否需要很少的内存去解决问题?
4.6 geo
- GEO是什么? 用于记录位置信息:
- 应用场景:
- geoadd:
- geopos:
- geodist:
- georadius:
- 相关说明:
- since 3.2+
- type geoKey = zset
- 没有删除API: zrem key member
五. Redis持久化的取舍和选择
5.1 持久化的作用
-
什么是持久化?
- Redis所有数据保存在内存中,对数据的更新将异步地保存到磁盘上。
-
持久化的实现方式
- 快照:
- 关于指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。
- 快照的作用主要是能够进行在线数据备份与恢复。当存储设备发生应用故障或者文件损坏时可以进行快速的数据恢复,将数据恢复某个可用的时间点的状态。
- 快照的另一个作用是为存储用户提供了另外一个数据访问通道,当原数据进行在线应用处理时,用户可以访问快照数据,还可以利用快照进行测试等工作。所有存储系统,不论高中低端,只要应用于在线系统,那么快照就成为一个不可或缺的功能。
MySQL Dump / Redis RDB
- 写日志:将每次操作的语句存储到日志中,如果需要进行数据回滚时,则将源数据依照命令顺序执行一次即可;
MySQL Binlog/kHbase HLog/Redis AOF
5.2 RDB
-
什么是RDB?
- RDB就是输入一段命令,这段命令将某一时刻的数据生成为一个RDB文件,然后存储在硬盘中。当我们需要恢复到那个时间段的数据时只需要执行启动载入命令即可将RDB内的数据恢复到Redis中;
-
触发机制-主要三种方式
- save(同步):
- 图示:
直接执行save即可,但是有一个问题,它是同步命令,大批量数据下可能会造成redis的阻塞.
- 文件策略:
- 如存在老的RDB文件,新替换老;
- 复杂度: O(N)
- bgsave(异步)
- 图示:
它是创建了一个fork子进程去执行,是异步的;
- save 与 bgsave的比较:
|命令|save|bgsave|
|-|-|-|
|IO类型|同步|异步|
|阻塞?|是|是(阻塞发生在fork)|
|复杂度|O(n)|O(n)|
|优点|不会消耗额外内存|不阻塞客户端命令|
|缺点|阻塞客户端命令|需要fork,消耗内存|
- 自动生成RDB:
- 通过设置配置信息,当changes(多少条数据被改变了)到了指定条数和时间,则进行生成RDB文件
- 缺点:无法控制RDB保存时间
- 如何关闭RDB?
- 最佳配置:
- 图示
- 说明:
- 一般关闭save自动配置
- 第二个设置是为了避免文件重复覆盖,为生成的RDB文件设置文件名称
- 第三个bigdiskpath 放一个很大硬盘的目录,可能后期会进行分盘
- 第四个是发生错误时停止写入,因为发生错误了redis也不能写入
- rdbcompression 是压缩配置,默认开启
- rdbchecksum 这个命令意思是是否对rdb文件进行检验,如果选择yes则表示对其检验。
-
触发机制-不容忽略方式 & 自动触发
- 全量复制:从节点执行全量复制的操作的时候,主节点会自动触发bgsave命令生成rdb文件并发送给节点
- debug reload:在执行debug reload(这个时候redis实例的run id 不会发生变化)重新加载redis的时候,也会自动触发bgsave;
- shutdown:默认情况下执行shutdown 命令,如果没有开启AOF持久化功能,就会自动执行bgsave.
-
RDB持久化的优缺点:
- 优点:
- 非常适合备份,全量复制等场景
- redis加载RDB恢复数据比使用AOF方式更快
- 缺点:
- 没有办法做到实时/准实时的持久化
- 因为RDB文件是一个压缩过的二进制文件,在redis的版本演进过程中,存在多个格式的RDB格式,因此存在老版本的redis不能完全兼容RDB新版格式的情况;
-
RDB总结:
- RDB是Redis内存到硬盘的快照,用于持久化
- save命令通常会阻塞Redis(线上或大数据量情况下慎用)
- bgsave不会阻塞Redis,但是会fork新进程
- save自动配置满足任一就会被执行。(一般情况下我们不会使用自动配置,因为它的RDB写入时间不可控,而生成RDB文件会消耗资源,是一个隐患。)
- 有些触发机制不容忽视。
5.3 AOF
5.4 RDB和AOF抉择
- RDB与AOF比较
命令 |
RDB |
AOF |
启动优先级 |
低 |
高 |
体积 |
小 |
大 |
恢复速度 |
快 |
慢 |
数据安全性 |
丢数据 |
根据策略决定 |
轻重 |
重 |
轻 |
RDB是一种快照形式,能够直接将某一时刻的完整数据保存;它能够很快的恢复,但是保存时却很耗费性能;而AOF相反,它是部分数据一直累计写入,恢复时很慢,保存时基本不耗费性能;我们可以根据实际场景来使用,也可以结合使用;
六. 常见的持久化开发运维问题
6.1 fork
- fork操作:
- 同步操作(fork与主线程是同步的,当初始化资源后不是了)
- 与内存量息息相关:内存越大,耗时越长(与机器类型越长)
- 查询持久化的执行时间:info:latest_fork_usec
- 改善fork:
- 优先使用物理机或者高效支持fork操作的虚拟化技术
- 控制redis实例最大可用内存:maxmemory
- 合理配置Linux内存分配策略:vm.overcommit_memory=1
- 降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制
6.2 子进程开销和优化
-
子进程开销和优化
- CPU:
- 开销:RDB和AOF文件生成,属于CPU密集型
- 优化:不做CPU绑定,不和CPU密集型部署
- 内存:
- 开销:fork内存开销,copy-on-write
- 优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled
- 硬盘:
- 开销:AOF和RDB文件写入,可以结合iostat,iotop分析
-
硬盘优化:
- 不要和高硬盘负载服务部署一起:存储服务、消息队列
- no-appendfsync-on-rewrite=yes
- 根据写入量决定磁盘类型:例如ssd
- 单机多实例持久化文件目录可以考虑分盘
6.3 AOF阻塞
- AOF追加阻塞:
- 为了保证AOF每秒刷盘、文件的实时性,会进行一个检测,如果同步时间小于2秒,则进行通过,如果大于2秒则会陷入阻塞等待同步线程执行完毕;
- AOF阻塞定位:
- 通过Redis日志进行分析,如:
- info Persistence:
- 通过硬盘,当硬盘资源比较紧张的时候,可能会发生阻塞,所以当发生阻塞的时候可以看看硬盘资源是否充足:
七. Redis复制的原理与优化
7.1 什么是主从复制
- 关注主从复制之前,我们看一下单机存在的问题:
- 机器故障: 如果一台机器发生了故障,整个服务可能会因此陷入瘫痪
- 容量瓶颈:单机容量有限
- QPS瓶颈:单机的QPS有限
- 主从复制的作用:
- 主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是实时的业务数据库,从数据库的作用和使用场合一般有几个:一是作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作;二是可在数据库作备份、数据统计等工作,这样不影响主数据库的性能;
两边的数据一样;
- Redis的主从模型支持一主一从,也支持一主多从,如图所示:
- ![在这里插入图片描述](https://img-blog.csdnimg.cn/1185829cb41d4051857f032f50207d3c.png?x-oss-process=image/ watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5pqX5L2Z,size_20,color_FFFFFF,t_70,g_se,x_16)
- 示例:
- 经过主库对从库的复制,我们在主库中写入的数据,可以在从库中读取到;
- 主从复制的作用:
- 提供了数据副本
- 扩展了读性能
- 总结:
- 一个master可以有多个slave
- 一个slave只能有一个master
- 数据流向是单向的,从master 到slave
7.2 主从复制配置 - 介绍
7.3 主从复制配置 - 操作
- 配置较繁琐,建议实际生产环境结合实际来进行设置,暂略。
7.4 runid 和复制偏移量
7.5 全量复制
- 全量复制:master文件会全部同步到slave,然后同步完成后再通过偏移量对比,再将增量的数据进行同步;
- 操作如下:
psync ? -1
: 同步命令,可以完成全量复制、部分复制的功能;
- +FULLRESYNC {runId} {offset} 得到具体redis实例的runid的偏移量;
- 如图所示:
7.6 全量复制开销 + 部分复制
- 全量复制开销如下:
- bgsave时间
- RDB文件网络传输时间
- 从节点清空数据时间
- 从节点加载RDB的时间
- 可能的AOF重写时间
- 部分复制:
- 如果发生类似抖动的时候,可以将复制的损失降低到最低;
- 第一步,出现连接丢失
- 第二步,复制缓冲区
- 第三步再次连接
- 第四步告诉偏移量
- 如果错过很多数据,则会全量复制,如果不多则直接发送数据
- 后面步骤则进行传输,写入主库的数据;
7.7 故障处理
- 主从结构-故障自动转移:
- slave发生故障,图示:
- master发生故障,图示:
- 主从复制故障转移问题:
7.8 主从复制常见问题
- 开发与运维中的问题:
- 读写分离:
- 读流量分摊到从节点。
- 类似:
- 可能絮叨问题:
- 复制数据延迟
- 读到过期的数据
- 过期策略:懒惰删除
- 采样速度
- salve 不能处理数据,故可能会读到脏数据,redis 3.2中已经解决了这个问题;
- 从节点故障
- 主从配置不一致:
- 例如maxmemory 不一致:丢失数据
- 例如数据结构优化参数(例如 hash-max-ziplist-entries):内存不一致
- 规避全量复制
-
第一次全量复制:
- 第一次不可避免
- 小主节点、低峰
-
节点运行ID不匹配:
- 主节点重启(运行ID变化)
- 故障转移,例如哨兵或集群
-
复制积压缓冲区不足:
- 网络中断,部分复制无法满足
- 增大复制缓冲区配置rel_backlog_size,网络“增强”。
- 规避复制风暴(master挂了,他有很多从节点,让它挂了重启后,可能会与多个从节点进行复制,导致了复制风暴,消耗大量性能)
- 单节点复制风暴:
- 问题:主节点重启,多从节点复制
- 解决:更换复制拓补
- 单机器复制风暴:
- 如右图:机器宕机后,大量全量复制
- 主节点分散多机器
八. Redis Sentinel
8.1 sentinel 目录
- 主从复制高可用
- 架构说明
- 安装配置
- 客户端连接
- 实现原理
- 常见开发运维问题
8.2 主从复制高可用?
-
作用:
- 它为主提供了一个备份
- 减轻主节点压力,实现了分流。
-
主从复制问题:
-
一主两从架构问题:
- 当master宕机后,我们要选中一个客户端,然后执行saelof no one ,使他成为一个master节点,然后再其他从节点saveof new master ,连接新的master节点;可以使用脚本来实现,但是较为复杂;
- Redis Sentinel为我们提供了这样一个方便的高可用功能;
8.3 redis sentinel 架构
- 它的结构还是主从结构。然后还有一个Sentinel 节点,能够完成故障判断、故障转移、故障通知的功能;而且Sentinel 实现了高可用,一台挂了也不影响;客户端从sentinel获取redis信息;
- 故障时的处理逻辑:
- 多个sentinel发现并确认master有问题
- 选举出一个sentinel作为领导
- 选出一个slave作为master
- 通知其余slave成为新的master 的slave
- 通知客户端主从变化
- 等待老的master复活成为新master的slave
8.4 redis sentinel 安装与配置
- 配置开启主从节点
- 配置开启sentinel监控主节点(sentinel是特殊的redis,sentinel默认端口是26379)
- 实际应该多机器
- 配置节点概要:
- redis主节点:
- redis从节点:
- sentinel主要配置:
8.5 redis sentinel 安装
8.6 java 客户端
- 客户端的实现基本原理:
- 第一步的时候,一般会连接Sentinel,获取到Sentinel集合,然后选择一个可用的Sentinel节点
- 第二步的时候,会根据Sentinel返回的redis信息,拿到master节点;
- 第三步的时候,会进行验证,验证返回的master节点此时是否为真正的master节点
- 第四步的时候,Sentinel与客户端会有一个发布订阅的模式,当master节点发生了变化后,会通知客户端连接新的节点;
客户端的原理都是类似的,无论什么语言,基本上都是这样子来进行实现;
- 客户端接入流程:
- Sentinel地址集合
- masterName
- 不是代理模式:
- jedis使用sentinel示例:
8.7 主观下线和客观下线
- 主观下线:每个sentinel 节点对Redis 节点失败的“偏见”
- 客观下线:所有Sentinel节点对Redis节点失败“达成共识”(超过quorum个统一)
8.8 领导者选举
- 原因:只有一个sentinel节点完成故障转移
- 选举:通过sentinel is-master-down-by-addr命令都希望成为领导者
- 收到命令的Sentinel节点如果没有同意通过其他Sentinel节点发送的命令,那么将统一该请求,否则拒绝;
- 如果该Sentinel节点发现自己的票数已经超过Sentinel集合半数且超过quorum,那么它将成为领导者。
- 如果此过程有多个Sentinel节点成为了领导者,那么将等待一段时间重新进行选举。
8.9 故障转移
- 故障转移(sentinel领导者节点完成)
- 从slave节点选出一个“合适的”节点作为新的master节点
- 对上面的slave节点执行slaveof no one命令让其成为master节点。
- 向剩余的slave节点发送命令,让它们成为新master节点的slave节点,复制规则和parallel-syncs参数有关;
- 更新对原来master节点配置为slave,并保持着对其“关注”,当其恢复后命令它去复制新的master节点。
- 选择“合适的”slave节点的规则
- 选择slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续。
- 选择复制偏移量最大的slave节点(复制的最完整),如果存在则返回,不存在则继续。
- 选择runId最小的slave节点;
8.10 节点运维
-
出现原因:
- 机器下线:例如过保等情况
- 机器性能不足:例如CPU、内存、硬盘、网络等
- 节点自身故障:例如服务不稳定等
-
节点下线:
- 从节点:临时下线还是永久下线,例如是否做一些清理工作,还是要考虑读写分离的情况;
- Sentinel节点:同上;
-
节点上线:
- 主节点:sentinel failover进行替换
- 从节点:slaveof即可,sentinel节点可以感知
- sentinel节点:参考其他sentinel节点启动即可
8.11 本章总结
- Redis Sentinel 是Redis的高可用实现方案,它具备:故障发现、故障自动转移、配置中心、客户端通知的功能
- Redis Sentinel 从Redis 2.8版本开始才正式生产可用,之前版本生产不可用;
- 尽可能在不同物理机上部署Redis Sentinel 所有节点。
- Redis Sentinel 中的Sentinel节点个数应该为大于等于3且最好为奇数。
- Redis Sentinel中的数据节点与普通数据节点没有区别。
- 客户端初始化时连接的是Sentinel节点集合,不再是具体的Redis节点,但Sentinel只是配置中心不是代理。
- Redis Sentinel 通过三个定时任务实现了Sentinel节点对于主节点、从节点、其余Sentinel节点的监控。
- Redis Sentinel在对节点做失败判定时分为主观下线和客观下线。
- 看懂Redis Sentinel 故障转移日志对于Redis Sentinel 以及问题排查非常有帮助。
- Redis Sentinel 实现读写分离高可用,可以依赖Sentinel节点的消息通知,获取Redis 数据节点的状态变化。
九. 初识Redis Cluster
9.1 呼唤集群
- 为什么呼唤?
1.并发量:单个可能10w/s ,使用集群可以支撑100w/s 的并发量
2.数据量:单个可以支持16G~256G的数据量,集群可以支撑500G + 的数据量。
3.网络流量的需求,等等。
我们可以使用更强悍的机器,使用超大内存,超牛的CPU,但是单机机器始终有性能上限,且单机成本昂贵性价比不高;使用集群可以解决这些问题。
- Redis Cluster is released in 3.0
9.2 数据分布概论
-
分布式数据库-数据分布:
-
分区方式:
-
数据分布对比:
分布方式 |
特点 |
典型产品 |
哈希分布 |
数据分散度高,键值分布与业务无关,无法顺序访问,支持批量操作 |
一致性哈希Memcache Redis Cluster 其他产品 |
顺序分布 |
数据分散度易倾斜 键值业务相关 可顺序访问 支持批量操作 |
BigTable HBase |
-
哈希分布:
9.3 节点取余分区
-
图示:
-
多倍扩容:
-
节点取余的优缺点:
- 简单,它是基于客户端分片:哈希+取余
- 节点伸缩:数据节点关系变化,导致数据迁移
- 迁移数量和添加节点数量有关:建议翻倍扩容
9.4 一致性哈希分区
- 一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形);下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。
- 特点:
- 一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
- 另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。
9.5 虚拟槽哈希分布
- 特点:
- Redis Cluster 使用的分区方式
- 预设虚拟槽:每个槽映射一个数据子集,一般比节点数大。
- 良好的哈希函数:例如CRC16
- 服务端管理节点、槽、数据
- 图示:
9.6 基本架构
- 单机架构:
- 分布式架构:
- Redis Cluster架构
- 图示:
- 节点:
它有一个配置叫: cluster-enabled:yes 配置是否以节点方式启动
- meet:
- 每个节点都可以互相知道彼此存在,且能互相通信:
- 指派槽:
- 对于客户端:
计算key,计算槽的位置
9.7 原生安装
- 两种安装方式:
- 原生命令安装-理解架构
- 配置开启节点
- meet
- 指派槽
- 主从关系分配
- 步骤:
- 配置开启redis:
配置好了就每个服务器运行并加载对应的conf文件
- meet-让每个节点间相互通信
- Cluster节点主要配置
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file "nodes.conf"
cluster-require-full-coverage yes
- 分配槽:
- 设置主从
9.8 原生安装-1-准备节点
-
创建单个文件的配置:
-
copy出五个节点需要的配置:
-
启动7000节点redis,然后查看是否成功启动:
-
启动五个节点信息
-
查看所有redis进程,观察是否都启动成功
-
查看单节点配置信息
-
查看单节点信息:
9.9 原生安装-2-节点握手
- 7000节点连接7001节点
第一个命令:redis-cli -p 7000 cluster meet 7001 ,可以将7000节点与7001节点握手。 第二个命令: redis-cli -p 7000 cluster nodes 是查看7000节点所属的集群信息
- 以此类推,一直到所有的节点互通完成:
redis-cli -p 7000 cluster meet 127.0.0.1 7002
redis-cli -p 7000 cluster meet 127.0.0.1 7003
redis-cli -p 7000 cluster meet 127.0.0.1 7004
redis-cli -p 7000 cluster meet 127.0.0.1 7005
- 查询节点信息,发现cluster-known-nodes = 6 说明我们全部节点已经连接成功:
9.10 原生安装-3-分配槽
-
分配槽我们可以写一个简单的脚本来分配槽
-
执行脚本:
- 执行7000:
-
sh addslots.sh 0 5461 7000
-
验证7000 的执行脚本是否执行成功:
看出已经分配了5462个槽,说明执行成功;
-
在集群中查看此节点信息:
发现7000节点中多了 0-5461 内容,说明它的槽已经被分配
- 给7001~7005分配槽:
sh addslots.sh 5462 10922 7001
sh addslots.sh 10923 16383 7002
- 我们给7000、7001、7002 都分配了节点,但是没有给3、4、5 分配节点,是因为他们会成为我们的从节点,从而进行高可用的故障转移备份;
-
注意:
- 如果使用
config get cluster*
命令,查询出来的cluster-require-full-coverage
的值为no ,则不需要所有节点都被分配槽才可以使用。
- 即只要有一个节点被分配了槽,且此处的配置为no,那么我们就可以连接任意节点,然后进行redis的相关操作,如图所示:
9.11 原生安装-4-分配主从
我们这里的教程是一个服务器中的,多个服务器之间的安装与此类似。
9.12 ruby环境准备-说明
- 说明:
- 官方提供了Ruby环境下的集群安装脚本,所以我们可以使用ruby环境来进行安装集群;
- Ruby环境准备:
- 下载、编译、安装Ruby:
- 安装rubygem redis:
- 安装redis-trib.rb:
cp ${REDIS_HOME}/src/redis-trib.rb /usr/local/bin
9.13 ruby环境准备-操作
- 准备脚本:
- 执行安装命令:
cat ruby.sh
- 进行解压缩:
tar -xvf ruby-2.3.1.tar.gz
- 进入目录:
cd ruby-2.3.1
- 设置prefix:
./configure -prefix=/usr/local/ruby
- 执行make命令:
make && make install
- 查看ruby是否安装成功:
- 命令如下:
ruby -v
,
- 如图所示说明安装成功:
- 回到上一层目录,然后安装ruby的客户端:
cd ..
wget http://rubygems.org/downloads/redis-3.3.0.gem
- 执行客户端安装:
sudo gem install -l redis-3.3.0.gem
- 执行以上步骤后,可以执行命令:
sudo gem list -- check redis gem
的相关依赖,如图所示:
- 查看redis check:
9.14 redis-trib构建集群
9.15 原生命令和redis-trib.rb对比
- 原生命令安装:
- 理解Redis Cluster架构
- 生产环境不适用
- 官方命令安装:
- 高效、准确
- 其他:
- 可视化部署
十. 深入Redis Cluster
10.1 集群伸缩原理
10.2 扩展集群-1.加入节点
- 扩容集群所需步骤:
- 准备新节点
- 加入集群
- 迁移槽和数据
- 准备新节点:
10.3 扩展集群-2.加入集群
- 加入集群:
- 作用:
- 为它迁移槽和数据,实现扩容
- 作为从节点负责故障转移
- 官方工具提供了功能:
10.4 扩展集群-3.迁移槽和数据
- 迁移槽和数据的步骤:
- 槽迁移计划:
- 将槽平均,为新的节点分配槽:
- 步骤:
- 对目标节点发送: cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据
- 对源节点发送:cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
- 源节点循环执行cluster getkeysinslot {slot} {count} 命令,每次获取count个属于槽的键
- 在源节点上执行 migrate {targetIp} {targetPort} key 0 {timeout} 命令把执行key迁移。
- 重复执行步骤3~4直到槽下所有的键数据迁移到目标节点。
- 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。
- 迁移数据-完整流程图:
- 迁移数据伪代码:
- 迁移数据
- 添加从节点
10.5 集群扩容演示
10.6 集群缩容-说明
- 步骤:
- 下线迁移槽:
- 忘记节点
- 关闭节点
- 如图所示:
10.7 moved异常说明和操作
-
moved重定向:
- 当客户端发送命令给任意节点时,节点会计算槽和对应节点,如果指向自身则进行处理和计算,如果不指向自身则回复moved,客户端拿着回复信息重定向到目标节点去;
-
槽命中,直接返回结果:
-
槽不命中,moved重定向:
-
redis-cli moved 异常:
只有集群模式才能重定向到非本节点的槽,所以没有使用集群模式且命令所属槽不在本地则无法执行命令,就会抛出异常;
10.8 ask重定向
-
ask重定向:
- 产生背景:
- 在集群缩容扩容的过程中,会比较慢,就会产生一个中间状态,就是ask重定向;
-
moved和ask的区别?
- 两者都是客户端重定向
- moved槽已经确定迁移,ask的槽还在迁移中;
10.9 smart客户端实现原理
- 使用目标就是追求性能:
- 从集群中选一个可运行节点,使用cluster slots初始化槽和节点映射。
- 将cluster slots的结果映射到本地,为每个节点创建JedisPool(避免频繁moved导致性能降低)
- 准备执行命令,在Jedis Cluster 内部维护了slot与节点的关系,我们传入命令的时候在客户端就能准确找到对应的节点连接,避免了重定向;执行命令的流程:
10.10 JedisCluster基本使用
- 基本使用图示:
- 使用技巧:
- 单例:内置了所有节点的连接池
- 无须手动借还连接池
- 合理设置commons-pool
10.11 整合spring
- 定义简单工厂:
@Getter
@Setter
@Component
public class JedisClusterFactory{
private JedisCluster jedisCluster;
private List<String> hostPortList;
private int timeout;
private Logger logger = LoggerFactory.getLogger(JedisClusterFactory.class);
public void init(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
Set<HostAndPort> nodeSet = new HashSet<HostAndPort>();
for(String hostPort: hostPortList){
String[] arr = hostPort.split(":");
if(arr.length != 2){
continue;
}
nodeSet.add(new HostAndPort(arr[0],Integer.parseInt(arr[1])));
}
try{
jedisCluster = new JedisCluster(nodeSet,timeout,jedisPoolConfig);
}catch(Exception e){
logger.error(e.getMessage(),e);
}
}
public void destroy(){
if(jedisCluster != null){
try{
jedisCluster.close();
}catch(IOException e){
logger.error(e.getMessage(),e);
}
}
}
}
10.12 多节点操作命令
- 多节点命令实现
Map<String,JedisPool> jedisPoolMap = jedisCluster.getClusterNodes();
for(Entry<String,JedisPool> entry: jedisPoolMap.entrySet()){
Jedis jedis = entry.getValue().getResource();
if(!isMaster(jedis)){
continue;
}
}
10.13 批量操作优化
10.14 故障发现
- 故障发现:
- 通过ping/pong 消息实现故障发现:不需要sentinel
- 主观下线和客观下线:
- 主观下线:
- 客观下线:
- 尝试客观下线:
- 通知集群内所有节点标记故障节点为客观下线
- 通知故障节点的从节点触发故障转移流程
- 如图所示:
10.15 故障恢复
- 故障恢复
- 检查资格:
- 每个从节点都会检查与故障主节点的断线时间。
- 超过 cluster-node-timeout * cluster-slave-validity-factor取消资格。
- cluster-slave-validity-factor: 默认是10
- 准备选举时间:
- 选举投票:
谁先到达可选举的时间,则更有可能获得更多的投票;
- 替换主节点:
- 当前从节点取消复制变为主节点。(slaveof no one)
- 执行clusterDelSlot 撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己。
- 向集群广播自己的pong消息,表明已经替换了故障从节点;
10.16 故障模拟
- 故障演练:
通过kill某个节点的进程来模拟宕机。
- 具体步骤:
- 执行kill -9 节点模拟宕机,例如:
kill -9 进程id
- 观察客户端故障恢复时间:
- 观察各个节点的日志,例如:tail -f client-failover-test.log (到指定redis的目录中查看log日志)
对故障模拟能够提前预知和防范风险;
10.17 Redis Cluster常见开发运维问题
-
集群完整性
- 完整性配置为: cluster-require-full-coverage 默认为yes
- 集群中16384个槽全部可用:保证集群完整性。
- 节点故障或者正在故障转移: (error)CLUSTERDOWN The Cluster is down
- 大多数业务无法容忍, cluster-require-full-coverage建议设置为no
-
带宽消耗
- 图示:
- 三个方面:
- 消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息。
- 消息数据量:slots槽数据(2kb空间)和整个集群1/10 的状态数据(10个节点状态数据约1kb)
- 节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高;
- 优化:
- 避免“大”集群,避免多业务使用一个集群,大业务可以多集群。
- cluster-node-timeout:带宽和故障转移速度的均匀;
- 尽量均匀分配到多机器上,保证高可用和带宽;
-
PubSub广播
- 图示:
- 问题:publish 在集群每个节点广播:加重带宽。
- 解决:单独“走”一套Redis Sentinel
-
集群倾斜-目录
- 数据倾斜:内存不均:
- 原因:
- 节点和槽分配不均
- redis-trib.rb info ip:port 查看节点、槽、键值分布
- redis-trib.rb rebalance ip:port 进行均衡(谨慎使用)
- 不同槽对应键值数量差异较大
- CRC16正常下比较均匀(哈希算法比较均匀)
- 可能存在hash_tag (一般的主要原因)
- cluster countkeysinslot {slot} 获取槽对应键值个数
- 包含bigKey
- bigKey: 例如大字符串、几百万的元素的hash、set等
- 从节点:redis-cli --bigkeys
- 优化数据结构
- 内存相关配置不一致
- hash-max-ziplist-value
- set-max-intset-entries等
- 以上配置,有的节点使用了但是有的节点没有配置,我们可以定期“检查”配置一致性来解决此问题;
- 请求倾斜:
- 热点key:重要的key或者bigkey
- 优化:
- 避免bigkey
- 热键不要用hash_tag
- 当一致性不高时,可以用本地缓存+MQ
-
读写分离
- 只读连接:集群模式的从节点不接受任何读写请求。
- 重定向到负责槽的主节点
- readonly命令可以读:连接级别命令
- 读写分离:更加复杂
- 同样的问题:复制延迟、读取过期数据、从节点故障
- 修改客户端:cluster slaves {nodeId}
-
数据迁移
- 官方迁移工具: redis-trib.rb import
- 只能从单击迁移到集群
- 不支持在线迁移:resource需要停写
- 不支持断点续传
- 单线程迁移:影响速度
- 在线迁移:
- 唯品会: redis-migrate-tool
- 豌豆荚: redis-port
-
集群vs单机
-
集群限制:
- key批量操作支持有限:例如mget、mset必须在一个slot
- key事务和Lua支持有限:操作的key必须在一个节点
- key是数据分区的最小粒度:不支持bigKey分区
- 不支持多个数据库:集群模式下只有一个db 0
- 复制只支持一层:不支持树形复制结构
-
分布式Redis不一定好,需要考虑实际
- Redis Cluster:满足容量和性能的扩展性,很多业务"不需要"
- 大多数时客户端性能会“降低”
- 命令无法跨节点使用:mget、keys、scan、flush、sinter等。
- Lua和事务无法跨节点使用。
- 客户端维护更复杂:SDK和应用本身消耗(例如更多的连接池)。
-
很多场景Redis Sentinel已经足够好。
10.18 本章总结
- Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。
- 搭建集群划分四个步骤:准备节点、节点握手、分配槽、复制。redis-trib.rb工具用于快速搭建集群。
- 集群伸缩通过在节点之间移动槽和相关数据实现。
- 扩容时根据槽迁移计划把槽从源节点迁移到新节点。
- 收缩时如果下线的节点有负责的槽需要迁移到其他节点,再通过cluster forget命令让集群内所有节点忘记被下线节点。
- 使用smart客户端操作集群达到通信效率最大化,客户端内部负责计算维护键-> 槽 -> 节点的映射,用于快速定位到目标节点。
- 集群自动故障转移过程分为故障发现和节点恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。
- 开发运维常见问题包括:超大规模集群带宽消耗,pub/sub广播问题,集群倾斜问题,单机和集群对比等。
十一. 缓存设计与优化
11.1 缓存的收益和成本
-
缓存的收益成本:
- 收益:
- 加速读写
- 通过缓存加速读写速度: CPU L1/L2/L3 Cache、Linux page Cache 加速硬盘读写、浏览器缓存、Ehcache缓存数据库结果
- 降低后端负载:
- 后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载等
- 成本:
- 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关。
- 代码维护成本:多了一层缓存逻辑
- 运维成本:例如Redis Cluster 、 云服务等
-
使用场景:
- 降低后端负载:对高消耗的SQL:join结果集/分组统计结果缓存。
- 加速请求响应:利用Redis/Memcache优化IO响应时间
- 大量写合并为批量写:如计数器先Redis累加再批量写DB
11.2 缓存的更新策略
11.3 缓存粒度问题
- 缓存粒度控制是什么意思呢?就是说我们在缓存的时候,一些数据我们是缓存重要的部分,还是缓存全部的属性,如图所示:
- 根据业务、实际场景来把握缓存的粒度,才是一个优良的redis设计。
- 缓存粒度控制-三个角度:
- 通用性:全量属性最好;
- 占用空间:部分属性最好;
- 代码维护:表面上全量属性更好。
11.4 缓存穿透问题
11.5 缓存雪崩优化
- 缓存雪崩-问题描述:
- 由于cache服务承载大量请求,当cache服务异常/脱机,流量直接压向后端组件(例如DB),造成级联故障。
- 缓存雪崩-优化方案
- 保证缓存高可用性
- 个别节点、个别机器、甚至是机房
- 例如Redis Cluster、Redis Sentinel、VIP
- 依赖隔离组件为后端限流
- 提前演练:例如压力测试
- Cache服务高可用:Redis Sentinel
- Cache服务高可用:主从漂移
- 依赖隔离组件-线程池/信号量隔离组件
- Spring Cloud 有Hytrix组件可以做线程隔离;
提前演练,防患于未然。
11.6 无底洞问题
-
问题描述:
- 机器到一定程度,集群添加机器也不能增加性能,反而性能下降,效率降低。
-
问题关键点:
- 机器越多,IO、网络耗时越多,当节点膨胀到一定程度可能会导致负收益。
- 更多的机器 !=更高的性能
- 批量接口需求(mget、mset等接口与集群不兼容)
- 数据增长与水平扩展的需求
-
优化IO的集中方法:
- 命令本身的优化:例如慢查询keys、hgetall bigkey
- 减少网络通信次数
- 降低接入成本:例如客户端长连接/连接池、NIO等。
-
四种批量优化的方法
- 例如慢查询keys、hgetall bigkey 避免使用。
- 减少网络通信次数
- 降低接入成本:例如客户端长连接/连接池、NIO等。
11.7 热点key的重建优化
- 热点key重建
- 图示:
- 当一个数据没有走缓存的时候,就会先查询数据源,然后再将数据存入缓存。而在高并发的场景下,其他线程也会执行这个操作。就会导致后续的存入命令是无效的,它虽然不会真正再重复存入数据,但是却消耗了大量的性能。
- 三个目标
- 减少重缓存的次数
- 数据尽可能一致
- 减少潜在危险
- 两个解决
-
互斥锁(mutex key):
- 通过加锁,后续的写操作会先进行等待,这样只会有一次写入命令;但是会存在等待问题,大量线程被hold;
- 示例代码:
-
永远不过期:
- 缓存层面:没有设置过期时间(没有用expire)
- 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
- 如图所示:
- 代码示例:
-
两种方案对比:
|方案|优点|缺点|
|互斥锁|思路简单,保持一致性|代码复杂度增加、存在死锁风险|
|永远不过期|基本杜绝热点key重建问题|不保证一致性、逻辑过期时间增加维护成本和内存成本|
十二. Redis云平台CacheCloud
12.1 Redis规模化困扰
- 遇到的问题:
- 发布构建繁琐,私搭乱盖
- 节点&机器等运维成本
- 监控报警初级
- CacheCloud
- 一键开启Redis(Standalone、Sentinel、Cluster)
- 机器、应用、实例监控和报警
- 客户端:透明使用、性能上报
- 可视化运维:配置、扩容、Failover、机器/应用/实例上下线。
- 已存在Redis直接接入和数据迁移。
- https://github.com/sohutv/cachecloud
- 使用规模:
- 使用场景:
- 全量视频缓存(视频播放API):跨机房高可用
- 消息队列同步(RedisMQ中间件)
- 分布式布隆过滤器(百万QPS)
- 计数系统:计数(播放数)
- 其他:排行榜、社交(直播)、实时计算(反作弊)等。
12.2 快速构建
- 解压:
sudo tar -xvf cachecloud-bin-1.2.tar.gz
- 进入目录:
cd cachecloud-web
- 添加一个数据库,我们可以简单看一下cachecloud 的sql,命令如下:
more cachecloud.sql
- 登录了本机的mysql后,将sql文件导入:
- 查看是否导入成功:
use cache_cloud;
show tables
exit
- 回到cachecloud文件夹目录,编辑jdbc.properties,将数据源设置为cache_cloud路径,如图所示:
账号密码根据实际来
- 启动
sudo sh start.sh
- 查看日志:
cd logs
tail -f cachecloud-web.log
- 在浏览器中输入地址:127.0.0.1:8585/manage/login
- 账号密码默认admin
- 界面如图所示:
12.3 机器部署
- 机器添加部署脚本:ssh账号、Redis安装部署
- CacheCloud添加机器,如图所示:
- 参考文档,在redis端执行对应脚本,然后在管理系统中接入,传入对应的ip地址等信息即可,如图所示:
- redis端执行脚本命令:
- 管理端执行接入命令:
12.4 应用接入
- 应用开通流程:
-
-
应用申请:
-
应用审批:
-
应用部署:
-
代码接入:
参考文档进行相关操作即可;
十四. 布隆过滤器
14.1 布隆过滤器
- 问题:现有50亿个电话号码,现有十万个电话号码,要快速准确判断这些号码是否已经存在?
- 通过数据库查询:实现快速有点困难;
- 数据预放在集合中:50亿万*8字节约等于40GB(内存极大浪费或不够)
- hyperloglog: 数据可能不准确;
类似问题有很多,比如垃圾邮件过滤(几亿封垃圾邮件)、文字处理软件(例如word)错误单词检测、 网络爬虫重复url检测、Hbase行过滤
- 布隆过滤器基本原理
-
布隆过滤器:
- 1970年伯顿.布隆提出,用很少的空间,解决上述类似问题。
- 实现原理:一个很长的二进制向量和若干个哈希函数;
-
布隆过滤器构建:
- 参数:m个二进制向量,n个预备数据,k个hash函数。
- 构建布隆过滤器:n个预备数据走一遍上面过程。
- 判断元素存在:走一遍上面过程:如果都是1,则表明存在,反之不存在。
- 误差率
- 肯定存在误差:恰好都命中了
- 直观因素:m/n的比率,hash函数的个数
- 实际误差率公式:
- 本地布隆过滤器
- 现有库:guava
- 本地布隆过滤器的问题:
- 容量受限制。
- 多个应用存在多个布隆过滤器,构建同步复杂。
- 基于Redis实现的布隆过滤器
- 基于位图实现:
- 实现方法:
- 定义布隆过滤器构造参数:m、n、k、误差概率
- 定义布隆过滤器操作函数:add和contain
- 封装Redis位图操作
- 开发测试样例
- 基于Redis单击实现存在的问题:
- 速度慢:比本地慢,输在网络;
- 容量受限:Redis最大字符串为512MB、Redis单机容量。
- 解决:基于Redis Cluster实现。
十五. Redis开发规范
15.1 key设计
- 三大建议:
- 可读行和可管理型:以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id,如:ugc:video:1
- 简洁性:保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视(redis3:39字节embstr),如:user:{uid}:friends:messages:{mid} 简化为u:{uid}m:{mid}
- 不要包含特殊字符:[反例:包含空格、换行、单双引号以及其他转义字符]
- key-value 个数与内存占用统计:
15.2 value设计
- value设计:
- bigkey(强制):
- string类型控制在10kb以内
- hash、list、set、zset元素个数不要超过5000
- 反例:一个包含几百万个元素的list、hash等,一个巨大的json字符串
- bigkey的危害:
-
网络阻塞
-
Redis阻塞:
-
集群节点数据不均衡:
对象差不多,但是内存消耗却不相同
-
频繁序列化:应用服务器CPU消耗
- Redis客户端本身不负责序列化
- 应用频繁序列化和反序列化bigkey:本地缓存或Redis缓存;
- 如何发现bigKey:
-
应用异常:
- 如果别的慢查询过长,会导致其他的reids连接或者读取超时;
-
reids-cli --bigkeys:
-
scan + debug object:
-
主动报警:网络流量监控、客户端监控:
-
内核热点key问题优化
使用内核统计
15.3 bigkey删除
- bigkey 删除
- 阻塞:注意隐性删除(过期、rename等)
- 删除可能会导致大量耗时,统计如图所示:
- 正确的方式:
- 在Redis 4.0 中可以使用:lazy delete(unlink命令):
- bigkey预防:
- 优化数据结构:例如二级拆分
- 物理隔离或者万兆网卡:不是治标方案
- 命令优化:例如hgetall-> hmget、hscan
- 报警和定期优化
- bigkey总结:
- 牢记Redis单线程特性
- 选择合理的数据结构和命令
- 清楚自身ops
- 了解bigkey的危害,以避免此类问题;
15.4 选择合适的数据结构
- 选择合适的数据结构,示例:
- 一个例子、三种方案:我们要讲picId存入 100 万条,使用string、hash以及若干个小hash的方案:
-
全部string: set picId userId:
-
一个hash: hset allPics picId userId:
-
若干个小hash:hset picId/100 picId%100 userId
- 三种方案内存对比:
- 三种方案内存分析:
- 三种方案优缺点对比:
15.5 键值生命周期
- Redis不是垃圾桶,设置缓存时应该设置生命周期;
- 周期数据需要设置过期时间,object idle time 可以找垃圾key-value
- 过期时间不宜集中:缓存穿透和雪崩问题,如图所示:
15.6 命令使用技巧
- 【推荐】O(N)以上命令关注N的数量
- 例如:hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。
- 【推荐】禁用命令
- 禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。
- 【推荐】合理使用select
- redis的多数据库较弱,使用数字进行区分;
- 很多客户端支持较差
- 同时多业务用多数据库实际还是单线程处理,会有干扰;
- 【推荐】Redis事务功能较弱,不建议过多使用。
- Redis的事务功能较弱(不支持回滚)
- 而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)
- 【推荐】Redis集群版本在使用Lua上有特殊要求
- 所有key,必须在1个slot上,否则直接返回error。
- “-ERR eval/evalsha command keys must in same slot\r\n”
- 【建议】必要情况下可以使用monitor命令时,主要不要长时间使用:
15.7 Java客户端优化
- 【推荐】
- 避免多个应用使用一个Redis实例
- 比如: 不相干的业务拆分,公共数据做服务化。
- 【推荐】:
- 使用连接池,标准使用方式:
- 连接池参数说明:
- 如何预估最大连接池? maxTotal怎么设置? maxIdle接近maxTotal即可
- 考虑因素
- 业务希望Redis并发量
- 客户端执行命令时间
- Reids资源:例如node(应用个数)* maxTotal 不能超过Redis最大连接数
- 资源开销:例如虽然希望控制空闲连接,但是不希望因为连接池的频繁释放创建连接造成不必要开销。
十六. 内存管理
16.1 Redis内存消耗
16.2 客户端缓冲区
-
输入缓冲区:
-
输出缓冲区配置
-
普通客户端缓冲区:
- 默认: client-output-buffer-limit normal 0 0 0
- 默认:没有限制客户端缓冲
- 注意:防止大的命令或者monitor:
-
slave客户端缓冲区
- 默认:client-output-buffer-limit slave 256mb 64mb 60
- 阻塞:主从延迟较高,或者从节点过多
- 注意:主从网络,从节点不要超过2个
-
pubsub客户端缓冲区
- 默认: client-output-buffer-limit pubsub 32mb 8mb 60
- 阻塞:生产大于消费
- 注意:根据实际场景适当调试
16.3 缓冲内存
-
缓冲内存-复制缓冲区
- 注意:此部分内存独享,考虑部分复制,默认1MB,可以设置更大
-
缓冲内存-AOF缓冲区:
- 注意:AOF重写期间,AOF的缓冲区,没有容量限制
16.4 对象内存
- 对象内存
- key:不要过长,量大不容忽视(redis3: embstr39字节)
- value:ziplist、intset等优化方式
- 内存碎片
- 必然存在:jemalloc
- 优化方式:
- 避免频繁更新操作:append、setrange等
- 安全重启,例如redis sentinel 和redis cluster等
- 子进程内存消耗
- 必然存在: fork(bgsave 和 bgrewriteaof)
- 优化方式:
- 去掉THP方式:2.6.38增加的特性
- 观察写入量:copy-on-write
- overcommit_memory=1
16.5 内存设置上限
十七. 开发运维常见坑
17.1 vm.overcommit_memory
-
含义:
-
实践-获取和设置:
-
实践-最佳实践
- Redis设置合理的maxmemory,保证机器有20%~30%的空闲内存
- 集中化管理AOF重写和RDB的bgsave
- 设置vm.overcommit_memory=1,防止极端情况下会造成fork失败。
17.2 swappiness
-
swappiness含义:
-
实践-设置
-
最佳实践
-
THP作用
-
OOM killer
- 作用:内存使用超出,操作系统按照规则kill掉某些进程。
- 配置方法:/proc/{progress_id}/oom_adj 越小,被杀掉概率越小
- 运维经验:不要过滤依赖此特性,应该合理管理内存:
17.3 安全的Redis
-
全球crackit攻击
2015年11月,全球350000+个Redis主机受到攻击
-
被攻击Redis特征:
- Redis所在的机器有外网IP
- Redis以默认端口6379为启动端口,并且是对外网开放的
- Redis是以root用户启动的
- Redis没有设置密码
- Redis的bind 设置为0.0.0.0 或者 “”.
-
攻击方式:
-
安全七法则:
- 设置密码:
- 服务端配置:requirepass和masterauth
- 客户端连接:auth命令和-a参数
- 相关建议:
- 密码要足够复杂,防止暴力破解
- masterauth不要忘记
- auth还是通过明文传输
- 伪装危险命令
- 服务端配置:rename-command 为空或随机字符
- 客户端连接:不可用或者使用指定随机字符
- 相关建议:
- 不支持config set动态设置
- RDB和AOF如果包含rename-command之前的命令,将无法使用
- config 命令本身是在Redis内核会使用到,不建议设置;
- bind
- 服务端配置:bind限制的是网卡,并不是客户端ip
- 相关建议:
- bind不支持config set
- bind 127.0.0.1 需要谨慎
- 如果存在外网网卡尽量屏蔽掉
- 防火墙
- 定期备份
- 不使用默认端口,防止被弱攻击杀掉
- 使用非root用户启动
17.4 热点key
-
客户端:
-
代理端:
-
服务端:
-
机器收集:
-
四种方案总结: