NoSQL 概述
什么是 NoSQL?
Not Only SQL(不仅仅是SQL),泛指非关系型的数据库,作为关系型数据库的补充。
为什么需要 NoSQL?
因为现在的应用有海量的用户,由此带来的高并发,使得基于磁盘的关系型数据库无法支撑。磁盘 IO 性能低下,数据关系复杂、扩展性差,不便于大规模集群。由此产生了 NoSQL,使用内存存储数据,不存储关系只存储数据。
NoSQL 特点
- 可扩容,可伸缩(数据之间没有关系)
- 大数据量下高性能(Redis 一秒写 8 万次,读取 11 万次)
- 灵活的数据模型(不需要事先设计数据库表)
- 高可用
NoSQL 四大分类
KV 键值对
- Redis
- Tair
- Memecache
文档型数据库
- MongoDB
- CouchDB
列存储数据库
- HBase
- 分布式文件系统:HDFS、GFS、OSS
图关系数据库
- Neo4j
- InfoGrid
当今时代的 3V + 3高
大数据时代的 3V:主要是描述问题的
- 海量 Volume
- 多样 Variety
- 实时 Velocity 大数据时代的 3高:主要是对程序的要求
- 高并发
- 高可扩
- 高性能
NoSQL 与 SQL 的应用场景
- 商品基本信息(名称、价格、厂商) ---- MySQL 关系型数据库
- 商品附加信息(描述、详情、评论) ---- MongoDB 文档型数据库
- 商品图片 ---- HDFS 分布式文件系统
- 搜索关键字 ---- ElasticSearch、Solr
- 热点信息(高频、波段性) ---- Redis、Tair KV 键值对
- 朋友圈社交网络 ---- Neo4j、InfoGrid 图关系数据库
Redis 概述
Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库,支持网络、可基于内存亦可持久化日志。Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave (主从)同步。
Redis 特征
- 数据间没有必然的关系
- 内部采用单线程
- 高性能(50 个并发执行 10 万个请求,读取速度为 11 万每秒,写的速度为 8 万每秒)
- 多种数据类型:string,list,set,hash,zset,geo,hyperloglog,bitmap
- 持久化
- 集群
- 事务
Redis 能干嘛?
- 加速热点数据的查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等
- 时效性信息控制,如验证码控制、投票控制等
- 分布式数据共享,如分布式集群架构中的 session 分离
- 分布式锁
- 消息队列(发布订阅),如秒杀、抢购、购票排队等
- 即时信息查询,如各位排行榜、各类网站访问统计、公交到站信息、在线人数信息(聊天室、网站)、设备信号等
- 高速缓存
Redis 解决方案列表
- 控制数据库表主键id,为数据库表主键提供生成策略,保障数据库表的主键唯一性
- 控制数据的生命周期,通过数据是否失效控制业务行为,适用于所有具有时效性限定控制的操作
- 应用于各种结构型和非结构型高热度数据访问加速
- 应用于购物车数据存储设计
- 应用于抢购,限购类、限量发放优惠卷、激活码等业务的数据存储设计
- 应用于具有操作先后顺序的数据控制
- 应用于最新消息展示
- 应用于随机推荐类信息检索,例如热点歌单推荐,热点新闻推荐,热卖旅游线路,应用APP推荐,大V推荐等
- 应用于同类信息的关联搜索,二度关联搜索,深度关联搜索
- 应用于同类型不重复数据的合并、取交集操作
- 应用于同类型数据的快速去重
- 应用于基于黑名单与白名单设定的服务控制
- 应用于计数器组合排序功能对应的排名
- 应用于定时任务执行顺序管理或任务过期管理
- 应用于及时任务/消息队列执行管理
- 应用于按次结算的服务控制
- 应用于基于时间顺序的数据操作,而不关注具体时间
Redis 为什么单线程还这么快?
Redis 是基于内存操作,CPU 不是 Redis 的性能瓶颈,Redis 的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了。 多线程 CPU 上下文会切换,而这非常耗时!对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!
Redis 下载与安装
Windows 版本
Windows 版本已经停更很久了
- 下载安装包:Github-Redis
- 解压安装包,文件包含:
- redis-server.exe 服务器启动命令
- redis-cli.exe 命令行客户端
- redis.windows.conf 核心配置文件
- redis-benchmark.exe 性能测试工具
- redis-check-aof.exe AOF 文件修复工具
- redis-check-dump.exe RDB 文件检查工具(快照持久化文件)
- 双击 redis-server.exe,即可运行服务
- 使用 redis-cli.exe 连接 Redis
Linux 版本
- 下载安装包:wget download.redis.io/releases/re…
- 解压安装包:tar -xvf redis-?.?.?.tar.gz
- 进入 Redis 根目录
- 基本的环境安装
- yum install gcc
- yum install gcc-c++
- 编译:make
- 安装:make install,默认的安装路径:/usr/local/bin
- 拷贝解压后文件夹中的配置文件 redis.conf 至安装路径
- 修改 redis.conf 中的 daemonize 为 yes(后台启动)
- 启动:redis-server redis.conf
压测
Redis 自带压力测试工具:redis-benchmark。
redis-benchmark [option] [option value]
redis-benchmark -c 50 -n 100000 # 50 个并发执行 10 万个请求
Redis 命令
Redis 命令不区分大小写,Redis 命令中心。Key 的命名惯例:表名:主键名:主键值[:字段名],比如:order:id:29437
通用命令
- clear:清除屏幕内容
- quit、exit、<Esc>:退出命令行客户端
- help command/@group:查看命令或者组中所有命令的帮助文档(分别为:命令格式,功能描述,所属群组,出现版本)
- select index:切换数据库,默认有 16 个数据库,且默认使用 0 数据库
- dbsize:查看 db 大小
- flushdb:清空当前数据库
- flushall:清空全部数据库
- shutdown:关闭 Redis
- ping:测试连接
- time:获取当前系统时间
Redis-Key
- keys *:查看所有 key(* 匹配任意数量的任意符号;? 匹配一个任意符号;[] 匹配一个指定符号)
- h?llo 匹配 hello, hallo 和 hxllo
- h*llo 匹配 hllo 和 heeeello
- h[ae]llo 匹配 hello 和 hallo, 但是不匹配 hillo
- h[^e]llo 匹配 hallo, hbllo, … 但是不匹配 hello
- h[a-b]llo 匹配 hallo 和 hbllo
- 如果你想取消字符的特殊匹配(正则表达式),可以在它的前面加\。
- exists key:判断当前的 key 是否存在
- del key:删除 key
- move key 1:移动当前 key 到数据库 1
- expire key 10:设置 key 的过期时间为 10 秒
- pexpire key milliseconds:设置 key 的过期时间,毫秒级
- expireat key timestamp:设置 key 某一时刻过期
- pexpireat key milliseconds-timestamp:设置 key 某一时刻过期,毫秒级
- ttl key:查看 key 的剩余时间,秒级,-1 表示永久有效;-2 已经过期或被删除或未定义
- pttl key:查看 key 的剩余时间,毫秒级,-1 表示永久有效;-2 已经过期或被删除或未定义
- persist key:将 key 从时效性转换为永久性
- type key:查看 key 的类型
- rename key newkey:修改 key 名称
- renamenx key newkey:有就修改
String
可以使用 incr 作为计数器(比如投票,高并发时不可能每次都操作数据库,可以写入 Redis 然后定时更新到数据库;比如 12306 抢票中的库存)或者分布式 id,可以缓存对象,可以使用 setnx 做分布式锁。
- set key value:设置值
- get key:获取值
- append key value:追加字符串,如果当前 key 不存在,就相当于 set key value
- strlen key:获取字符串的长度
- incr key:自增 1,要求 value 可以被解析为数字,但存储还是 String
- decr key:自减 1
- incrby key step:step 步长,指定增量
- decrby key step:step 步长,指定减量
- getrange key start end:获取指定范围的字符串,start,end 都包含在内,字符串下标从 0 开始,如果输入负数,那么表示倒数第几位(比如 -1,表示倒数第一位)
- setrange key offset value:替换指定位置开始的字符串(offset 也会被替换,并且只会替换 value 字符串的长度)
- setex key 10 value:设置 key 的值,过期时间为 10 秒(set with expire)
- psetex key milliseconds value:设置 key 的值,过期时间单位为毫秒
- setnx key value:如果 key 不存在,创建 key 并返回 1,否则返回 0(set if not exist,分布式锁中常常使用)
- mset k1 v1 k2 v2 ...:同时设置多个值
- mget k1 k2 k3 ...:同时获取多个值
- msetnx k1 v1 k2 v2 ...:原子性操作,要么一起成功,要么一起失败
- getset key value:先 get 再 set
List
Redis 中 List 实际上是一个双向链表,所以在两边插入或者修改值,效率最高。可以用作栈(Lpush Lpop)、队列(Lpush Rpop)、阻塞队列。如果移除了所有值,空链表,则 key 就自动删除。还可以将分页操作中的第一页数据保存到 List 中,第 2 也及更多的信息通过数据库加载。
- Lpush key elem1 elem2 ...:将一个值或者多个值,插入到列表头部 (左)
- Lpop key:移除 list 的第一个元素,并返回
- Lrange key start end: 通过区间获取具体的值!start,end 都包含在内,字符串下标从 0 开始,如果输入负数,那么表示倒数第几位(比如 -1,表示倒数第一位)
- Rpush key elem1 elem2 ...:将一个值或者多个值,插入到列表尾部 (右)
- Rpop key:移除 list 的最后一个元素,并返回
- Lindex key index:通过下标获得 list 中的某一个值
- Llen key:返回列表的长度
- Lrem key count elem:移除 list 中指定个数的元素,精确匹配
- Ltrim key start end:截取指定范围的元素(start,end 包含),列表被改变,只保留截取的部分
- RpopLpush source destination:移除 source 的最后一个元素,将它放入 destination 的头部
- Lset key index elem:将列表中指定下标的值替换为另外一个值,更新操作,如果不存在列表(或者不存在下标)去更新就会报错
- Linsert key before/after target elem:将某个元素 elem 插入到列中某个元素 target 的前面或者后面,如果 target 不存在(或者 key 不存在),则失败
Hash
value 是 Map 集合。更适合对象的存储,尤其是经常变动的信息。
- Hset key field value [field value ...]:设置 KV 键值对
- Hget key field:获取一个字段值
- Hmset key field value [field value ...]:设置 KV 键值对,和 Hset 没区别
- Hmget key field [field ...]:获取多个字段值
- Hgetall key:获取所有字段值
- Hdel key field [field ...]:删除指定字段
- Hlen key:获取字段数量
- Hexists key field:判断指定字段是否存在
- Hkeys key:获取所有 field
- Hvals key:获取所有 value
- Hincrby key field increment:指定增量
- Hsetnx key field value:如果不存在则设置并返回 1,否则返回 0
Set
Set 中的值不能重复,无序。可以使用 Srandmember 随机抽取,用于热点歌单推荐、热点新闻推荐等等。可以用于保存好友信息,交集能实现共同好友,其他还有好友推荐“六度空间理论”、美食推荐。
- Sadd key member1 member2 ...:添加值
- Smembers key:查看指定 set 的所有值
- Sismember key member:判断某一个值是不是在set集合中,在返回 1,不在返回 0
- Scard key:获取 set 中元素个数
- Srem key member1 member2 ...:移除指定元素
- Srandmember key [count]:随机抽取一个(指定个数)元素,不放回抽取(不会重复,并且如果 count 大于 set 长度,只会返回 set 中的所有元素)
- Spop key [count]:随机删除元素
- Smove source destination member:将指定的元素移动到另一个 set 集合
- Sdiff k1 [k2 k3 ...]:差集
- Sinter k1 [k2 k3 ...]:交集
- Sunion k1 [k2 k3 ...]:并集
- Sdiffstore destination k1 [k2 k3 ...]:求差集并存储到指定集合中
- Sinterstore destination k1 [k2 k3 ...]:求差集并存储到指定集合中
- Sunionstore destination k1 [k2 k3 ...]:求差集并存储到指定集合中
ZSet
在 Set 的基础上增加了一个权值,用来排序。除特有命令外,其他与 Set 一致,只是 Set 命令以 S 开头,ZSet 命令以 Z 开头。可以用作各种排名,比如:Top 榜、亲密度排名、聊天室活跃度。
- Zadd key score member [score member ...]:添加值
- Zrange key start stop [withscores]:查看指定范围的元素,正序
- Zrevrange key start stop [withscores]:查看指定范围的元素,逆序
- Zrangebyscore key min max [withscores] [limit offset count]:按照 score 过滤,正序展示(min 可取 -inf 表示负无穷,max 可取 +inf 表示正无穷,min 和 max 都包含在内)
- Zrevrangebyscore key max min [withscores] [limit offset count]:按照 score 过滤,逆序展示(min 可取 -inf 表示负无穷,max 可取 +inf 表示正无穷,min 和 max 都包含在内)
- Zrem key member [member ...]:删除指定元素
- Zremrangebyrank key start stop:根据排名删除(start 和 stop 都删除)
- Zremrangebyscore key min max:根据 score 删除(min 和 max 都删除)
- Zcard key:获取元素个数
- Zcount key min max:获取指定区间的元素个数(min 可取 -inf 表示负无穷,max 可取 +inf 表示正无穷,min 和 max 都包含在内)
- Zrank key member:获取元素对应的索引(从 0 开始,也可称为排名),正序排名
- Zrevrank key member:逆序排名
- Zscore key member:获取元素的 score
- Zincrby key increment member:修改元素的 score
GeoSpatial
可以计算地理位置的信息,两地之间的距离。可用来做“附近的人”。只有 6 个专用命令,但是其底层是用 ZSet 实现的,所以可以使用 ZSet 命令来操作 Geo。
- geoadd key longitude latitude member [longitude latitude member ...]:添加地理位置(有效的经度从 -180 度到 180 度,有效的纬度从 -85.05112878 度到 85.05112878 度)
- geopos key member [member ...]:获取目标定位
- geodist key member1 member2 [m/km/ft/mi]:查看两地直线距离(米/千米/英尺/英里,默认米)
- georadius key longitude latitude radius m/km/ft/mi [withcoord] [withdist] [COUNT count] [asc/desc]:以指定的定位,查找方圆 radius 内的数据(withcoord 显示其经纬度,withdist 显示距离,COUNT count 只显示指定数量的数据)
- georadiusbymember key member radius m/km/ft/mi [withcoord] [withdist] [COUNT count] [asc/desc]:找出位于指定元素周围的其他元素
- geohash key member [member ...]:返回元素的 11 位长度 GeoHash字符串,将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近
- Zrange key start end:查看地图中指定范围的元素
- Zrem key member [member ...]:删除指定元素
HyperLogLog
基数估算算法,统计不重复的元素,有一定得误差。其优点是占用的内存是固定的,2^64 个不同的元素只需要占用 12KB 内存。比如计算网页的 UV(页面访问量:一个人访问一个网站多次,但是还是算作一个人),传统的方式是使用 Set 保存用户的 id,然后统计 Set 中元素的个数,如果用户数量很大,那么内存就吃不消,但是使用 HyperLogLog,只需要 12KB 内存(PFadd 命令不是一次性分配 12KB 内存,会随着基数的增加内存逐渐增大;PFmerge 命令合并后占用的存储空间为 12KB,无论合并之前数据量多少),只有 0.81% 的误差,对于统计 UV 来说,可以忽略。所以如果可以接受误差,那么一定可以使用 HyperLogLog。
- PFadd key elem [elem ...]:添加元素
- PFcount key [key ...]:统计元素的基数数量
- PFmerge destkey sourcekey [sourcekey ...]:合并元素并放入 destkey
Bitmap
位图,是一种数据结构,操作二进制位来记录信息,只有 0 和 1 两种状态。比如记录用户打卡情况,只有打卡和未打卡两种情况,使用 0 表示未打卡,1 表示已打卡,那么记录一位用户一年的打卡情况只需要 365bit ≈ 46byte。其他只有两种状态的也可以使用 Bitmap。
- setbit key offset value:设置 key 下标为 offset 位的值(offset 从 0 开始)
- getbit key offset:获取目标位置的值
- bitcount key [start end]:统计指定 key 中 1 的数量
- bitop operation destkey key [key ...]:对指定 key 按位进行交、并、非、异或操作,并将结果保存到 destkey 中(operation 对应为:and、or、not、xor)
事务
Redis 的事务本质就是一组命令的集合。一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行。一次性、顺序性、排他性的执行一组命令!Redis 事务没有隔离级别(所有的命令在事务中,并没有直接被执行,只有发起执行命令 exec 的时候才会执行,不会产生脏读、幻读等情况);单条命令保证原子性,但是事务不保证原子性(原子性:要么一起成功,要么一起失败)。
- multi:开启事务
- set...:命令入队
- exec:执行事务
- discard:取消事务,multi 之后 exec 之前
- watch key [key ...]:监控
- unwatch:取消监控 注意点:
- 定义事务的过程中,命令格式输入错误怎么办?
- 语法错误,指命令书写格式有误
- 如果定义的事务中所包含的命令存在语法错误,整体事务中所有命令均不会执行。包括那些语法正确的命令。
- 定义事务的过程中,命令执行出现错误怎么办?
- 运行错误,指命令格式正确,但是无法正确的执行。例如对 list 进行 incr 操作
- 能够正确运行的命令会执行,运行错误的命令不会被执行,注意:已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。
Redis 锁
乐观锁
- 悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁!
- 乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁!比如:更新数据的时候,先获取 version,然后进行一系列操作,最后更新的时候比较 version。 具体步骤:
- 监控某个 key
- 开启事务
- 业务操作
- 执行事务
- 取消监控 只要监控的值发生改变,那么事务执行失败,所有操作都不会执行。
# 正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379> unwatch
OK
# 执行之前另外一个线程修改了 money,此时事务执行失败
127.0.0.1:6379> watch money # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失 败!
(nil)
127.0.0.1:6379> unwatch # 如果修改失败,先解锁,然后继续上述步骤就好
OK
分布式锁
- 使用 setnx 命令设置公共锁。
- 对于执行 setnx 命令返回设置成功的,拥有控制权,可以执行下一步的具体业务操作。
- 对于返回设置失败的,不具有控制权,排队或等待。
- 操作完毕通过 del 释放锁。
- 改良:如果某个应用获取到锁,在业务操作过程中宕机了,那么锁将永远不会被释放。
- 可以通过 expire 或者 pexpire 为锁添加时限性
- 由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
- 例如:持有锁的操作最长执行时间 127ms,最短执行时间 7ms。
- 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
- 锁时间设定推荐:最大耗时 * 120% + 平均网络延迟 * 110%
- 如果业务最大耗时<<网络平均延迟,通常为2个数量级,取其中单个耗时较长即可
Java 连接 Redis
- Java 连接 Redis 服务
- Jedis,是 Redis 官方推荐的 Java 连接开发工具
- SpringData Redis
- Lettuce
- 可视化客户端
- Redis Desktop Manager
- Redis Client
- Redis Studio
Jedis
导入依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
测试:连接数据库、执行操作、断开连接
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.2.100", 6379);
System.out.println(jedis.ping());
jedis.close();
}
}
API 都和命令一样,一个都没有变化,事务使用 Jedis 实现以下。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
public class TestTransaction {
public static void main(String[] args) {
String m1 = "user:1:money";
String m2 = "user:2:money";
new Thread(() -> {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
jedis.set(m1, "1000");
jedis.set(m2, "2000");
System.out.println(jedis.watch(m1, m2));
Transaction multi = jedis.multi();
try {
// 开启事务后,所有操作都必须通过 multi 对象操作,否则报错
// JedisDataException: Cannot use Jedis when in Multi. Please use Transaction or reset jedis state.
// jedis.incrBy(m1, 200);
multi.incrBy(m1, 200);
multi.decrBy(m2, 200);
// int i = 1 / 0;
Thread.sleep(1000);
List<Object> exec = multi.exec();
if (exec != null) {
exec.forEach(o -> System.out.println(o.getClass().getName() + ", " + o));
} else {
System.out.println("执行失败");
}
} catch (Exception e) {
e.printStackTrace();
//程序出现异常,放弃事务
multi.discard();
} finally {
System.out.println(jedis.get(m1));
System.out.println(jedis.get(m2));
jedis.unwatch();
jedis.close();
}
}).start();
new Thread(() -> {
Jedis jedis = new Jedis("127.0.0.1", 6379);
try {
Thread.sleep(500);
jedis.set(m1, "3000");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
SpringBoot 整合 Redis
在 SpringBoot2.x 之后,原来使用的 Jedis 被替换为了 Lettuce。
- Jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 Jedis Pool!更像 BIO 模式
- Lettuce:采用 netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式 POM 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>redis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
配置文件:application.properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
启动类:RedisApplication.java
package com.example.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
测试类:RedisApplicationTest.java
package com.example.redis;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RedisApplicationTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test() {
/*
* redisTemplate 自身有一些常用的 API,还有对应 8 中数据类型的获取方法,如下:
* opsForValue ----> String / Bitmap
* opsForList ----> List
* opsForHash ----> Hash
* opsForSet ----> Set
* opsForZSet ----> ZSet
* opsForGeo ----> Geo
* opsForHyperLogLog ----> HyperLogLog
*/
String k = "rt:test";
redisTemplate.opsForValue().set(k, "hello,world");
System.out.println(redisTemplate.opsForValue().get(k));
}
}
运行测试,在 redis-cli 中发现:
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\art:test"
这是因为默认使用的 jdk 序列化方式(需要 POJO 都实现 Serializable),通过下方的源码,就知道 RedisTemplate 是可以覆盖的。
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 可以自定义来替换这个
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 默认的 RedisTemplate 没有过多的设置,redis 对象都需要序列化!
// 两个泛型都是 Object, Object 类型,使用时需要强制转换,可以自定义成自己要的
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 因为 String 类型是最常用的,所以单独做了一个
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
自定义 RedisTemplate 并添加 Jackson 序列化,注意上方的 POM 依赖里面没有 Jackson 依赖,可以通过添加 web 启动器的方式,或者直接添加 Jackson 依赖,如下:
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-web</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.3</version>
</dependency>
package com.example.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 一般使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// Json 序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash 的 key 也采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序列化方式采用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序列化方式采用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
Redis 配置
- 配置文件 unit 单位对大小写不敏感
- 包含 INCLUDES,类似 Java 中的 import
# 导入并加载指定配置文件信息,用于 redis 公共配置较多的 redis 实例配置文件,便于维护
include /path/to/other.conf
- 网络 NETWORK
bind 127.0.0.1 # 绑定的ip
protected-mode yes # 保护模式
port 6379 # 端口设置
timeout 0 # 客户端无动作多少秒后,关闭连接(设置为 0,则关闭该功能)
- 通用 GENERAL:运行方式、日志、数据库
daemonize yes # 以守护进程的方式运行,默认是 no,我们需要自己开启为yes
pidfile /var/run/redis_6379.pid # 如果以后台的方式运行,我们就需要指定一个 pid 文件
# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" # 日志的文件位置名
databases 16 # 数据库的数量,默认是 16 个数据库
always-show-logo yes # 是否总是显示LOGO
- 快照 SNAPSHOTTING:RDB(Redis DataBase)设置
持久化,在规定的时间内,执行了多少次操作,会持久化到文件 .rdb .aof,Redis 是内存数据库,如果没有持久化,那么数据断电即失。
# 如果 900s 内,如果至少有一个 1 key 进行了修改,进行持久化操作
save 900 1
# 如果 300s 内,如果至少 10 key 进行了修改,进行持久化操作
save 300 10
# 如果 60s 内,如果至少 10000 key 进行了修改,进行持久化操作
save 60 10000
stop-writes-on-bgsave-error yes # 持久化过程中如果出错,是否停止持久化操作
rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源
rdbchecksum yes # 保存 rdb 文件的时候,进行错误的检查校验
dbfilename dump.rdb # RDB 文件名
dir ./ # rdb 文件保存的目录
- 复制 REPLICATION:主从复制
# 配置 master 地址
replicaof 127.0.0.1 6379
# 如果 master 有配置 requirepass,slave 需要配置
masterauth 123456
- 安全 SECURITY:设置密码
requirepass 123456 # 设置 Redis 的密码
- 客户端 CLIENTS:限制最大连接数
maxclients 10000 # 设置能连接上 redis 的最大客户端的数量
- 内存管理 MEMORY MANAGEMENT:配置最大内存以及逐出算法
maxmemory <bytes> # redis 配置最大的内存容量
maxmemory-policy noeviction # 内存到达上限之后的处理策略,可选策略如下:
# 检测易失数据
# 1、volatile-lru:挑选最近最少使用的数据淘汰
# 2、volatile-lfu:挑选最近使用次数最少的数据淘汰
# 3、volatile-ttl:挑选将要过期的数据淘汰
# 4、volatile-random:任意选择数据淘汰
# 检测全库数据
# 5、allkeys-lru:挑选最近最少使用的数据淘汰
# 6、allkeys-lfu:挑选最近使用次数最少的数据淘汰
# 7、allkeys-random:任意选择数据淘汰
# 放弃数据驱逐
# 8、noeviction(驱逐):禁止驱逐数据会引发 OOM
# LRU means Least Recently Used
# LFU means Least Frequently Used
maxmemory-samples 5 # 每次选取待删除数据的个数
- AOF配置 APPEND ONLY MODE
appendonly no # 默认不开启 aof 模式,默认使用 rdb 方式持久化,在大部分所有的情况下,rdb 完全够用
appendfilename "appendonly.aof" # 持久化的文件的名字
# appendfsync always # 每次修改都会追加,消耗性能
appendfsync everysec # 每秒执行一次,可能会丢失这 1s 的数据
# appendfsync no # 不执行 sync,这个时候操作系统自己同步数据,速度最快!
更多具体的配置,在下方会做说明。
持久化
Redis 是内存数据库,如果不将内存中的数据保存到磁盘,那么一旦服务器进程退出,服务器中的数据也会丢失,所以 Redis 提供了持久化功能。
RDB(Redis DataBase)
Redis 会单独创建(fork)一个子进程来进行持久化,先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。
默认的就是 RDB,一般情况下不需要修改这个配置。
RDB 保存的文件是 dump.rdb,在配置文件中快照中进行配置(dbfilename 配置文件名称,dir 配置存放目录)。生产环境会备份这个文件。
触发机制,自动生成一个 dump.rdb 文件:
- save 规则满足的情况下,会触发 rdb
- 执行 flushall 命令,会触发 rdb
- shutdown redis,会触发 rdb(使用 kill 等强制措施是不会触发的)
- 执行 save 命令,手动执行一次 rdb(save 命令会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用)
- 执行 bgsave 命令,手动启动后台保存操作,但不是立即执行(bgsave 命令是针对 save 阻塞问题做的优化。Redis 内部所有涉及到 RDB 操作都采用 bgsave 的方式,save 命令可以放弃使用) 如何从 dump.rdb 恢复 Redis? 只需要把 dump.rdb 文件放在配置的存放目录里面,redis 启动的时候会自动检查 dump.rdb 恢复其中的数据。也可以通过命令查看存放目录:
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"
可以使用 redis-check-rdb 修复 dump.rdb文件
优点:
- RDB 是一个紧凑压缩的二进制文件,存储效率较高
- RDB 内部存储的是 redis 在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景
- RDB 恢复数据的速度要比 AOF 快很多 缺点:
- RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据
- bgsave 指令每次运行要执行 fork 操作创建子进程,要牺牲掉一些性能
- Redis 的众多版本中未进行 RDB 文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
AOF(Append Only File)
以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。与 RDB 相比可以简单描述为改记录数据为记录数据产生的过程。AOF 的主要作用是解决数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
默认是不开启的,需要手动进行配置,只需要将 appendonly 改为 yes 就开启了 aof,然后重启 Redis 即可生效。
如果 aof 文件有错误,Redis 是启动不了的,可以使用下面的命令修复文件
redis-check-aof --fix appendonly.aof
写数据的三种策略(appendfsync):
- always(每次):每次写入操作均同步到 AOF 文件中,数据零误差,性能较低,不建议使用。
- everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,数据准确性较高,性能较高,建议使用,也是默认配置,在系统突然宕机的情况下丢失 1 秒内的数据
- no(系统控制):由操作系统控制每次同步到AOF 文件的周期,整体过程不可控
RDB 保存的文件是 appendonly.aof,在配置文件中 AOF 中进行配置(appendfilename 配置文件名称,dir 配置存放目录)。
AOF 重写
随着命令不断写入 AOF,文件会越来越大,为了解决这个问题,Redis 引入了 AOF 重写机制压缩文件体积。AOF 文件重写是将 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程。简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录。 作用:
- 降低磁盘占用量,提高磁盘利用率
- 提高持久化效率,降低持久化写时间,提高 IO 性能
- 降低数据恢复用时,提高数据恢复效率 重写规则:
- 进程内已超时的数据不再写入文件
- 忽略无效指令,重写时使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据的写入命令,如 del key1、hdel key2、srem key3、set key4 111、set key4 222 等
- 对同一数据的多条写命令合并为一条命令,如 lpush list1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。为防止数据量过大造成客户端缓冲区溢出,对list、set、hash、zset等类型,每条指令最多写入64个元素 可以通过 redis.conf 配置自动重写:
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
也可以手动重写:
bgrewriteaof
RDB 与 AOF
RDB VS AOF
| 持久化方式 | RDB | AOF |
|---|---|---|
| 占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) |
| 存储速度 | 慢 | 快 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 会丢失数据 | 根据策略决定 |
| 资源消耗 | 高/重量级 | 低/轻量级 |
| 启动优先级 | 低 | 高 |
总结
- RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
- AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾,Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
- 只做缓存,如果只希望你的数据在服务器运行的时候存在,也可以不使用任何持久化
- 同时开启两种持久化方式
- 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件,那要不要只使用 AOF 呢?作者建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 Bug,留着作为一个万一的手段。
- 性能建议
- 因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则。
- 如果 Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF 文件就可以了,代价一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设到 5G 以上,默认超过原大小 100% 大小重写可以改到适当的数值。
- 如果不 Enable AOF,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔 IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时宕机,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个。
发布订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道,下图展示了频道 channel1,以及订阅这个频道的三个客户端 —— client2、client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时,这个消息就会被发送给订阅它的三个客户端:
![]()
- 第一个 redis-cli 客户端,先订阅
redis 127.0.0.1:6379> SUBSCRIBE runoobChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
- 第二个 redis-cli 客户端,发布消息
redis 127.0.0.1:6379> PUBLISH runoobChat "Redis PUBLISH test"
(integer) 1
redis 127.0.0.1:6379> PUBLISH runoobChat "Learn redis by runoob.com"
(integer) 1
- 第一个 redis-cli 客户端接受到消息
1) "message"
2) "runoobChat"
3) "Redis PUBLISH test"
1) "message"
2) "runoobChat"
3) "Learn redis by runoob.com"
维护频道字典,将订阅者放入对应频道链表中,当有消息发送过来时,通知所有订阅者。观察者模式。一般使用消息队列,如:RabbitMQ(可以参见RabbitMQ 深入浅出)、RocketMQ、ActiveMQ、Kafka。
主从复制
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点 (master/leader),后者称为从节点 (slave/follower);数据的复制是单向的,只能由主节点到从节点。
特点
- Master 以写为主,Slave 只能读
- 默认情况下,每台 Redis 服务器都是主节点
- 一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点
作用:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
- 读写分离:master 写、slave 读,提高服务器的读写负载能力。 一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的(宕机),原因如下:
- 从结构上,单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大。
- 从容量上,单个 Redis 服务器内存容量有限,就算一台 Redis 服务器内存容量为 256G,也不能将所有内存用作 Redis 存储内存,一般来说,单台 Redis 最大使用内存不应该超过20G。
主从复制,读写分离。80% 的情况下都是在进行读操作,将读负载分摊到多个服务器,减少服务器的压力。架构中经常使用,至少一主二从。
原理
阶段一:建立连接阶段
建立 slave 到 master 的连接,使 master 能够识别 slave,并保存 slave 端口号
- 步骤1:设置 master 的地址和端口,保存 master 信息
- 步骤2:建立 socket 连接
- 步骤3:发送 ping 命令(定时器任务)
- 步骤4:身份验证
- 步骤5:发送 slave 端口信息 至此,主从连接成功!
阶段二:数据同步阶段工作流程
在 slave 初次连接 master 后,复制 master 中的所有数据到 slave(全量复制),将 slave 的数据库状态更新成 master 当前的数据库状态。
- 步骤1:请求同步数据
- 步骤2:创建 RDB 同步数据
- 步骤3:恢复 RDB 同步数据
- 步骤4:请求部分同步数据
- 步骤5:恢复部分同步数据
至此,数据同步工作完成!
master 说明:
- 如果 master 数据量巨大,数据同步阶段应避开流量高峰期,避免造成 master 阻塞,影响业务正常执行
- 复制缓冲区大小设定不合理,会导致数据溢出。如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使 slave 陷入死循环状态。
repl-backlog-size 1mb
- master 单机内存占用主机内存的比例不应过大,建议使用 50% - 70% 的内存,留下 30% - 50% 的内存用于执行 bgsave 命令和创建复制缓冲区 slave 说明:
- 为避免 slave 进行全量复制、部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务
slave-serve-stale-data yes|no
- 数据同步阶段,master 发送给 slave 信息可以理解 master 是 slave 的一个客户端,主动向 slave 发送命令
- 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,如果 master 带宽不足,因此数据同步需要根据业务需求,适量错峰
- slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是 slave。注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,应谨慎选择
阶段三:命令传播阶段
当 master 数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为命令传播。master 将接收到的数据变更命令发送给 slave,slave 接收命令后执行命令。 命令传播阶段出现了断网现象:
- 网络闪断闪连:忽略
- 短时间网络中断:部分复制
- 长时间网络中断:全量复制 部分复制的三个核心要素
- 服务器的运行 id(run id)
服务器运行 ID(runid)是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行 id。由40位字符组成,是一个随机的十六进制字符。用于识别身份。runid 在每台服务器启动时自动生成的,master 在首次连接 slave 时,会将自己的 runid 发送给 slave,slave 保存此 id,通过 info Server 命令,可以查看节点的 runid。
- 主服务器的复制积压缓冲区
复制缓冲区,又名复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令,每次传播命令,master 都会将传播的命令记录下来,并存储在复制缓冲区。复制缓冲区默认数据存储空间大小是 1M,由于存储空间大小是固定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列。每台服务器启动时,如果开启有 AOF 或被连接成为 master 节点,即创建复制缓冲区。
- 主从服务器的复制偏移量
一个数字,描述复制缓冲区中的指令字节位置。同步信息,比对 master 与 slave 的差异,当 slave 断线后,恢复数据使用。master 复制偏移量:记录发送给所有 slave 的指令字节对应的位置(多个),发送一次记录一次;slave 复制偏移量:记录 slave 接收 master 发送过来的指令字节对应的位置(一个),接收一次记录一次。
心跳机制
进入命令传播阶段候,master 与 slave 间需要进行信息交换,使用心跳机制进行维护,实现双方连接保持在线。
- master 心跳:
- 指令:PING
- 周期:由 repl-ping-slave-period 决定,默认 10 秒
- 作用:判断 slave 是否在线
- 查询:INFO replication,获取 slave 最后一次连接时间间隔,lag 项维持在 0 或 1 视为正常
- slave 心跳:
- 指令:REPLCONF ACK {offset}
- 周期:1 秒
- 作用1:汇报 slave 自己的复制偏移量,获取最新的数据变更指令
- 作用2:判断 master 是否在线 注意事项:
- 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步操作
min-slaves-to-write 2
min-slaves-max-lag 8
# slave 数量少于 2 个,或者所有 slave 的延迟都大于等于 10 秒时,强制关闭 master 写功能,停止数据同步
- slave 数量由 slave 发送 REPLCONF ACK 命令做确认
- slave 延迟由 slave 发送 REPLCONF ACK 命令做确认
常见问题
- 频繁的全量复制--master 重启
- 问题现象:伴随着系统的运行,master 的数据量会越来越大,一旦 master 重启,runid 将发生变化,会导致全部 slave 的全量复制操作。
- 解决方案:
- master 内部创建 master_replid 变量,使用 runid 相同的策略生成,长度 41 位,并发送给所有 slave;
- 在 master 关闭时执行命令 shutdown save,进行 RDB 持久化,将 runid 与 offset 保存到 RDB 文件中;
- repl-id repl-offset
- 通过 redis-check-rdb 命令可以查看该信息
- master 重启后加载 RDB 文件,恢复数据。重启后,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中。
- master_repl_id = repl master_repl_offset = repl-offset
- 通过 info 命令可以查看该信息
- 作用:本机保存上次 runid,重启后恢复该值,使所有 slave 认为还是之前的 master。
- 频繁的全量复制--网络环境不佳
- 问题现象:网络环境不佳,出现网络中断,slave 不提供服务。
- 问题原因:复制缓冲区过小,断网后 slave 的 offset 越界,触发全量复制。
- 最终结果:slave 反复进行全量复制。
- 解决方案:修改复制缓冲区大小 repl-backlog-size
- 建议设置如下:
- 测算从 master 到 slave 的重连平均时长 second
- 获取 master 平均每秒产生写命令数据总量 write_size_per_second
- 最优复制缓冲区空间 = 2 * second * write_size_per_second
- 频繁的网络中断--master 与 slave 断开连接
- 问题现象:master 的 CPU 占用过高 或 slave 频繁断开连接。
- 问题原因:
- slave 每 1 秒发送 REPLCONF ACK 命令到 master
- 当 slave 接到了慢查询时(keys * ,hgetall 等),会大量占用 CPU 性能
- master 每 1 秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应
- 最终结果:master 各种资源(输出缓冲区、带宽、连接等)被严重占用。
- 解决方案:通过设置合理的超时时间 repl-timeout(该参数定义了超时时间的阈值,默认60秒,超过该值,释放 slave),确认是否释放 slave
- 频繁的网络中断--slave 与 master 断开连接
- 问题现象:slave 与 master 连接断开。
- 问题原因:
- master 发送 ping 指令频度较低
- master 设定超时时间较短
- ping 指令在网络中存在丢包
- 解决方案:提高 ping 指令发送的频度 repl-ping-slave-period(超时时间 repl-timeout 的时间至少是 ping 指令频度的 5 到 10 倍,否则 slave 很容易判定超时)。
- 数据不一致
- 问题现象:多个 slave 获取相同数据不同步。
- 问题原因:网络信息不同步,数据发送有延迟。
- 解决方案:
- 优化主从间的网络环境,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象;
- 监控主从节点延迟(通过 offset)判断,如果 slave 延迟过大,暂时屏蔽程序对该 slave 的数据访问。
slave-serve-stale-data yes|no
# 开启后仅响应 info、slaveof 等少数命令(慎用,除非对数据一致性要求很高)
环境配置
只配置 slave,不用配置 master,因为默认就是 master。
127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色 master
connected_slaves:0 # 没有从机
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
- 复制出三个配置文件(命令可以使用 redis_端口.conf),修改对应的信息:
- port 端口
- daemonize yes
- pidfile ./redis_端口.pid
- logfile "redis_端口.log"
- dbfilename dump_端口.rdb
- appendfilename "appendonly_端口.aof"
- 启动三个 Redis
- slave 连接 master,slave 端配置
- 方式一:客户端发送命令,slaveof <masterip> <masterport>,重启后需要重新发送命令
- 方式二:启动服务器参数,redis-server -slaveof <masterip> <masterport>
- 方式三:配置文件,replicaof <masterip> <masterport>
- 主从断开连接,slave 端发送命令
slaveof no one
# slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据
- 授权访问(master 设置了 requirepass),slave 端配置文件
masterauth <password>
- 如果 master 宕机,slave 仍然处于 slave 角色(此时 master 恢复的话,就会重新连接),可以使用 slaveof no one 让自己变成主机,然后将其他的节点手动连接到这个新的 master。
Sentinal 哨兵模式
主从复制一旦 master 宕机,slave 无法自动转变为 master,需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,优先考虑哨兵模式。Redis 从 2.8 开始正式提供了 Sentinel(哨兵)架构来解决这个问题。
介绍
哨兵 (sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的 master 并将所有 slave 连接到新的 master。
作用
- 监控
- 不断的检查 master 和 slave 是否正常运行
- master 存活检测、master 与 slave 运行情况检测
- 通知(提醒)
- 当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知。
- 自动故障转移
- 断开 master 与 slave 连接,选取一个 slave 作为 master,将其他 slave 连接到新的 master,并告知客户端新的服务器地址
- 注意
- 哨兵也是一台 redis 服务器,只是不提供数据服务
- 通常哨兵配置数量为单数
多哨兵模式
一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1 先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1 主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一
定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover [故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
哨兵配置
Sentinel 运行期间会实时改变配置文件内容。
# Example sentinel.conf
# 哨兵 sentinel 实例运行的端口,默认 26379
port 26379
# 哨兵 sentinel 的工作目录
dir /tmp
# 哨兵 sentinel 监控的 redis 主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母 A-z、数字 0-9 、这三个字符 ".-_" 组成。
# quorum 配置多少个 sentinel 哨兵统一认为 master 主节点失联,那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在 Redis 实例中开启了 requirepass foobared 授权密码,这样所有连接 Redis 实例的客户端都要提供密码
# 设置哨兵 sentinel 连接主从的密码,注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
# 指定多少毫秒之后,主节点没有应答哨兵 sentinel,此时哨兵主观上认为主节点下线,默认 30 秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生 failover 主备切换时最多可以有多少个 slave 同时对新的 master 进行同步,
# 这个数字越小,完成 failover 所需的时间就越长,
# 但是如果这个数字越大,就意味着越多的 slave 因为 replication 而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个 slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
# 1. 同一个 sentinel 对同一个 master 两次 failover 之间的间隔时间。
# 2. 当一个 slave 从一个错误的 master 那里同步数据开始计算时间。直到 slave 被纠正为向正确的 master 那里同步数据时。
# 3. 当想要取消一个正在进行的 failover 所需要的时间。
# 4. 当进行 failover 时,配置所有 slaves 指向新的 master 所需的最大时间。
# 不过,即使过了这个超时,slaves 依然会被正确配置为指向 master,但是就不按 parallel-syncs 所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知 相关人员。
# 对于脚本的运行结果有以下规则:
# 若脚本执行后返回 1,那么该脚本稍后将会被再次执行,重复次数目前默认为 10;
# 若脚本执行后返回 2,或者比 2 更高的一个返回值,脚本将不会重复执行。
# 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为 1 时的行为相同。
# 一个脚本的最大执行时间为 60s,如果超过这个时间,脚本将会被一个 SIGKILL 信号终止,之后重新执行。
# 通知型脚本:当 sentinel 有任何警告级别的事件发生时(比如说 redis 实例的主观失效和客观失效等等),
# 将会去调用这个脚本,这时这个脚本应该通过邮件,SMS 等方式去通知系统管理员关于系统不正常运行的信息。
# 调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。
# 如果 sentinel.conf 配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则 sentinel 无法正常启动成功。
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个 master 由于 failover 而发生改变时,这个脚本将会被调用,通知相关的客户端关于 master 地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前 <state> 总是“failover”,<role> 是“leader”或者“observer”中的一个。
# 参数 from-ip,from-port,to-ip,to-port 是用来和旧的 master 和新的 master (即旧的 slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
哨兵启动命令
redis-sentinel sentinel_端口.conf
总结
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮! 缺点:
- Redis 不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
Redis 集群
现状问题
- 单机 redis 提供的服务 QPS 可以达到 10万/秒,当前业务 OPS 已经达到 20万/秒;
- 内存单机容量达到 256G,当前业务需求内存容量 1T。
解决方案:Redis Cluster
集群架构
为了高可用,至少 3主3从,共 6 台服务器。一一配对,当 slave 宕机时,集群标记该机器 FAIL,对集群无影响,重新上线时集群取消该机器的 FAIL 标记;当 master 宕机时,集群标记该机器 FAIL,并且其对应的 slave 自动变成 master 提供服务,旧 master 重新提供服务后,变为新 master 的 slave。
集群作用
- 分散单台服务器的访问压力,实现负载均衡
- 分散单台服务器的存储压力,实现可扩展性
- 降低单台服务器宕机带来的业务灾难
集群结构设计
数据存储设计
- 将所有的存储空间计划切割成 16384 份(slot 插槽),每台主机保存一部分(平均分配,并且结点会记录插槽位置信息)
- 通过算法设计,计算出 key 应该保存的位置:使用 crc16 算法计算 key,将计算结果与 16384 取模得到应该保存的位置
- 将 key 放入对应的位置
- 新增或者删除结点,会重新计算、分配插槽
集群内部通讯设计
- 各个数据库相互通信,保存各个库中槽的编号数据
- 一次命中,直接返回
- 一次未命中,告知具体位置
通过数据存储设计,确保了最多两次命中数据,高效。
集群搭建
采用 单机 3主3从。Redis 中文网集群教程
- 配置文件 redis-6379.conf
bind 127.0.0.1
port 6379
daemonize yes
dir /opt/redis-data
databases 16
logfile redis-cluster-6379.log
pidfile redis-cluster-6379.pid
# RDB
dbfilename dump-cluster-6379.rdb
rdbcompression yes
rdbchecksum yes
save 10 2
# AOF
appendonly yes
appendfsync always
appendfilename appendonly-cluster-6379.aof
# CLUSTER
# 开启 cluster 功能
cluster-enabled yes
# cluster 配置文件名,该文件属于自动生成,仅用于快速查找文件并查询文件内容
cluster-config-file node-cluster-6379.conf
# 节点服务响应超时时间,用于判定该节点是否下线或切换为从节点
# 测试时可以设置短一点,生产环境建议 30 秒到 1 分钟
cluster-node-timeout 10000
# master 连接的 slave 最小数量
# cluster-migration-barrier <count>
- 复制出其他 5 个配置文件
sed 's\6379\6380\g' redis-6379.conf > redis-6380.conf
sed 's\6379\6381\g' redis-6379.conf > redis-6381.conf
sed 's\6379\6382\g' redis-6379.conf > redis-6382.conf
sed 's\6379\6383\g' redis-6379.conf > redis-6383.conf
sed 's\6379\6384\g' redis-6379.conf > redis-6384.conf
- 开启 6 个会话窗口,分别启动 6 个 Redis
redis-server /usr/local/bin/redis-cluster-conf/redis-6379.conf
redis-server /usr/local/bin/redis-cluster-conf/redis-6380.conf
redis-server /usr/local/bin/redis-cluster-conf/redis-6381.conf
redis-server /usr/local/bin/redis-cluster-conf/redis-6382.conf
redis-server /usr/local/bin/redis-cluster-conf/redis-6383.conf
redis-server /usr/local/bin/redis-cluster-conf/redis-6384.conf
启动完成后,Redis 处于待连接状态,redis-cluster-6379.log 日志内容如下
15844:C 06 Jan 2021 13:16:19.761 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
15844:C 06 Jan 2021 13:16:19.761 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=15844, just started
15844:C 06 Jan 2021 13:16:19.761 # Configuration loaded
15844:M 06 Jan 2021 13:16:19.762 * Increased maximum number of open files to 10032 (it was originally set to 1024).
15844:M 06 Jan 2021 13:16:19.763 * No cluster configuration found, I'm aaaa8ae93c56443127e210f3f54cc1daff01e68b
15844:M 06 Jan 2021 13:16:19.764 * Running mode=cluster, port=6379.
15844:M 06 Jan 2021 13:16:19.765 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
15844:M 06 Jan 2021 13:16:19.765 # Server initialized
15844:M 06 Jan 2021 13:16:19.765 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
15844:M 06 Jan 2021 13:16:19.765 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
15844:M 06 Jan 2021 13:16:19.765 * Ready to accept connections
- 开启集群
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:6379 127.0.0.1:6380 \
127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384
# 老版本的需要使用 src/redis-trib.rb create --cluster-replicas 1 127.0.0.1:6379 ...
# 也可以使用 utils/create-cluster 脚本启动
--cluster-replicas 1:表示只有 1 个 slave。后面的 redis 服务全部写上 ip:port,它会自动分配 master 和 slave 角色,以及自动分配 master 和 slave 主从关系。
![]()
此时,再次查看 redis-cluster-6379.log
15844:M 06 Jan 2021 13:29:11.045 # configEpoch set to 1 via CLUSTER SET-CONFIG-EPOCH
15844:M 06 Jan 2021 13:29:11.071 # IP address for this node updated to 127.0.0.1
15844:M 06 Jan 2021 13:29:16.041 # Cluster state changed: ok
15844:M 06 Jan 2021 13:29:18.128 * Replica 127.0.0.1:6382 asks for synchronization
15844:M 06 Jan 2021 13:29:18.128 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '706b714c3f9f12e032510af413c9fb367ad642bc', my replication IDs are '0054dbc2b6ab7fa0cf25f0f3487a23762de3d10c' and '0000000000000000000000000000000000000000')
15844:M 06 Jan 2021 13:29:18.128 * Starting BGSAVE for SYNC with target: disk
15844:M 06 Jan 2021 13:29:18.131 * Background saving started by pid 15883
15883:C 06 Jan 2021 13:29:18.147 * DB saved on disk
15883:C 06 Jan 2021 13:29:18.147 * RDB: 2 MB of memory used by copy-on-write
15844:M 06 Jan 2021 13:29:18.166 * Background saving terminated with success
15844:M 06 Jan 2021 13:29:18.166 * Synchronization with replica 127.0.0.1:6382 succeeded
可以发现 6379 是 master,其 slave 是 6382,连接后 slave 立马请求同步数据。
- 客户端连接测试
- 第一次采用普通连接,set 值出错,因为 name 计算出来的插槽 5798 位置不在 6379 的 0 ~ 5460 范围内。
- 第二次采用集群连接的方式,发现客户端自动重定向到 6380,并且 set 成功。
- 第三次采用集群连接的方式,在 6382 get name,也会自动重定向到 6380,并且 get 成功。
与 sentinel 不同的是 cluster 模式中的 slave 不提供读的功能(更不提供写功能)。
- 查看集群结点情况
[root@bogon ~]# redis-cli -c -p 6379
127.0.0.1:6379> cluster nodes
c540a50155f5d84fd666addfd41f2587e262e93d 127.0.0.1:6384@16384 slave ae7245c003237ac71baab5d83e7752184cd00735 0 1609915141702 6 connected
8cbb0a6497be66c0eeb7fa029d8a890372cb6002 127.0.0.1:6380@16380 master - 0 1609915139685 2 connected 5461-10922
aaaa8ae93c56443127e210f3f54cc1daff01e68b 127.0.0.1:6379@16379 myself,master - 0 1609915140000 1 connected 0-5460
ae7245c003237ac71baab5d83e7752184cd00735 127.0.0.1:6381@16381 master - 0 1609915138000 3 connected 10923-16383
b8b8f4bd0492840b8f37a35652cc6078674fa7d7 127.0.0.1:6382@16382 slave aaaa8ae93c56443127e210f3f54cc1daff01e68b 0 1609915140695 4 connected
17695790129c6e53d974d89df4ad15ee01a7c18e 127.0.0.1:6383@16383 slave 8cbb0a6497be66c0eeb7fa029d8a890372cb6002 0 1609915140000 5 connected
这个信息在 node-cluster-6379.conf 中也会体现出来
[root@bogon redis-data]# cat node-cluster-6379.conf
c540a50155f5d84fd666addfd41f2587e262e93d 127.0.0.1:6384@16384 slave ae7245c003237ac71baab5d83e7752184cd00735 0 1609910958000 6 connected
8cbb0a6497be66c0eeb7fa029d8a890372cb6002 127.0.0.1:6380@16380 master - 0 1609910959000 2 connected 5461-10922
aaaa8ae93c56443127e210f3f54cc1daff01e68b 127.0.0.1:6379@16379 myself,master - 0 1609910958000 1 connected 0-5460
ae7245c003237ac71baab5d83e7752184cd00735 127.0.0.1:6381@16381 master - 0 1609910957861 3 connected 10923-16383
b8b8f4bd0492840b8f37a35652cc6078674fa7d7 127.0.0.1:6382@16382 slave aaaa8ae93c56443127e210f3f54cc1daff01e68b 0 1609910959000 4 connected
17695790129c6e53d974d89df4ad15ee01a7c18e 127.0.0.1:6383@16383 slave 8cbb0a6497be66c0eeb7fa029d8a890372cb6002 0 1609910959894 5 connected
vars currentEpoch 6 lastVoteEpoch 0
- slave 下线,所有结点都会将下线的节点标记为 FAIL
# redis-cluster-6379.log 日志内容
15844:M 06 Jan 2021 15:10:15.156 # Connection with replica 127.0.0.1:6382 lost.
15844:M 06 Jan 2021 15:10:25.343 * FAIL message received from 8cbb0a6497be66c0eeb7fa029d8a890372cb6002 about b8b8f4bd0492840b8f37a35652cc6078674fa7d7
# node-cluster-6379.conf 内容
c540a50155f5d84fd666addfd41f2587e262e93d 127.0.0.1:6384@16384 slave ae7245c003237ac71baab5d83e7752184cd00735 0 1609917023346 6 connected
8cbb0a6497be66c0eeb7fa029d8a890372cb6002 127.0.0.1:6380@16380 master - 0 1609917024363 2 connected 5461-10922
aaaa8ae93c56443127e210f3f54cc1daff01e68b 127.0.0.1:6379@16379 myself,master - 0 1609917023000 1 connected 0-5460
ae7245c003237ac71baab5d83e7752184cd00735 127.0.0.1:6381@16381 master - 0 1609917024057 3 connected 10923-16383
b8b8f4bd0492840b8f37a35652cc6078674fa7d7 127.0.0.1:6382@16382 slave,fail aaaa8ae93c56443127e210f3f54cc1daff01e68b 1609917015221 1609917013191 4 disconnected
17695790129c6e53d974d89df4ad15ee01a7c18e 127.0.0.1:6383@16383 slave 8cbb0a6497be66c0eeb7fa029d8a890372cb6002 0 1609917022331 5 connected
vars currentEpoch 6 lastVoteEpoch 0
重新将 6382 上线后,所有结点清除 6382 的 FAIL 标记,6382 向 6379 请求同步数据。
15844:M 06 Jan 2021 15:16:28.721 * Clear FAIL state for node b8b8f4bd0492840b8f37a35652cc6078674fa7d7: replica is reachable again.
15844:M 06 Jan 2021 15:16:29.698 * Replica 127.0.0.1:6382 asks for synchronization
15844:M 06 Jan 2021 15:16:29.698 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '65a1552a9d747d634c8a824ad0b44297ed51012f', my replication IDs are 'f58d385b5df5ee15782872420963857b7e6f8147' and '0000000000000000000000000000000000000000')
15844:M 06 Jan 2021 15:16:29.698 * Starting BGSAVE for SYNC with target: disk
15844:M 06 Jan 2021 15:16:29.699 * Background saving started by pid 16202
16202:C 06 Jan 2021 15:16:29.701 * DB saved on disk
16202:C 06 Jan 2021 15:16:29.702 * RDB: 2 MB of memory used by copy-on-write
15844:M 06 Jan 2021 15:16:29.737 * Background saving terminated with success
15844:M 06 Jan 2021 15:16:29.737 * Synchronization with replica 127.0.0.1:6382 succeeded
- master 下线,所有结点都会将下线的节点标记为 FAIL,并且它的 slave 自动变为 master
# redis-cluster-6382.log 日志内容
16198:S 06 Jan 2021 15:18:14.830 # Connection with master lost.
16198:S 06 Jan 2021 15:18:14.830 * Caching the disconnected master state.
16198:S 06 Jan 2021 15:18:15.281 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:15.282 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:15.282 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:16.297 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:16.297 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:16.298 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:17.316 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:17.316 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:17.316 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:18.338 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:18.338 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:18.338 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:19.352 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:19.352 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:19.353 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:20.371 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:20.371 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:20.372 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:21.387 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:21.387 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:21.387 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:22.402 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:22.402 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:22.403 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:23.419 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:23.419 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:23.419 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:24.439 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:24.439 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:24.439 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:25.457 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:25.457 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:25.457 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:25.662 * FAIL message received from 8cbb0a6497be66c0eeb7fa029d8a890372cb6002 about aaaa8ae93c56443127e210f3f54cc1daff01e68b
16198:S 06 Jan 2021 15:18:25.662 # Cluster state changed: fail
16198:S 06 Jan 2021 15:18:25.762 # Start of election delayed for 786 milliseconds (rank #0, offset 5740).
16198:S 06 Jan 2021 15:18:26.472 * Connecting to MASTER 127.0.0.1:6379
16198:S 06 Jan 2021 15:18:26.472 * MASTER <-> REPLICA sync started
16198:S 06 Jan 2021 15:18:26.472 # Error condition on socket for SYNC: Connection refused
16198:S 06 Jan 2021 15:18:26.573 # Starting a failover election for epoch 7.
16198:S 06 Jan 2021 15:18:26.576 # Failover election won: I'm the new master.
16198:S 06 Jan 2021 15:18:26.576 # configEpoch set to 7 after successful failover
16198:M 06 Jan 2021 15:18:26.576 # Setting secondary replication ID to f58d385b5df5ee15782872420963857b7e6f8147, valid up to offset: 5741. New replication ID is 98786184f01dabee0bb9c017e7b15e8d10842885
16198:M 06 Jan 2021 15:18:26.576 * Discarding previously cached master state.
16198:M 06 Jan 2021 15:18:26.576 # Cluster state changed: ok
日志中尝试连接 master 十次,因为配置文件中配置了 cluster-node-timeout 为 10 秒(说明 1 秒 PING 一次),然后标记 master 状态为 FAIL,最后把自己变成 master。如果此时恢复 6379,也只是 slave 了。
- cluster 节点操作命令
- cluster nodes:查看集群节点信息
- cluster replicate <master-id>:进入一个从节点 redis,切换其主节点
- cluster meet ip:port:发现一个新节点,新增主节点
- cluster forget <id>:忽略一个没有 slot 的节点
- cluster failover:手动故障转移
- 其他命令,如加入新节点,参看Redis 中文网集群规范,或者如下:
[root@bogon ~]# redis-cli --cluster help
Cluster Manager Commands:
create host1:port1 ... hostN:portN
--cluster-replicas <arg>
check host:port
--cluster-search-multiple-owners
info host:port
fix host:port
--cluster-search-multiple-owners
reshard host:port
--cluster-from <arg>
--cluster-to <arg>
--cluster-slots <arg>
--cluster-yes
--cluster-timeout <arg>
--cluster-pipeline <arg>
--cluster-replace
rebalance host:port
--cluster-weight <node1=w1...nodeN=wN>
--cluster-use-empty-masters
--cluster-timeout <arg>
--cluster-simulate
--cluster-pipeline <arg>
--cluster-threshold <arg>
--cluster-replace
add-node new_host:new_port existing_host:existing_port
--cluster-slave
--cluster-master-id <arg>
del-node host:port node_id
call host:port command arg arg .. arg
set-timeout host:port milliseconds
import host:port
--cluster-from <arg>
--cluster-copy
--cluster-replace
help
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.
常见问题及解决方案
使用缓存可以大幅提升应用的性能和效率,避免了频繁访问关系型数据库,但是同样也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
缓存预热
系统启动后迅速宕机,很可能是缓存中无数据,大量请求同时访问数据库,导致服务器宕机。在系统启动前将热点数据提前加载到缓存中。 解决方案:
- 提前将数据缓存起来
缓存雪崩
短时间内大量 key 失效,导致大量请求到达数据库。可能造成 Redis 和数据库资源被严重占用甚至崩溃。 解决方案:
- 构建多级缓存
- Nginx 缓存 + Redis 缓存 + EHCache 缓存
- 搭建 Redis 集群
- 防止因为 Redis 宕机造成过多 key 集体失效
- 限流、降级
- 限制请求数量,或者关闭部分功能
- 数据过期时间调整
- 超热数据永久不过期
- 根据业务数据有效期分类错峰
- 过期时间采用 固定时间 + 随机值 的形式
- 人工维护
缓存击穿
缓存击穿是指某一个 key 失效后,大量请求同时涌入,直接请求数据库,击穿了缓存。这种情况下,Redis 平稳运行,但是数据库压力激增甚至崩溃。 解决方案:
- 增加热点数据的有效期,甚至永久
- 构建多级缓存
- 设置不同的失效时间,保证不会同时过期
- 加互斥锁
- 同一时间同一个 key 只能有一个线程可以去查询数据库
缓存穿透
缓存穿透指 Redis 中没有数据,到数据库里面查找,也没有数据。如果大量请求都查找这个不存在的数据,那么会导致 Redis 服务器 CPU 占用激增,数据库压力激增甚至崩溃。 解决方案:
- 缓存空值
- 对 null 数据进行缓存,设定较短的有效期,比如 30 至 60 秒,最高不能超过 5 分钟
- 白名单策略
- 提前预热各种分类数据 id 对应的bitmap,id 作为 bitmap 的 offset,相当于设置了数据白名单。当加载正常数据时,放行,加载异常数据时直接拦截(效率偏低)
- 使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略)
- key 加密
- 问题出现后,临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验
- 例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问