单线程网络IO、KV读写

Redis的网络IO和KeyValue读写是由一个线程来完成的。
而Redis的持久化、异步删除、集群数据同步是额外的线程执行。

也由于Redis是单线程的,所以要特别小心耗时的操作,这些操作会阻塞后续指令。

简单来说就是处理事务一套、前台接待一套。不会因为前面办事导致人均等待时间太久。

Redis使用IO多路复用(epoll),将连接信息、事件放到队列中,使其能够处理并发的客户端连接。

socket: {
s0
s1
s2
s3
"..."
}

IO多路复用: {
s3 -> s2 -> s1 -> s0
}

事件处理器: {
连接处理器
命令请求处理器
命令回复处理器
}

socket -> IO多路复用 -> 文件事件分派器 -> 事件处理器

详解GET key

Redis相当于HashMap,也由于Hash是无序的,因此scan这样的流式查询,在查改场景中,可能会漏扫中途插入到前面下标的元素。

Redis持久化

RDB Snapshot

默认情况下,Redis将内存数据快照保存为dump.rdb,可以使用

save <time_duration> <row_insertion>

指示Redis多少秒内插入多少条数据后持久化到数据库
也可以直接用savebgsave命令写入数据库

bgsave 异步持久化

bgsave使用写时复制COW。bgsave从主线程fork出来,当主线程修改数据时,bgsave线程会将写入数据拷贝一份,然后写入rdb

Append-Only File

快照不能做到完全持久,假如服务宕机,可能会丢失几条写入。
这时候我们直接做个命令日志AOF,将执行的修改指令写入appendonly.aof

appendonly yes
appendfilename "appendonly.aof"

aof有三种模式appendfsync

  • always:立刻写入磁盘
  • everysec:每秒写一次
  • no:交给OS调度
    但是,由于aof是记录命令,需要执行时间,对于持久化大量数据比较耗时间。
    对于连续操作(如自增)aof会优化为1条命令,可以用bgrewriteaof命令手动重写
# 最小重构大小
auto-aof-rewrite-min-size 64mb
# 增长了100%,即128mb就重构
auto-aof-rewrite-percentage 100

Redis4 混合持久化

由于Redis重启时优先使用aof恢复数据,rdb利用率不高。因此出现了混合持久化

# 必须同时开启aof
aof-use-rdb-preamle yes
# 可以直接把快照关掉,因为混合持久化都写在aof里面

开启后,当aof重写时,会直接写入rdb,将rdb快照和aof增量存储在一起。
于是Redis重启可以先读rdb,再执行增量aof恢复数据,提高效率。

Redis主从

# redis-<your_port>.conf
pidfile /var/run/redis_<your_port>.pid
logfile "<your_port>.log"
# 数据存放目录
dir /usr/local/redis/data/<your_port>

### 主从复制
replicaof <main_redis_ip> <port>
# 从节点,只读
replica-read-only yes


### 启动
# 启动从节点
redis-server redis-<your_port>.conf
# 连接到从节点
redis-cli -p <minor_redis_port>

主从原理

master: {
rdb data
repl buffer
}
slave

slave -> master: 1. psync全量复制同步数据(通过socket长连接)
master.rdb data -> master.rdb data: 2.1 收到psync命令,执行bgsave生成最新rdb快照
master.repl buffer -> master.repl buffer: 2.2 主节点将增量写语句更新到buffer
master.rdb data -> slave: 3. 发送rdb数据
slave -> slave: 4. 清空旧数据,加载主节点rdb
master.repl buffer -> slave: 5. 发送缓冲区写命令
slave -> slave: 6. 执行主节点buffer写命令
master -> slave: 7. 主节点通过socket长连接,持续发送写命令给从节点,保持数据一致

断点续传

master: {
repl backlog buffer
}
slave

slave -> master: 1. 连接断开
master.repl backlog buffer -> master.repl backlog buffer: 2. 主节点增量写命令写入buffer
slave -> master: 3. 恢复socket长连接
slave -> master: 4. psync(offset)带偏移量
master -> slave: 5. 若offset在buffer中,断点以后的数据发送给从节点;否则,全量发送
master -> slave: 6. 持续发送buffer写命令,保持数据一致

如果存在很多从节点,那么主节点传输压力会比较大。可以采用树型架构,让从节点再给它的子节点传输数据。

哨兵高可用

sentinel_cluster: {
sentinel1 <-> sentinel2 <-> sentinel3 <-> sentinel1
}

client -> master <-> sentinel_cluster
master -> slave1
master -> slave2
client -> sentinel_cluster
sentinel_cluster <-> slave1
sentinel_cluster <-> slave2

哨兵会动态监听redis主节点,如果主节点挂了,哨兵会选择一个新redis示例作为主节点(通知给client端)

开启哨兵

# sentinel.conf

port 26379
pidfile <your_file>
logfile <your_file>
dir "<your_dir>"

# quorm是指多少个sentinel同时认为主节点挂了,才让master失效,一般设置为一半以上
sentinel monitor mymaster <redis_ip> <redis_port> <quorm>

启动哨兵./redis-sentinel sentinel.conf

Redis Cluster

当哨兵集群选举新节点的时候,服务会宕机几秒钟。因此我们需要Cluster

client1 -> RedisCluster
client2 -> RedisCluster
RedisCluster: Hash slot: CRC16(key) % 16384
RedisCluster -> Redis集群
Redis集群: {
master1 -> slave1-1
master1 -> slave1-2

master2 -> slave2-1
master2 -> slave2-2

master3 -> slave3-1
master3 -> slave3-2
}

在Cluster中,每个master数据是不重叠的,数据会被分片储存。通过Hash算法来决定存储数据到哪一个master节点。
使用Cluster,可以避免Redis服务完全宕机。
2的幂次取模小技巧:

Xmod2n=X & (2n1)X \mod 2^n = X \text{ \& } (2^n - 1)

Redis集群搭建

redis-cluster/
|-- 8000
| `-- redis.conf
|-- 8010
`-- 8020
  1. Redis配置
# ...其他配置

daemonize yes
port 8000
dir /path/to/redis-cluster/8000/
# 启用集群
cluster-enabled yes
cluster-config-file nodes-8000.conf
cluster-node-timeout 5000
# 密码
requirepass <your_password>
masterauth <your_auth_password>
  1. 启动所有master和slave节点
redis-server /path/to/redis-cluster/80*/redis.conf
ps aux | grep redis
  1. 开启集群
# replicas表示节点的副本,配置为1,则1主1从
redis-cli -a <your_auth_password> --cluster create --cluster-replicas 1 \
localhost:8000 localhost:8001 localhost:8002 ...

注意,第二次启动集群后,就不需要这一步了。节点会自动读取nodes-8000.conf文件,恢复上次集群状态。

  1. 进入redis节点验证配置
cluster info
cluster nodes

Redission原理

Thread1: {
Redission
}
Thread2: {
Redission
}

Thread1.Redission -> Try Lock
Try Lock -> 守护线程: 加锁成功
守护线程 -> Redis(Master): lock,每隔10s检查线程是否仍持有锁。如果持有,则延长锁失效时间

Thread2.Redission -> Try Lock
Try Lock -> Thread2.Redission: 加锁失败,使用while自旋尝试加锁

Redission利用了Redis Lua脚本保证原子操作。