Redis 分布式实践
一. Redis 初识
1.1 Redis 导学
-
Redis有哪些特点?
- 高性能的key-value服务器
- 多种数据结构
- 丰富的功能
- 高可用分布式支持
-
适合人群:
- 初学者
- 进阶者
- 希望了解企业级开发Redis的同学
-
技术储备:
- 了解Linux命令的基本使用
- 了解常用数据结构
- 了解一门编程语言
-
授课思路:
- 开发运维结合
- 原理接入
- 实战案例分析
-
课程目标
- 全面了解Redis单机的相关功能
- 全面了解Redis高可用和分布式方案
- 理解企业级Redis的开发运维
1.2 Redis 初识
-
目录:
- Redis 是什么
- Redis 的特性回顾
- Redis 单机安装
- 使用场景
-
Redis是什么?
-
开源项目
-
它是一个基于键值的存储服务系统(是一种Map结构,一个key值,一个value值)
-
支持多种数据结构
-
性能高,功能丰富
-
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
- 方法1:
-
Redis客户端连接
-
Redis客户端返回值
1.15 redis 常用配置
- 配置有哪些?
Redis 的守护进程默认是关闭的,建议选为Yes,这样能够打印日志。对外端口号默认是6379;工作目录关系到日志文件和持久化文件存在于哪个目录中;
- 为什么取6379?
- 使用
config get *
命令可以显示有多少个配置,图示如下:
1.16 redis安装启动演示
- 参考博主的另外一篇博客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 字符串
-
字符串键值结构
字符串的value不能大于512MB
-
场景:
- 缓存、计数器、分布式锁、等等
-
get、set、del命令
-
incr、decr、incrby、decrby命令
-
实战:
- 实现如下功能=> 记录网站每个用户个人主页的访问量?
- 使用如下方式:
incr userid:pageview
(单线程:无竞争) userid:pageview代表key值名称,每个用户+每个页面作为一个唯一的值
- 使用如下方式:
- 缓存视频的基本信息(数据源在MySQL中)伪代码
- 图示:
先查询redis,如果不存在则访问mysql,查询出来后再将数据存入redis中后再返回给前端;
- 伪代码演示:
- 图示:
- 分布式id生成器:
利用redis的单线程特性;
- 实现如下功能=> 记录网站每个用户个人主页的访问量?
-
set、setnx、setxx命令
- 图示:
- 演示:
- 图示:
-
mget、mset命令:
- 图示:
- 例子:
- n次get:
使用mget可以省略大量的网络时间,使用它的时候要注意场景,如果查询的数据量特别大也需要注意慎重。
- 图示:
-
getset、append、strlen命令
- 例子如下:
- 演示:
- 例子如下:
-
incrbyflot、getrange、setrange命令
- 图示:
- 示例:
- 图示:
-
字符串总结
2.6 hash
-
哈希键值结构:
它的特点有点类似于一个map内部又嵌套了一个map 结构。
-
注意:
- field 不能相同,value可以相同
-
hget/hset/hdel
- API:
- 演示:
- API:
-
hexists/hlen
- API:
- 演示:
- API:
-
hmget/hmset
- API:
- 演示:
- API:
-
实战:记录网站每个用户个人主页的访问量?
- 使用命令
hincrby user:1:info pageview count
- 使用命令
-
实战:缓存视频的基本信息(数据源在mysql中)伪代码
-
hgetall/hvals/hkeys
- API:
- 演示:
注意:小心使用hgetall,它的执行速度比较慢,后面的将等待;
- API:
-
相似API:
- get=> hget
- set setnx => hset hsetnx
- del => hdel
- incr incrby decr decrby => hincrby
- mset=> hmset
- mget=> hmget
左边的是string 里的相关api,右侧是hash相关的api
-
使用string、hash 对比:
-
方案一: v1: 采用string结构,直接存取,value为对象的json:
-
方案二:v2: 采用string结构,将属性值filed 提取出来与key拼接作为一个key,它这个key是key+field的组合:
-
方案三:采用hash结构,field作为一个大map中的小map的key:
-
各个方案对比如下:
方案一的节约内存是跟方案二比而得出的;哈希不能对单独的属性数据设置过期时间,只能整体设置过期时间;
-
-
hsetnx/hincrby/hincrbyfloat
- API:
- 哈希总结:
- API:
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
- 执行lset 操作,会修改执行索引处的值。比如此处
-
演示:
-
实战:
通过时间排序存入,然后获取最新的微博可以拿取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:
- API:
- 下面为一些集合间的api操作:
- sdiff sinter sunion
- API:
- API:
- 实战:
- 微博共同关注:
- 微博共同关注:
- 一些命令对应一些实战案例:
- SADD => Tagging
- SPOP/SRANDMEMBER => Random item
- SADD + SINTER => Social Graph
- sdiff sinter sunion
2.10 zset
-
zset是一个有序集合。它是一个key-value 集合,但是value中是由两部分组成,一部分是score 一部分是value, 通过score 让整个集合有序:
-
集合 Vs 有序集合
-
列表 Vs 有序集合
-
有序集合都是以Z开头的命令
-
zadd
-
zrem:
-
zscore:
-
zincrby:
-
zcard:
-
演示:
-
zrange:
-
zrangebyscore:
-
zcount:
-
zremrangebyrank:
-
zremrangebyscore:
-
演示:
-
实战:
- 排行榜:
- 排行榜:
-
有序集合总结:
操作类型 基本命令 基本操作 zadd、zrem、zcard、zincrby、zscore 范围操作 zrange、zrangebyscore、zcount、zremrangebyrank 集合操作 zunionstore、zinterstore
三. Redis客户端的使用
3.1 Java 客户端:Jedis
-
获取Jedis
- Jedis是什么?
- 它是一个基于Java语言的客户端
- Maven依赖导入:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> <type>jar</type> <scope>compile</scope> </dependency>
- jedis 直连:
- 基本操作:
- 生成一个Jedis对象,这个对象负责和指定Redis节点进行通信
Jedis jedis = new Jedis("127.0.0.1",6379)
- jedis执行set操作
jedis.set("hello","world")
- jedis执行get操作,value = “world”
String value = jedis.get("hello")
- 生成一个Jedis对象,这个对象负责和指定Redis节点进行通信
- 构造对象说明:
- Jedis(String host,int port, int connectionTimeout, int soTimeout)
- host: Redis 节点的所在机器的IP
- port: Redis 节点的端口
- connectionTimeout: 客户端连接超时
- soTimeout: 客户端读写超时
- Jedis(String host,int port, int connectionTimeout, int soTimeout)
- 基本操作:
- Jedis是什么?
-
Jedis基本使用:
- string:
- hash:
- list:
- set:
- zset:
- string:
-
Jedis连接池使用:
- Redis直连:
- 连接池:
- 方案对比:
- Redis直连:
-
简单使用:
- 演示:初始化Jedis连接池,通常来讲JedisPool是单例的。GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
- 实战代码演示:
Jedis jedis = null; try{ // 1. 从连接池获取jedis对象 jedis = jedisPool.getResource(); // 2. 执行操作 jedis.set("hello","world"); }catch(Exception e){ e.printStackTrace(); }finally{ if(jedis!= null) // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池 jedis.close(); }
3.2 Go客户端:redigo简介
-
官网下载:
-
简单使用-连接:
-
简单使用-命令:
做一个简单介绍,具体开发可以参考官方文档
3.3 Jedis配置优化
-
commons-pool 配置(1)- 资源数控制:
参数名 含义 默认值 使用建议 maxTotal 资源池最大连接数 8 后面讨论 maxIdle 资源池允许最大空闲连接数 8 后面讨论 jmxEnabled 是否开启jmx监控,可用于监控 true 建议开启 -
commons-pool 配置(2)- 借还参数
参数名 含义 默认值 使用建议 blockWhenExhausted 当资源池用尽后,调用者是否要等待。只有当为true时,下面的maxWaitMillis 才会生效 true 建议使用默认值 maxWaitMillis 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒) -1: 表示永不超时 不建议使用默认值 testOnBorrow 向资源池借用连接时是否做连接有效性检测(ping),无效连接会被移除 false 建议false testOnReturn 向资源池归还连接时是否做连接有效性检测(ping),无效连接会被移除 false 建议false -
出现问题的一些地方:
- 慢查询阻塞:池子连接都被hang住
- 资源池参数不合理:例如QPS高、池子小。
- 连接泄漏(没有close()):此类问题比较难定位,例如client list、netstat等,最重要的是代码
- DNS异常等
-
推荐连接池写法:
四. 瑞士军刀Redis其他功能
4.1 慢查询
-
生命周期:
-
两个配置:
- slowlog-max-len
- 先进先出队列
- 固定长度
- 保存在内存中
- 如图所示:
- slowlog-log-slower-than
- 慢查询阀值(单位:微秒)
- slowlog-log-slower-than=0,记录所有命令
- slowlog-log-slower-than < 0, 不记录任何命令
- slowlog-max-len
-
配置方法:
- 默认值:
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:
-
-
使用建议:
- 注意每次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…
- 执行完上图中的命令后,就类似达成下图中的效果,索引0 是1 ,索引5是1…
-
getbit:
-
bitcount:
-
bitop:
-
bitpos:
-
实战:
- 独立用户统计:
- 使用set 和 Bitmap:
- 只有10万独立用户数量呢?
- 使用set 和 Bitmap:
- 使用经验:
- 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 ...]
- 向hyperloglog添加元素:
-
演示:
-
内存消耗(百万独立用户)
-
使用经验:
- 是否能容忍错误?(错误率:0.81%)
- 是否需要单条数据?
- 是否需要很少的内存去解决问题?
4.6 geo
- GEO是什么? 用于记录位置信息:
- 应用场景:
- 摇一摇
- 根据距离找附近的人
- 租房附近多少米
- geoadd:
- geopos:
- geodist:
- georadius:
- API:
- 演示:
- API:
- 相关说明:
- 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则表示对其检验。
- save(同步):
-
触发机制-不容忽略方式 & 自动触发
- 全量复制:从节点执行全量复制的操作的时候,主节点会自动触发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
-
RDB有什么问题?
- 耗时耗数据
- 生成时需要将所有数据进行dump,首先会消耗大量的资源、消耗大量的时间、大量的IO性能
- 容易丢失数据:
- 它不是实时每秒进行更新,所以它不能保证备份的一直都是最新数据;
- 耗时耗数据
-
AOF 运行原理 - 创建
- 写入:
- 当redis每执行一条命令时则将命令写入到AOF文件中,这个AOF文件则记录了每次执行的命令;因为每次只执行记录一条命令,且在每次运行命令时都会记录,所以它既不是很消耗性能,同时也能近乎实时的记录数据的变更;
- 加载AOF:
- 当发生需要恢复数据的情况时,执行AOF相关恢复命令,它就会将记录的命令依次执行,最终恢复。由于恢复时要执行大量的命令,故会有很大的耗时;
- 写入:
-
AOF的三种策略:
- always
- 先写到缓冲区中,然后根据一定的策略再写入到AOF文件中。先写到缓冲区中这样通过内存操作有更大的性能;
- everysec:
- 它会每秒将上一秒的命令回写到写入到硬盘,在高的数据量的情况下会保护硬盘,但是如果发生宕机了可能会丢失一秒的数据。
- 它是Redis的一个默认配置
- no:
- 由操作系统决定什么时候写入硬盘;
- always
-
三种策略比较:always/everysec/no
命令 always everysec no 优点 不丢失数据 每秒一次fsync 丢一秒数据 不用管 缺点 IO开销较大,一般的sata盘只有几百TPS 丢1秒数据 不可控 -
AOF重写:
- 什么是AOF重写?因为AOF记录的是每条命令的字符串,而在线上环境的写操作可能会有很多,所以AOF的文件会快速增加。AOF重写就是提供了一种压缩方式,将一些可优化的语句组合在一起,比如list命令可能有100个,经过优化压缩后可能就只有一个命令,这样就能极大地减少了AOF文件的大小,同时优化了恢复时AOF的时间;
AOF 重写会触发Redis的缓存淘汰策略。
- 为什么要AOF重写,优点在哪里? 它可以减少硬盘占用量、加快数据恢复速度;
- 什么是AOF重写?因为AOF记录的是每条命令的字符串,而在线上环境的写操作可能会有很多,所以AOF的文件会快速增加。AOF重写就是提供了一种压缩方式,将一些可优化的语句组合在一起,比如list命令可能有100个,经过优化压缩后可能就只有一个命令,这样就能极大地减少了AOF文件的大小,同时优化了恢复时AOF的时间;
-
AOF重写的两个方式:
- bgrewriteaof命令:
- Redis Bgrewriteaof命令用于异步执行一个AOF文件重写操作。重写会创建一个当前AOF文件的体积优化版本。即使Bgrewriteaof执行失败,也不会有任何数据都市,因为旧的AOF文件在Bgrewriteaof成功之前不会被修改;
从Redis 2.4开始,AOF重写由Redis自行触发,BGREWRITEAOF仅仅用于手动触发重写操作。
- Redis Bgrewriteaof命令用于异步执行一个AOF文件重写操作。重写会创建一个当前AOF文件的体积优化版本。即使Bgrewriteaof执行失败,也不会有任何数据都市,因为旧的AOF文件在Bgrewriteaof成功之前不会被修改;
- bgrewriteaof命令:
-
AOF重写配置:
- 自动触发时机(需要同时满足):
- AOF重写流程:
- 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分析
- CPU:
-
硬盘优化:
- 不要和高硬盘负载服务部署一起:存储服务、消息队列
- no-appendfsync-on-rewrite=yes
- 根据写入量决定磁盘类型:例如ssd
- 单机多实例持久化文件目录可以考虑分盘
6.3 AOF阻塞
- AOF追加阻塞:
- 为了保证AOF每秒刷盘、文件的实时性,会进行一个检测,如果同步时间小于2秒,则进行通过,如果大于2秒则会陷入阻塞等待同步线程执行完毕;
- AOF阻塞定位:
- 通过Redis日志进行分析,如:
- info Persistence:
- 通过硬盘,当硬盘资源比较紧张的时候,可能会发生阻塞,所以当发生阻塞的时候可以看看硬盘资源是否充足:
- 通过Redis日志进行分析,如:
七. 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 主从复制配置 - 介绍
-
两种实现方式:
- slaveof命令
- 配置
-
命令实现:
- 复制:
- 图示:
- 复制的过程是异步的,复制需要很多步骤和时间
- 图示:
- 取消复制:
- 6380 不想成为任何人的从属,就可以使用取消复制命令;此时6380的连接会断开,之前复制的数据不会删除,但是新的不会接受。他也可以重新连接新的主,若做此操作会自动删除以前的数据,然后建立新的主从关系;
- 图示:
- 复制:
-
配置实现:
slaveof ip port slave-read-only yes
-
比较:
方式 命令 配置 优点 无须重启 统一配置 缺点 不便于管理 需要重启
7.3 主从复制配置 - 操作
- 配置较繁琐,建议实际生产环境结合实际来进行设置,暂略。
7.4 runid 和复制偏移量
-
runid: redis在启动的时候,会随机生成一个id来作为这个运行中的redis的标识;主从复制时,可以判断redis是否发生了变化(比如重启后的redis的runid与之前是不一样的,新加的又会多一个runid)
- 查询命令:
redis-cli -p 6379 info server | grep run
- 如图所示:
- 查询命令:
-
偏移量:
- 数据写入量的字节,记录了写了多少数据,同时copy到从节点的时候,也会记录偏移量。如果两个机器的偏移量一致,说明同步数据已经一致,若不一致,则说明没有同步完。
- 查询偏移量:
- 命令:
redis-cli -p 6379 info replication
- 命令:
7.5 全量复制
- 全量复制:master文件会全部同步到slave,然后同步完成后再通过偏移量对比,再将增量的数据进行同步;
- 操作如下:
psync ? -1
: 同步命令,可以完成全量复制、部分复制的功能;- +FULLRESYNC {runId} {offset} 得到具体redis实例的runid的偏移量;
- 如图所示:
7.6 全量复制开销 + 部分复制
- 全量复制开销如下:
- bgsave时间
- RDB文件网络传输时间
- 从节点清空数据时间
- 从节点加载RDB的时间
- 可能的AOF重写时间
- 部分复制:
- 如果发生类似抖动的时候,可以将复制的损失降低到最低;
- 第一步,出现连接丢失
- 第二步,复制缓冲区
- 第三步再次连接
- 第四步告诉偏移量
- 如果错过很多数据,则会全量复制,如果不多则直接发送数据
- 后面步骤则进行传输,写入主库的数据;
- 如果发生类似抖动的时候,可以将复制的损失降低到最低;
7.7 故障处理
- 主从结构-故障自动转移:
- slave发生故障,图示:
- master发生故障,图示:
- slave发生故障,图示:
- 主从复制故障转移问题:
- 图示:
- 哨兵模式!
- 图示:
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主要配置:
- redis主节点:
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
- 分配槽:
- 设置主从
- 配置开启redis:
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 分配节点,是因为他们会成为我们的从节点,从而进行高可用的故障转移备份;
- 执行7000:
-
注意:
- 如果使用
config get cluster*
命令,查询出来的cluster-require-full-coverage
的值为no ,则不需要所有节点都被分配槽才可以使用。 - 即只要有一个节点被分配了槽,且此处的配置为no,那么我们就可以连接任意节点,然后进行redis的相关操作,如图所示:
- 如果使用
9.11 原生安装-4-分配主从
-
设置三主三从:
- 让7003 成为从7000的从节点:
里面的4918那串数字可以通过
redis-cli -p 7000 cluster nodes
查到; - 让7004 成为7001 的从节点:
- 让7005 成为7002的从节点:
- 分配完成,使用如下命令进行验证:
- 让7003 成为从7000的从节点:
-
完成过程中我们不会使用手动安装,我们可以使用官方工具来进行,但是手动安装可以让我们理解完整的过程;
我们这里的教程是一个服务器中的,多个服务器之间的安装与此类似。
9.12 ruby环境准备-说明
- 说明:
- 官方提供了Ruby环境下的集群安装脚本,所以我们可以使用ruby环境来进行安装集群;
- Ruby环境准备:
- 下载、编译、安装Ruby:
- 安装rubygem redis:
- 安装redis-trib.rb:
cp ${REDIS_HOME}/src/redis-trib.rb /usr/local/bin
- 下载、编译、安装Ruby:
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:
- 命令如下:
cd redis cd src ./redis-trib.rb
- 图示如下:
- 命令如下:
9.14 redis-trib构建集群
-
前面的工作做好了,我们需要配置开启redis,如图所示:
- 以前的步骤:
- 使用trib 我们可以一键开启:
- 以前的步骤:
-
将我们之前手动搭建的集群进行杀死,然后替换成我们现在的一键搭建:
- 查询运行的redis进程并杀死对应进程:
- 为了防止干扰,我们将data目录下的数据进行清除:
- 进入data目录:
cd ../data
- 清除相关数据:
rm -rf *
- 进入data目录:
- 查询运行的redis进程并杀死对应进程:
-
前面准备工作做完,后续核心具体步骤如下:
- 每个节点启动redis:
- 查看进程是否启动成功:
- 使用 trib 的create 命令,直接创建集群(一键创建,可以省略掉我们之前手动创建要进行的一些步骤:创建连接、分配槽、创建主从关系等步骤):
- 每个节点启动redis:
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 多节点操作命令
- 多节点命令实现
// 获取所有节点的JedisPool Map<String,JedisPool> jedisPoolMap = jedisCluster.getClusterNodes(); for(Entry<String,JedisPool> entry: jedisPoolMap.entrySet()){ //获取每个节点的Jedis连接 Jedis jedis = entry.getValue().getResource(); // 只删除主节点数据 if(!isMaster(jedis)){ continue; } // finally close }
10.13 批量操作优化
-
批量操作如何实现呢?mget、mset必须在一个槽,四种优化方法:
- 串行mget:
- 串行IO:
- 并行IO:
- hash_tag:
- 串行mget:
-
四种方案优缺点分析:
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日志)
对故障模拟能够提前预知和防范风险;
- 执行kill -9 节点模拟宕机,例如:
10.17 Redis Cluster常见开发运维问题
-
集群完整性
- 完整性配置为: cluster-require-full-coverage 默认为yes
- 集群中16384个槽全部可用:保证集群完整性。
- 节点故障或者正在故障转移: (error)CLUSTERDOWN The Cluster is down
- 大多数业务无法容忍, cluster-require-full-coverage建议设置为no
- 完整性配置为: cluster-require-full-coverage 默认为yes
-
带宽消耗
- 图示:
- 三个方面:
- 消息发送频率:节点发现与其他节点最后通信时间超过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
- 官方迁移工具: redis-trib.rb import
-
集群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 缓存的更新策略
-
更新场景:
- LRU/LFU/FIFO算法剔除:例如maxmemory-policy
- 超时剔除:例如expire
- 主动更新:开发控制生命周期(比如主动删除数据再更新最新的该key数据)
-
缓存更新策略
|策略|一致性|维护成本|
|LRU/LIRS算法剔除|最差|低|
|超时剔除|较差|低|
|主动更新|较差|高| -
两条建议:
- 低一致性:最大内存和淘汰策略
- 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底。两者结合起来。
11.3 缓存粒度问题
- 缓存粒度控制是什么意思呢?就是说我们在缓存的时候,一些数据我们是缓存重要的部分,还是缓存全部的属性,如图所示:
- 根据业务、实际场景来把握缓存的粒度,才是一个优良的redis设计。
- 缓存粒度控制-三个角度:
- 通用性:全量属性最好;
- 占用空间:部分属性最好;
- 代码维护:表面上全量属性更好。
11.4 缓存穿透问题
-
缓存穿透是什么?大量请求不命中,直接访问数据库,导致缓存穿透。如图所示:
- 缓存的意义就在于保护数据库,而缓存穿透就导致缓存失去了意义。
-
原因:
- 业务代码自身问题
- 恶意攻击、爬虫等
-
如何发现:
- 业务的响应时间(没有走缓存,可能查询时间会变慢。)
- 业务本身问题
- 相关指标:总调用数、缓存层命中数、存储层命中数。
-
解决办法1- 缓存空对象:
- 可以把不存在的结果当做一个结果缓存到redis中;
- 如图所示:
- 缺点:
- 需要更多的键
- 缓存层和存储层数据“短期”不一致。
- 缓存空对象示例代码:
-
解决办法2-布隆过滤器拦截
- 先将请求在过滤器中进行拦截。
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文件导入:
-
如图所示:
-
导入命令为:
source /opt/cachecloud-web/cachecloud.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端执行脚本命令:
- 管理端执行接入命令:
- 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
- 选择合适的数据结构
- 过期设计
- 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命令):
- 在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 内存设置上限
-
内存管理
-
设置内存上限:
- 注意:定义实例最大内存,便于管理及其内存,一般要预留30%
-
动态调整内存上限
-
内存回收策略:
- 删除过期键值:
- 惰性删除:访问key-> expire dict -> del key
- 定时删除:每秒运行10次,采样删除:
- 删除过期键值:
-
-
内存溢出策略:超过maxmemory后出发相应策略,由maxmemory-policy控制
- Noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回端错误信息 “(error)OOM command not allowed when used memory”,此时Redis只响应读操作,由maxmemory-policy控制
- Volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有课删除的键对象,回退到noeviction策略。
- Allkeys-lru: 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
- Allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,直到腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
-
需不需要使用Redis?
- 数据:大数据、冷数据不需要
- 功能性:关系型数据、消息队列有更好的方案
-
总结:
- 内存是宝贵资源。
- 结合场景选择和优化数据结构
- 序列化是有成本的。
- 不要忽视键长度。
十七. 开发运维常见坑
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
-
客户端:
-
代理端:
-
服务端:
-
机器收集:
-
四种方案总结: