redis数据结构原理
待整理~
redis持久化
RDB持久化
1.执行流程
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。
- 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换,替换后子进程信号通知主进程
2.rdb自动持久化配置:
时间策略要按照实际情况配置多条,数据的存储时不均匀的,高峰期短时间间隔要存一次,低峰期长时间间隔要存一次。
# 文件名称
dbfilename dump.rdb
# 时间策略
save 900 1
save 300 10
save 60 10000
# 文件保存路径
dir /etc/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
3.rdb策略将内存中的数据生成快照保存到磁盘,是全量存储,内存大的话会比较耗时,大量磁盘io。
4.持久化的触发方式:
- save命令:client向server发送save命令,同步阻塞。
- bgsave命令:server中执行命令,异步子进程执行。
自动持久化,通过save配置项完成。
5.rdb文件恢复:如果开启了aof,则不生效。如果没开启aof,则寻找dir配置下的rdb文件。
AOF持久化
1.aof同步步骤:
- Redis收到写命令后首先会追加到AOF缓冲区aof_buf(write)
- 通过一定时机(配置决定),调用系统函数 fsync() 把AOF缓冲区的数据刷到磁盘
2.落盘时机配置(fsync):
- always: 命令写入aof缓冲区后立即调用系统fsync操作同步到AOF文件,数据绝对安全。
- no:有操作系统决定何时调用fsync()
- everysec:每秒调fsync()一次。 若损失数据只损失1秒,对于大多数系统来说足够了。
3.文件重写rewrite
- 由父进程fork子进程进行重写
- 使用写时重写技术,在重写完成期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。
- 重写文件完成后,将aof_rewirte_buf数据输入新文件
- 用新aof文件替换老aof文件。
redis集群三种模式
主从模式(实现主从分离,提高吞吐,多机备份)
从数据库一般都是只读的,并且接收主数据库同步过来的数据
要点:
- 主从复制还是哨兵和集群能够实施的基础,主从复制是Redis高可用的基础。
- 再看CAP,由于redis复制是异步复制,会导致短时的数据不一致,所以无法满足一致性C,但可以保证当网络分区发生时,各个节点依旧可用。redis主从模式是CP,而不是AP。redis会使用最终一致性策略,保证主从同步数据一致。
- 主从复制实现原理:
6大步骤: 1. 设置主节点ip及端口、2. 建立套接字socket、 3. 发送ping命令、 4. 权限验证 、 5. 同步 6. 命令传播
设置主节点ip及端口: 使用slaveof命令,可配置文件使用、可命令行使用
复制阶段:从节点向主节点发送psync或sync命令(2.8版本之前),可以实现全量复制与增量复制
命令传播:当通过复制阶段后,主从节点进入命令传播阶段,主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。命令传播是异步的,主节点并不会等从节点同步执行完命令,这样会导致“延迟不一致”现象。
- 延迟不一致现象:网络波动、写命令频率过高,会导致数据延迟不一致。同时,repl-disable-tcp-nodelay配置也会影响,设置为yes,会将代发数据合并发送,延迟大概40ms(取决于系统),如果设置为no,写命令实时发送同步。
- 全量复制和部分复制
1.全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作
2.部分复制:用于网络中断等情况后的复制,只同步网络断开期间的缓存写数据,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
- 全量复制原理剖析:
fork子进程,开始执行bgsave,生成RDB文件,同时开辟缓冲区记录从现在开始的写命令。2. 将rdb文件传输给从服务器,从服务器执行完毕后,主节点将缓冲区写数据同步到从节点,保证最终一致性。3. 如果从节点开启了AOF,会触发bgrewriteof,从而保证从节点的aof文件最新。
- 部分复制原理剖析:
1.复制偏移量:与mysql类似,主从节点各自维护offset字段,不一致则复制
2.复制积压缓冲区:底层结构是定长、先进先出FIFO的队列,可以repl-backlog-size参数设置大小。 当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
3.服务器运行id(runid):每个从节点都存储着主节点同步下来的runid,当网络分区发生时,重连后slave节点会判断同步的runid是否存在,如果存在优先考虑增量复制,如果不存在,则全量复制。
- psync命令复制原理:主节点收到psync命令后,进行判断,如果命令为psync命令,则执行全量复制。如果收到的为psync
{runid} {offset}命令,则执行增量复制。执行增量复制过程中如果offset差超过了buffer,则执行全量复制。 - 心跳检测机制及其作用:
- 检测主从链接 2. 辅助实现min-slaves选项 3. 检测命令丢失,保证数据一致
通过判断在线的从节点数量,实现min-slaves。
min-slaves概念,保证高可用:
未达到下面两个条件时,写操作就不会被执行
min-slaves-to-write 3 #最少包含的从服务器
min-slaves-max-lag 10 #延迟值
心跳会返回offset信息,通过offset判断从节点的数据同步是否及时,不及时则通过offset补发数据 - 主从复制超时原理:断开连接后会尝试重连。
主节点判断超时:每秒1次调用复制定时函数replicationCron(),如果时间大于到上次REPLCONF ACK的时长,则断开连接,释放资源(缓冲器、连接、带宽),超时时间由参数repl-timeout值控制。
从节点判断超时:主要也是由repl-timeout参数控制。1. 连接建立阶段,若大于时间则断开连接。 2. 全量复制阶段:如果收到rdb的时间超时,则断开连接。 3. 命令传播阶段:如果收到主节点ping的时间过长,超过timeout,则断开连接。
如果rdb文件过大,会导致一直同步失败,会无线重连,应适当调大timeout值
- 主库挂了发生什么?
不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务。
如果slave-serve-stale-data参数设置为yes,主节点挂掉后从节点可以继续提供服务,如果设置为no,主节点挂掉后从节点不再提供读数据服务,仅提供info、slaveof等少量命令。在强一致场景需要考虑设置为no,如分布式锁,如果主节点挂掉,数据没来得及同步从节点,会导致从节点读不到,锁失效。
重启master节点需要保证rdb文件或者aof文件是最新。
- redis如何保证主从服务器连接正常且数据最终一致?
命令传播阶段,从服务器会利用心跳检测机制定时的向主服务发送消息。
- 如果提高数据实时一致性?
优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;
- java客户端连接redis如何实现读写分离,读负载均衡?
常见的客户端有jredis,lettuce,redission。以lettuce为例,可以使用
StatefulRedisMasterSlaveConnection connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), redisURI);
connection.setReadFrom(ReadFrom.NEAREST);
// MASTER 主读
// SLAVE 从读
// MASTER_PREFERRED 优先master -> slave
// SLAVE_PREFERRED 优先slave -> master
// NEAREST 最近节点读
// 实现很简单,重写ReadFrom的select方法,自带的方法均无法实现负载均衡
static final class ReadFromSlavePreferred extends ReadFrom {
@Override
public List<RedisNodeDescription> select(Nodes nodes) {
List<RedisNodeDescription> result = new ArrayList<>(nodes.getNodes().size());
//优先添加slave节点
for (RedisNodeDescription node : nodes) {
if (node.getRole() == RedisInstance.Role.SLAVE) {
result.add(node);
}
}
//最后添加master节点
for (RedisNodeDescription node : nodes) {
if (node.getRole() == RedisInstance.Role.MASTER) {
result.add(node);
}
}
return result;
}
// 自定义负载均衡
@Bean(destroyMethod = "close")
StatefulRedisMasterSlaveConnection<String, String> statefulRedisMasterSlaveConnection(RedisClient redisClient, RedisURI redisURI) {
StatefulRedisMasterSlaveConnection connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), redisURI);
connection.setReadFrom(new ReadFrom() {
@Override
public List<RedisNodeDescription> select(Nodes nodes) {
List<RedisNodeDescription> list = nodes.getNodes();
Collections.shuffle(list);
return list;
}
});
return connection;
}