单点Redis的问题
- 数据丢失问题:Redis是内存存储,服务器重启可能会丢失数据
解决方案:实现Redis数据持久化 - **并发能力问题:**单节点Redis并发能力虽然不错,但是无法满足如618这样的高并发场景
解决方案:搭建主从集群,实现读写分离 - 故障回复问题:如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段
解决方案:利用Redis哨兵,实现健康检测喝自动恢复 - 存储能力问题:Redis基于内存,单节点能存储的数据量难以满足海量数据需求
解决方案:搭建分片集群,利用插槽机制实现动态扩容
Redis持久化
持久化存在两种方式:RDB 和 AOF
RDB持久化
RDB全称Redis DataBase Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单的来说九十八内存中的所有数据都记录到磁盘中,党Redis实例故障重启后,从磁盘读取快照文件,恢复数据
快照文件称为RDB文件,默认保存在当前运行目录。
使用方法:
- 在客户端中使用save命令:由Redis主进程来执行RDB,会阻塞所有命令,如果数据量比较大,则消耗的时间比较长
- bgsave:开启子进程执行RDB,避免主进程受到影响
- Redis再停机的时候会自动执行一次RDB
RDB自动触发机制
RDB自动触发机制,需要在其配置文件中进行查看
可以得知,如果再900秒内修改一次或者在300s内修改超过10次或者60秒内修改超过10000次,则自动进行一个保存,执行bgsave
RDB其他配置
rdbcompression:保存的时候是否要进行一个压缩,默认为yes;如果进行了压缩就会消耗cup,建议关闭,存储空间占用大比较好解决
dbfilename:文件存储的名字
dir:标识存储的路径
rdbchecksum:默认开启,表示在存储快照后,redis使用CRC64算法来进行数据校验。这个操作会增加10%的性能消耗。如果希望获取最大的性能提升,改为no即关闭了此功能。
RDB的fork原理
bgsave开始时会fork主进程得到子进程,紫禁城共享主进程大的内存数据,完成fork后读取内存数据并写入RDB文件中
fork采用的是copy-on-write技术:
- 挡住进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会则会拷贝一份数据执行写操作。
RDB方式的流程
- fork主进程得到一个子进程,共享内存空间
- 紫禁城读取内存数据并写入新的RDB文件
- 用心RDB文件替换旧的RDB文件
RDB的缺点
RDB执行间隔时间长,两次RDB直接写入数据有丢失的风险
fork紫禁城、压缩、写出RDB文件都比较耗时
AOF持久化
AOF全程Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF,可以看作是命令日志文件。
AOF默认为关闭的,需要在配置文件中开启,此外,通过appendfilename可以指定文件的名称
AOF的命令记录频率:
由配置文件可知,存在三种模式:
no:写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓存写入到磁盘,速度最快
everysec:写命令执行完先放入AOF,然后标识每隔一秒将缓冲区的数据写入到AOF文件中默认方案
always: 标识每执行一次写命令,立即记录到AOF文件中,速度慢,但是最安全
配置项 | 刷机时机 | 优点 | 缺点 |
---|---|---|---|
always | 同步刷盘 | 可靠性最高,几乎不会丢失数据 | 性能影响比较大 |
everysec | 每秒刷机 | 性能适中 | 最多丢失1s内的数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
AOF重写机制:
因为AOF记录的是命令,导致AOF文件会比RDB文件大得多。而且AOP会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。
可以通过bgrewriteaof
命令,可以让AOF执行重写功能,用最傻送的命令达到相同的效果
Redis也会在自动触发阈值的时候自动重写AOF文件,阈值也可以在Redis.conf中配置
auto-aof-rewrite-percentage:AOF文件比上次文件增长超过了多少百分比则触发重写
auto-aof-rewrite-min-size:AOF文件体积最小多大以上触发重写
AOF与RDB的比较
AOF与RDB各有各的优点缺点,如果对数据安全性要求比较高,在实际开发中往往会两种结合使用
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,所以文件体积比较大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低 | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源,单AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求比较高的比较常见 |
Redis主从
单点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
数据同步原理
全量同步
主从第一次同步是全量同步:
- slave节点请求进行增量同步
- master节点判断replication id是否一致,发现不一致,拒绝增量同步,进行全量同步
- master将完整内存数据生成RDB文件(bgsave),发送RDB文件道slave
- slave清空本地数据,加载master的RDB文件
- master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
- slave执行接收到的命令,保持与master之间的同步
master如何判断slave是不是第一来同部数据集?
-
Replication ID
:简称replid,是数据集的标记,id一只则说明是统一数据及,每个master都有唯一的replid,slave则会继承master界的点replid -
offset
:偏移量,随着记录的repl_baklog中的数据增多而捉逐渐增大slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,则说明slave数据落后于master,需要进行更新。
因此slave做数据同步,必须向master声明自己的replid和offset,master才可以判断到底需要同步哪些数据。
如何判断是否为第一次来?
根据replication id,如果发现ID不一致,则表明为第一次来,那么就会想slave返回主节点的replication id和offset,同时在进行一个全量同步。
增量同步
主从第一次同步时全量同步,但是如果slave重启后(无论是否正常重启),此外,slave 能够在repl_baklog中找到offset,则执行增量同步
repl_baklog大小是有上限的(类似一个循环队列,相当于一个环),写满了就会覆盖最早的数据,如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log进行增量同步,只能再次进行全量同步
优化
-
在master中配置repl-diskless-sync yes 启用无磁盘复制,避免全量同步时的磁盘ID。如果开启这个选项,建议网络性能比较高但是磁盘读写能力比较差
-
控制Redis单节点的上线,单节点的上线不要太大,减少RDB导致的过多磁盘IO
-
适当提高repl_baklog的大小,发现slave宕机后尽快实现故障恢复,尽可能避免全量同步
-
限制一个master上的slave节点数量,如果实在是太多的slave,则可以使用主-从-从链式结构,减少master压力
Redis哨兵
作用
哨兵(sentinel)机制用于自动故障恢复。
- 监控:sentinel 会不断检查master和slave是否按照预期工作
- 自动故障恢复:如果master故障,sentinel会将一个slave提升为master,当故障实例恢复后也以新的master为主
- 通知:sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将新消息推送给Redis客户端
服务状态监控机制
sentinel基于心跳机制检测服务状态,每隔一秒相机群的每个实例发送ping命令:
- 主观下线:如果sentinel节点发现某个实力未在规定时间相应,则认为该实例主观下线
- 客观下线:若超过指定数量(quorum)的sentinel都热瓦内该实例主观下线,那该实例客观下线,quorum值最好超过sentinel实例数量的一半
选举新的master
一旦master发生故障,sentinel需要在slave中选择一个作为新的master,选择依据是:
- 首先会判断slave节点与master节点断开时间的长短,如果超过指定值(down-after-milliseconds*10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果为0则表示用不参与选举,默认值为100
- 如果slave-priority一样,则判断slave节点的offset值,越大说明越新,优先级越高
- 最后是判断slave节点的运行id,学校优先级越高
如何实现故障转移
当选中了一个slave为新的master后,故障转移步骤:
- sentinel给备选的slave节点发送
slave no one
命令,让该节点成为master - sentinel给所有其他
slave发送slaveof 新masterIP 新master端口号
命令让slave称为新master的从节点,开始从新的master上同步数据 - 最后sentinel将故障主节点标记为slave,党故障节点恢复后会自动成为新master的slave节点(修改了他的配置文件)
哨兵集群配置
这里我们在默认的sentinel.conf文件的基础上进行修改,该文件在解压redis的地方
对应的服务如下:
IP | 端口号 | 角色 |
---|---|---|
192.168.18.132 | 6379 | master |
192.168.18.133 | 6379 | slave |
192.168.18.134 | 6379 | slave |
192.168.18.132 | 26379 | sentinel |
192.168.18.133 | 26379 | sentinel |
192.168.18.134 | 26379 | sentinel |
配置文件修改:
# 最后面的2 标识数量超过2时认为主观下线,从而进行客观下线
sentinel monitor mymaster 192.168.18.132 6379 2
# 工作目录
dir "/opt/module/redis/bin/tmp"
随后直接启动sentinel启动指令./sentinel sentinel.conf
,启动之后即可看到
此时我们将主节点【192.168.18.132】进程直接结束来模拟宕机,结果如下
从图中可知哨兵在检测道132宕机后先选出来了一个leader,随后在进行一个投票,最后三票通过选择了133这台机器,所有到最后master就转换到了133,我们在133上进行一个查看:
我们重启一下132节点
可见已经被修改成了133的从节点了
RedisTemplate的哨兵模式
pom文件
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.rover12421</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
yml文件:
logging:
level:
io.lettuce.core: debug
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.18.132:26379
- 192.168.18.133:26379
- 192.168.18.134:26379
Redis配置类:
package com.wxk.redistest;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import io.lettuce.core.ReadFrom;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
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.StringRedisSerializer;
/**
* @author wxk
*/
@Configuration
@EnableCaching
public class RedisConf {
//用于实现哨兵模式
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
FastJsonConfig fastJsonConfig = fastJsonRedisSerializer.getFastJsonConfig();
SerializerFeature[] serializerFeatures = new SerializerFeature[] {SerializerFeature.WriteDateUseDateFormat, SerializerFeature.WriteMapNullValue};
fastJsonConfig.setSerializerFeatures(serializerFeatures);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
Controller类
@RestController
public class Controller {
@Resource
RedisTemplate redisTemplate;
@GetMapping("/test")
public String get(String key){
return redisTemplate.opsForValue().get(key)+"";
}
@GetMapping("/set")
public String set(String key,String value){
redisTemplate.opsForValue().set(key,value);
return "ok";
}
}
我们直接创建一个新的key
url如下:localhost:8080/set?key=time&value=time
获取key:localhost:8080/test?key=time
结果如下:
我们查看控制台:
可以看到从三个哨兵中选取了132的哨兵,随后获取相应的信息,由于第一步是写操作,写操作需要主节点来进行,所以最后选中了主节点:
Redis分片集群
分片集群结构
主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题
分片集群的特征:
- 集群中可以有多个master,每个master保存不同的数据
- 每个master都可以由多个slave节点
- master之间通过ping监控不辞健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确的节点
散列插槽
Redis会把每一个master节点映射到0~16383共16384哥插槽(hash slot)上,查看集群信息是就能看到
数据key不适于节点绑定的,而是与插槽绑定的。redis会根据key的有效部分计算插槽值,分为两种情况:
- key中包含"{}“,且”{}“中至少包含一个字符,”{}"中的部分是有效部分
- key中不包含"{}",整个key都是有效部分
计算方式是利用CRC16算法
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384进行取余
- 余数作为插槽,寻找插槽所在的实例即可
如何将同一类数据固定到同意Redis实例中?
将这一类数据使用相同的有效部分,例如key都可以以{wxk}作为前缀