Redis 学习笔记
Redis
RDBMS: Relational Database Management System
NoSQL: Not Only SQL
Redis 优点
在内存中运行, 速度快
以单线程模式运行, 因此没有线程切换的额外开销, 也不需要锁机制, 没有资源竞争
采用了 IO 多路复用机制, 使其在网络IO操作中能并发处理大量的客户端请求
Redis是单线程,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能,比如持久化、异步删除、集群数据同步等,都是由额外的线程执行的。
Redis 的缺点是使用单线程架构导致无法充分利用 CPU 多核特性。但是该问题可以通过单机多开来优化这个问题, 且 Redis 6.0+ 支持了多线程。
数据类型
在 redis-cli 中可以通过 object encoding <k1> 来查看数据的底层实现
推荐阅读: Redis 中的数据结构
string
短字符串 (embstr), 长字符串 (raw) 和 整数 (int)
embstr 与 raw 之间的界限是 44 个字节: 64 - 3(sdshdr8) - 16(redisObject) - 1(\0) = 44
list
压缩列表 (ziplist) , 双向循环链表 (linkedlist) 和 快速列表 (quicklist)
在 Redis 3.2 之前 list 的底层实现是 ziplist 和 linedlist, 但是在 3.2 之后 quicklist 替代了原来的底层实现
因此 ziplist, linkedlist 为
3.2-
, quicklist 为3.2+
hash
压缩列表 (ziplist) 和 哈希表 (hashtable)
hash 类型的 ziplist 是以两个节点为一组, 分别存放 field 和 value
set
set 的特点是无序, 没有重复的元素
有序数组 (intset) 和 哈希表 (hashtable)
有序数组可以直接使用二分搜索来查找数据
zset
压缩列表 (ziplist) 和 跳表 (skiplist)
ziplist 可以很方便的拿到两端的节点, 通过每个节点上的前继节点处理数据, 节省内存,但是太长的话性能低下。
linkedlist 可以很方便的处理数据, 但是占用内存太多。
quicklist 可以看成由多个 ziplist 组成的 linkedlist, 当 ziplist 节点的长度达到一定数量后新建一个 ziplist 节点添加到 linkedlist 中,性能高,而且节省内存。
skiplist 是可以实现二分查找的有序链表。
常用配置
redis 默认提供了两个配置文件 redis.conf 和 sentinel.conf
修改端口
port 6379
修改 pidfile
pidfile /var/run/redis_6379.pid
设置数据存放目录 (需要先创建该目录)
dir /data/redis/6379
开启外网连接:
bind 0.0.0.0
protected-mode yes 只有当 bind 未设置且 requirepass 未设置时只接收本机的连接, 但是上面绑定了 0.0.0.0, 因此保护模式不会生效
开启守护运行:
daemonize yes
command
CONFIG GET *
config set <key> <val> # 临时设置
info
monitor # 监视 Redis
# 连接命令
auth <pass>
echo <msg>
ping
quit
# 数据库操作
select 0
dbsize
flushdb
flushall
# 数据操作
keys *
type <key> # 获取数据的类型
sort <key>
move <key> <db>
del <key>
randomkey # 随机获取一个 key
rename <key> <newKey>
# 限时数据
expire <key> <second>
ttl <key> # 查看数据剩余有效时间
persist <key> # 移除数据的过期时间
# 主从复制
replicaof/slaveof
# 订阅者模式
PSUBSCRIBE pattern [pattern …] # 订阅一个或多个符合给定模式的频道
PUBSUB subcommand [argument [argument …]] # 查看订阅与发布系统状态
PUBLISH channel message # 将信息发送到指定的频道
PUNSUBSCRIBE [pattern [pattern …]] # 退订所有给定模式的频道
SUBSCRIBE channel [channel …] # 订阅给定的一个或多个频道的信息
UNSUBSCRIBE [channel [channel …]] # 退订指定的频道
# 事务
watch
unwatch
multi
exec
discard
string
set k1 v1
mset k1 v1 k2 v2 k3 v3 # 不支持集群操作, 因为无法确保数据都 hash 在同一个数据槽中
get k1
mget k1 k2 k3
getset <key> <newValue> # 获取旧值并设置新值
exists <key>
strlen <key> # 获取字符串长度
setnx <key> <value> # 当该键不存在时设置数据
incr <key>
decr <key>
incrbyfloat <key> <step> # 增长指定数值, 步长可以为负数
incrby <key> <step>
decrby <key> <step>
append <key> <value>
getrange <key> <start> <end> # 类似 python 中的字符串切片
setrange <key> <offset> <value> # 替换指定索引位置上的内容
list
Rpush <key> [val ...]
Lpush <key> [val ...]
Rpop <key>
Lpop <key>
Lindex <key> <index> # index 为负数时表示从右往左的索引
Lrange <key> <start> <stop> # 类似于 python 中的切片操作
Ltrim <key> <start> <stop> # 移除指定范围之外的数据
Lset <key> <index> <newVal>
Linsert <key> <BEFORE|AFTER> <target> <val>
Lrem <key> <count | -count | 0> <val>
len <key>
BRpop <key> [key ...] <timeout> # 获取数据, 如果没有数据则在超时时间内进行等待
BLpop <key> [key ...] <timeout> # 超时则返回 nil 和等待时间, 否则返回指定数据的键和值
RpopLpush <sourceKey> <destKey> # 从源列表中右侧弹出数据压入目标列表左侧并返回该数据
BRpopLpush <sourceKey> <destKey> <timeout>
hash
hset <key> <field> <val> # 设置一条数据
hmset <key> <field> <val> [field val ...] # 设置多条数据
hget <key> <field>
hget <key> <field> [field ...]
hlen <key>
hkeys <key>
hvals <key>
hgetall <key> # 获取 hash 中的所有数据, key/val 两行为一组
kexists <key> <field> # 判断 hash 中是否含有指定的键
hsetnx <key> <field> <val>
hdel <key> <field> [field ...]
hincrby <key> <field> <step>
hincrbyfloat <key> <field> <step>
set
sadd <key> <member> [members ...]
scard <key> # 获取 set 长度
smembers <key> # 获取 set 中的所有数据
sismember <key> <val>
srem <key> <member> [members ...]
spop <key> [count] # 随机弹出指定数量的 member, 因为 set 是无序的
smove <sourceKey> <destKey> <member> # 移动 sourceKey 中的数据到 destKey 中
sdiff <fromKey> <toKey> [otherKey ...] # 差集运算, 返回只在 fromKey 中的 member
sinter <fromKey> <toKey> [otherKey ...] # 交集运算
sunion <fromKey> <toKey> [otherKey ...] # 并集运算
sdiffstore <destKey> <fromKey> [otherKey ...] # 将差集存放到指定 destKey 中, 可以用于复制数据
sinterstore <destKey> <fromKey> [otherKey ...]
suniontore <destKey> <fromKey> [otherKey ...]
zset
zadd <key> <order> <val> [order val ...] # 向有序集合中添加元素, order/val 为一组数据
zscore <key> <member> # 查询指定元素的 order
zrem <key> <member> [member ...]
zrange <key> <start> <stop> [withscores] # 升序查看元素
zrevrange <key> <start> <stop> [withscores] # 降序查看
zrangebyscore <key> <min> <max> [withscores] [limit offset count] # 查看指定范围内的数据
zcard <key> # 获取元素总数
zcount <key> <min> <max> # 获取执行范围内的元素总数
zincrby <key> <step> <member>
过期清除机制
redis 中可以对数据设置有效期, 其原理是 Redis 内部维护了一个过期字典。
-
惰性删除: 取值时判断是否过期, 但是如果过期的数据不再进行取值操作就会一直存在, 浪费内存空间
-
定时删除: 插入带有有效期的数据时启用一条线程在该数据到期后删除数据, 但是当存在大量定时数据时, 需要巨额的线程开销
-
定期删除: 程序启动时同时启动一条守护线程定期遍历数据, 删除掉过期数据, 但是如果在清理线程执行间隔对已过期的数据取值时会出现脏读现象
-
定期 + 惰性: Redis 采用了【定期】 + 【惰性】的清理机制, 通过配合使用这两种删除策略, 服务器可以很好地在合理使用 CPU 时间和避免浪费内存空间之间取得平衡。 (但是在定期清理线程中每次只遍历部分随机数据, 而非全部数据)
内存淘汰策略
当 Redis 占用内存超过配置文件中的 maxmemory 参数时, 就会触发内存淘汰机制, 来删除部分数据以释放内存空间。
- 【volatile-lru】: 当内存不足以容纳新写入数据时, 在过期字典中使用 LRU 算法 (最久未使用) 得到一部分 key, 然后将它们淘汰
- 【allkeys-lru】: 当内存不足以容纳新写入数据时, 在数据字典中使用 LRU 算法得到一部分 key, 然后将它们淘汰 (推荐使用)
- 【volatile-lfu】: 在过期字典中使用 LFU 算法 (使用频率最低) 淘汰数据
- 【allkeys-lfu】: 在数据字典中使用 LFU 算法淘汰数据
- 【volatile-random】: 在过期字典中随机淘汰数据
- 【allkeys-random】: 在数据字典中随机淘汰数据
- 【volatile-ttl】: 在过期字典中淘汰掉将要过期的数据
- 【noeviction】: 不淘汰任何数据, 返回错误给调用者
从过期字典选取 key 的所有策略中, 如果最终仍无法完成内存释放任务, 则会同 noeviction 策略一样抛出异常
数据持久化
由于 redis 在运行时是将数据存放在内存中的, 因此需要在关闭 redis 之前将内存中的数据持久化到硬盘, 以便于再次载入这些数据, 避免数据丢失。
RDB: 内存数据的快照
AOF: 所有修改 redis 数据的指令日志
当Redis做RDB或AOF重写时,都是通过执行fork操作创建子进程来完成的
RDB
# 官方默认开启 rdb, 快照文件名为 dump.rdb
save 900 1
save 300 10
save 60 10000
# 通过设置 save 参数为空即可关闭 rdb 备份
save ""
-
生成 rdb 文件共有三种方式: save, bgsave 和 自动触发
-
其中 save 指令会阻塞 redis 直至文件保存完毕, 而 bgsave 是通过 fork 子线程来生成 rdb 文件
-
自动触发的情况有: 根据配置文件的 save 配置触发 bgsave, flushall 指令触发 和 shutdown 指令触发
AOF
# 官方默认关闭 aof, 改为 yes 即可开启
appendonly no
修改 aof 保存策略
# aof 文件也并不是直接将每条修改指令写入到硬盘中的, 而是先将修改指令追加到 aof_buf 缓存中, 并写入到 AOF 文件, 而最终同步到硬盘的工作根据保存策略来执行
appendfsync everysec
# no: 不自动调用 fsync, 由操作系统决定何时刷新 aof 缓存到磁盘, 高效率
# always: 每次写操作都会刷新 aof 缓存到硬盘, 高安全性
# everysec: 每秒执行一次 aof 缓存刷新, 以上二者的中和, 平衡了性能和数据安全
-
与 rdb 不同的是 aof 文件中保存的是对 redis 中数据的修改指令
-
当过期的数据被删除后, redis 会向 AOF 文件追加一条 DEL 命令
-
当 aof 文件过大时会新建一份移除了冗余指令的 aof 文件, 用于替换之前体积较大的 aof 文件
混合持久化
Redis 4.0 之后可以通过在配置文件中设置 aof-use-rdb-preamble yes 指定启用 rdb + aof 增量混合日志, 改善了 aof 大文件的问题。
混合持久化文件仍未 aof 文件, 只是在文件的开头插入了 rdb 数据。
RDB 和 AOF 的区别
- rdb 快照文件保存的是内存中的有效数据, 而且该文件经过了压缩, 但是由于 rdb 备份的同步周期相对较长, 因此会存在很大的数据丢失的可能性 (恢复大文件较快)
- aof 日志文件则保存的是 redis 中对数据的修改指令, 该文件无法压缩, 但是 aof 的可以通过高频率的备份机制最大限度的保证数据完整性 (文件体积相对 rdb 更大, 且恢复大文件较慢)
因此选用哪种持久化机制取决于对 redis 中数据的敏感性, 如果对内存中的数据不敏感, 你甚至可以同时关闭 RDB 和 AOF
当RDB与AOF两种方式都开启时, Redis会优先使用AOF文件恢复数据, 因为AOF文件中保存的文件比RDB文件更完整。
主从复制
主从模式可以实现数据的读写分离, 减轻主机的压力
-
Redis 的主从配置中, 主机不需要额外的操作, 只需要在从机上配置主机信息即可。
-
在主从模式下主机 down 以后, 不影响从机的读操作, 只是会暂停数据同步, 当主机再次上线后从机自动重连到主机上
-
replicaof 是 slaveof 的代替命令[5.0+]
在 client 中指定主机
启动 redis 实例并通过 redis-cli 连接到实例中, 然后在实例中执行以下命令
replicaof <MasterIp> <MasterPort>
从机 down 以后, 不影响其他节点, 但是再次上线后不会自动以从机的身份挂载到主机上
通过配置文件指定主机
# 通过在配置文件中写入 replicaof 指令可以实现从机上线自动挂载
replicaof <MasterIp> <MasterPort>
# 如果主机设置了密码则需要配置主机密码否则无法完成数据同步
masterauth <MasterPass>
# 配置文件中默认从机不允许执行写操作, 改为 yes 后从机可以在自己的数据库中写入数据, 但是不会和任何节点进行同步
replica-read-only yes
主从级联模式
一级从机可以继续向下拓展从机, 来减轻主机的向一级从机同步数据的压力。
同理可实现多层次的主从模式, 但是无论是一主多从模式还是主从级联模式都只有一个执行写操作的主库。
查看主从服务状态
info replication
# 查看当前角色
role
-
主机可以查看所有从机地址及状态信息
-
从机只能查看主机地址及自己的状态信息
取消从机角色
replicaof no one
不会删除数据库中已有数据, 此时可以执行任意读写命令
哨兵模式
哨兵模式解决了主机下线后系统无法继续执行写操作的问题。
- 哨兵模式可以在主机宕机后重新选择新任主机并把其他从机挂载到新任主机上, 以代替原 master 的任务。
- 一旦重新指定了新任主机, 原主机再上线后也将成为新任主机的从机。
- 从机只有在初始挂载时进行数据全量同步, 后续数据由主机向从机发送增量更新。
- 哨兵的任务: 监控主机状态、选择新任主机、通知从机修改配置切换新主机。
配置哨兵
哨兵本质上也是一个 server, 只是它不提供数据读写之类的服务
# 复制一份没有注释和空行的 sentinel 配置文件
cat sentinel.conf | grep -v "^#" | grep -v "^$" > sentinel-26379.conf
# 监控指定主机并命名为 mymaster, quorum 设为 1 表示: 至少有 1 个 sentinel 检测到主机不可达时, sentinel 才能确定主机已经客观下线, 此时会发生故障转移
sentinel monitor mymaster 127.0.0.1 6379 1
# 如果主机设置了密码, 则需要配置主机密码
sentinel auth-pass mymaster <MasterPass>
# 启动哨兵
redis-sentinel sentinel-26379.conf
哨兵集群
可以通过配置哨兵集群来降低哨兵误判的概率
- 哨兵实例之间可以互相发现, 靠的是 Redis 提供的 pub/sub 机制, 也就是发布/订阅机制。哨兵和主库建立连接, 就可以在主库上发布消息, 比如发布自己的 IP 和端口, 同时它也会从主库上订阅消息, 获得其他哨兵的连接信息。
- 哨兵通过向主库发送 info 指令来获取所有从库的信息
Leader 选举流程
- 当某个 sentinel 发现 Master 下线后, 它会主观认为 Master 已经下线, 此时向其它的 sentinel 发送 is-master-down-by-addr 命令来确定主机是否已客观下线
- 当 sentinel 确定主机已经客观下线时, 会通知其他 sentinel 投票自己担任 Leader
- 收到投票通知的 sentinel 如果还没有同意过其它 sentinel 的投票, 就同意该投票通知, 否则拒绝该投票通知
- 当某个 sentinel 的投票数量超过哨兵总数的 1/2 (因此哨兵的数量常设置为奇数) 且超过监控配置中的 quorum 时自动当选 Leader
故障转移流程
- 根据评分标准确定新任主机
- 通知其他 replica 切换主机配置信息
- 通知客户端主从变化
- 当旧的 master 重新上线时, 将其设为新任主机的 replica
新任主机评分标准
- 从机优先级最高者当选新任主机, 当无法确定时向下执行判定
- 从机复制偏移量最大者当任新任主机, 当无法确定时向下执行判定
- 从机 ID 最小者担任新任主机
哨兵服务命令
# 显示被监控的所有master以及它们的状态
SENTINEL masters
# 获取指定名称下的 master 的信息
SENTINEL master <MasterName>
# 获取指定名称下的 replica 信息
SENTINEL slaves <MasterName>
总结
在哨兵模式的架构下, 哨兵列表成为了客户端通信的入口, 此时对于客户端来说哨兵列表担任了配置中心的角色, 客户端通过哨兵中心获取真正的 master 主机 和 replica 主机, 最终与 master 主机进行写操作通信, 与 replica 主机进行读操作通信。
而对于主机和从机列表来说哨兵的任务就是: 监控主机状态、选择新任主机、通知从机修改配置切换新主机。
Redis 集群
哨兵模式的架构下也仍然只有一个执行写操作的主机, 完整的数据都存放在一台主机上。随着整个系统中的数据量的不断增大, 终究会使 master 主机无法承受巨额的数据处理任务, 最终宕机。因此可以通过使用 Redis 集群技术将数据分区存放, 以减小单台主机的压力, 从而提高了系统的可用性, 数据安全性和扩展性。
- Redis 集群使用的是数据槽机制, 没有使用 Hash 一致性算法。
- 当集群中的任意一组主从节点都下线时, 整个集群将无法继续提供服务。
数据共享机制
在单机模式或主从复制模式下, 一个 Redis 实例中默认都会创建 16 个 db, 而在集群模式下 Redis 将 16K (16384) 个数据槽分配到各个集群内的主机上, 在存储/获取数据时通过对该数据的 key 进行指定的数据运算得到目标数据槽序号, 然后经过预先分配的数据槽存放规则得到该数据最终存放的主机信息, 并通过请求转发的机制在目标数据槽中放入/取出该数据。
为什么数据槽数量设置为 16K?
Redis 的作者的解释是:
- 减小通信数据包的体积
- 控制集群的规模在 1000 以内
搭建步骤
在 Redis 5.0 之后 redis-trib.rb 被集合到 redis-cli 里, 可以直接使用 redis-cli --cluster 来管理集群
# 配置文件中需要开启集群功能
cluster-enabled yes
# 存放集群信息的文件
cluster-config-file nodes-6900.conf
# 节点超时检测时间
cluster-node-timeout 15000
# 配置文件修改完成后, 先使用 redis-server 启动所有的集群节点
# 新建集群, 集群中每台主机下分配一台从机 (三主三从)
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:6900 127.0.0.1:6901 127.0.0.1:6902 127.0.0.1:6903 127.0.0.1:6904 127.0.0.1:6905
# 删除节点上的集群信息 (当节点不为空时无法添加到集群中)
redis-cli -p 6900 cluster reset hard
# 检查集群状态
redis-cli --cluster check 127.0.0.1 6900
# 查看集群数据信息
redis-cli --cluster info 127.0.0.1 6900
# 帮助信息
redis-cli --cluster help
向集群中添加新节点
# 集群主节点扩容, 然后通过 info 指令可以查看新增节点 id (2d383cd3f73f814ec391394e3f7e0748fcb83db2)
redis-cli --cluster add-node 127.0.0.1:6906 127.0.0.1:6900
# 为主节点挂载从节点 (通过 127.0.0.1:6900 就可以找到之前添加的节点)
redis-cli --cluster add-node 127.0.0.1:6907 127.0.0.1:6900 --cluster-slave --cluster-master-id 2d383cd3f73f814ec391394e3f7e0748fcb83db2
# 为所有节点重新均分数据槽
redis-cli --cluster rebalance 127.0.0.1:6900
# 以交互模式修改集群中数据槽分配情况
redis-cli --cluster reshard 127.0.0.1:6900
删除节点的步骤与添加节点顺序相反, 需要先使用 reshard 将欲删除节点上的数据槽转移到其他节点, 然后 del-node 即可。
拓展
PUB/SUB
# command, 订阅指定频道
SUBSCRIBE "blog.redis"
SUBSCRIBE "blog.redis" "blog.rocketmq"
# 订阅/退订指定模式频道
PSUBSCRIBE|PUNSUBSCRIBE "blog.r*"
PSUBSCRIBE|PUNSUBSCRIBE "blog.r*" "blog.j?va" "blog.j[ae]va"
# 退订指定频道
UNSUBSCRIBE "blog.redis"
UNSUBSCRIBE "blog.redis" "blog.rocketmq"
# 发送消息
PUBLISH "blog.redis" "redis-in-action-01"
# 查看频道列表
PUBSUB CHANNELS [pattern]
# 查看频道订阅数量
PUBSUB NUMSUB <channel>
# 查看模式频道数量
PUBSUB NUMPAT
事务
- Redis 中事务以
MULTI
命令开始,然后将多个命令放到事务当中,最后由EXEC
命令来提交事务的运行或DISCARD
来放弃执行事务。 - 事务期间不会影响其他客户端的操作。
- 如果在开启事务之前 watch 了指定数据, 且该数据被其他客户端修改, 那么事务不会成功提交。
- 如果在事务期间执行了错误的命令那么事务不会成功提交。
- 如果事务提交后某条命令出错了, 那么不会影响其他命令的执行, 且没有回滚操作。