基本概念
- 名称: Remote Dictionary Service
- 特点:
- 基于内存的快速访问
- 以key-value形式存储
- 用途:
- 作为缓存,存储不经常改变的数据
- 作为数据库,存储热点数据
- 作为消息队列,实现异步通信(用Redis Stream)
- 场景:
- 用户登录后端保存短信验证码,并设置失效时间
- 存储Session数据,实现分布式缓存【Servlet Session默认运行在内存中,持久化到硬盘上】
- 同类(NoSQL): Mongodb、MemCached
- 存储结构:
- 默认16个db(0~15)
- 数据类型:
- 键规则:
- 不能重复
- 作用: 标识存储的数据
- 数据类型:
string
- 命名规则: 不能太长(因为查询的效率低,查询起来不方便), 不能太短(容易重复,同时可读性也差), 可以采用
MORNINGSTAR_STU_LIST
的格式
- 值规则: 值支持五种数据类型
string
: 字符串类型【e.g. 用户个人介绍】- 最基础的数据存储类型,无论存入的是字符串、整数、浮点类型都会以字符串写入
- 最多可以容纳的数据长度是512M
- 其他类型都可以存储4G个字符串
hash
: 键和值都是string
类型的Map容器【e.g. 会话数据】list
: 按照插入顺序排序的字符串链表【e.g. 消息队列、评论列表】set
: 字符串集合【e.g. 用户好友列表】sorted set/zset
: 有序的字符串集合,每个元素有一个分数用来决定它的顺序【e.g. 权重排行榜】
- 键规则:
- 并发访问: 使用单线程模型来处理客户端的命令请求,让所有的命令执行都在一个线程中按顺序执行(无锁队列),避免了多线程带来的复杂性和线程安全问题
常用命令
string
:set <key> <value>
: 添加或修改一个键和值,键不存在就是添加,存在就是修改get <key>
: 获取值,如果存在就返回值,不存在返回nil
del <key1> [<key2>...]
: 删除指定的键,返回删除的个数setex <key> <seconds> <value>
或set <key> <value> ex <seconds>
: 设置指定键的值和过期时间expire <key> <seconds>
: 设置一个过期时间【存在键返回1,否则返回0】setnx <key> <value>
或set <key> <value> nx
: 保存键值对,如果键存在则不保存,不存在则保存mset <key1> <value1> [<key2> <value2>...]
hash
:hset <key> <field> <value>
hget <key> <field>
hmset <key> <field1> <value1> [<field2> <value2>...]
hmget <key> <field1> [<field2>...]
hdel <key> <field1> [<field2>...]
hgetall <key>
hkeys key
hvals key
list
:lpush <key> <value1> [<value2>...]
: 从左向指定的键中添加元素,返回元素个数rpush <key> <value1> [<value2>...]
lpop <key>
: 从左边删除一个元素,返回被删除的元素rpop <key>
lrange <key> <startIndex> <endIndex>
: 得到键中[startIndex
,endIndex
]范围内的元素【从左向右:0
~n
,从右向左:-1
~-(n+1)
,所有元素:0
~-1
】lindex <key> <index>
: 查询指定索引的元素llen <key>
: 获取列表的长度brpop <key> <seconds>
: 移出并获取最后一个元素,无则元素阻塞至超时【Blocking Right POP】lrem <key> <count> <value>
: 从表头删除指定个数的元素
set
:sadd <key> <value1> [<value2>...]
smembers <key>
: 得到这个集合中所有的元素sismember <key> <value>
: 判断元素在集合中是否存在,存在返回1,不存在返回0srem <key> <value1> [<value2>...]
sinter <key1> <key2> [<key3>...]
: 返回给定所有集合的交集
zset
: 【分数是double
的,底层是ziplist(压缩表)或skiplist(跳跃表)】zadd <key> <score1> <value1> [<key2> <value2>...]
zrange <key> <startIndex> <endIndex> [withscores]
zrevrange <key> <startIndex> <endIndex> [withscores]
zrem <key> <value1> [<value2>...]
zcard <key>
: 得到元素个数zrank <key> <value>
: 得到元素的索引号zscore <key> <value>
geo
: 地理信息geoadd
:GEOADD key longitude latitude member [longitude latitude member ...]
geopos
:GEOPOS key member [member ...]
geodist
:GEODIST key member1 member2 [m|km|ft|mi]
georadius
:GEORADIUS key longitude latitude radius m|km|ft|mi
georadiusbymember
:GEORADIUSBYMEMBER key member radius m|km|ft|mi
geohash
:GEOHASH key member [member ...]
- 通用:
rename <key1> <key2>
keys <pattern>
: 查询键,可以使用*
匹配多个字符,使用?
匹配1个字符del <key1> [<key2>...]
exists <key>
type <key>
select <dbId>
move <key> <dbId>
ttl <key>
: 返回键的剩余生存时间,以秒为单位
【如果键不存在或者已过期,返回-2
;如果key存在并且没有设置过期时间(永久有效),返回-1】flushall
: 清空redis所有的库
- 其他:
- 认证:
auth <password>
- 认证:
持久化机制
- 定义: 将内存中的数据同步到磁盘
-
方式: 【一般在企业开发中两种持久化机制会配合着使用】
- RDB: (Redis DataBase) 默认的持久化方式,以二进制的方式将数据写入文件中【每隔一段时间写入一次】
- 默认文件名:
dump.rdb
- 使用:
- 配置:
save <seconds> <num>
<seconds>
秒内执行过<num>
次set操作,则调用bgsave(异步save)持久化一次- 可以同时配置多个
save
命令 save ""
: 不使用RDB存储,不能主从
- 手动: 调用
save
或bgsave
- 配置:
- 优缺点:
- 优点:
- 持久化空间效率高(磁盘占用小),持久化的是内存中的数据
- 数据库宕机后,数据恢复的效率要更高
- 性能最大化【由子进程完成持久化工作】
- 缺点:
- 可能导致数据丢失
- 优点:
- 默认文件名:
- AOF: (Append Only File) 以文本文件的方式记录用户的每次操作,数据还原时候,读取AOF文件,模拟用户的操作,将数据还原
- 默认文件名:
appendonly.aof
- 启动AOF:
appendonly yes
- 同步策略(
appendfsync
):- 每秒同步(
everysec
) - 每修改同步(
always
) - 完全依赖操作系统(
no
)【每30秒一次】
- 每秒同步(
- AOF文件格式:
*2
: 表示有2个命令$6
: 表示下一个词有6个字符
- 重写优化:
- 配置
#当前aof文件大小超过上一次aof文件大小的百分之多少时进行重写。如果之前没有重写过,以 启动时aof文件大小为准 auto-aof-rewrite-percentage 100 #限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化 auto-aof-rewrite-min-size 64mb
- 手动:
bgrewriteaof
- 配置
- 优缺点:
- 优点:
- 数据安全性更高
- 缺点:
- 恢复速度比RDB更慢
- 磁盘占用比RDB更大
- 优点:
- 默认文件名:
- RDB: (Redis DataBase) 默认的持久化方式,以二进制的方式将数据写入文件中【每隔一段时间写入一次】
-
实践:
- 如当前只追求高性能,不关注数据安全性,则关闭RDB和AOF,如redis宕机重启,直接从数据源恢复数据
- 如需较高性能且关注数据安全性,则开启RDB,并定制触发规则
- 如更关注数据安全性,则开启AOF
单线程设计
- 原因:
- 在单线程模型中,可以避免多线程带来的上下文切换和线程同步的开销,从而提高性能
- 单线程模型简化了代码的复杂性,减少了并发编程中常见的问题,如死锁、竞态条件等
- 虽然Redis是单线程的,但它主要受限于内存和网络带宽,而不是CPU,因此单线程设计不会成为性能瓶颈
- 单线程模型使得 Redis 的行为更加可预测,因为所有的操作都是顺序执行的
- 发展: Redis 6.0引入了多线程功能,用于处理客户端的输入输出操作,进一步提高了性能,但这并不改变Redis核心操作的单线程特性
事务机制
- 性质: 弱事务
- 特点:
- 某操作编译出错(语法问题)后,事务无法执行
- 某操作执行出错后,继续执行后面的操作
- 能保证事务中的操作不被事务外的操作干扰
- 使用:
#开启事务 multi #添加命令 sadd user:1001:follow 1002 sadd user:1002:follow 1001 sadd user:1001:fans 1002 sadd user:1002:fans 1002 #执行事务 exec # 取消事务 discard
高可用
- 目标: 减少系统的停机时间,提高服务的稳定性和可靠性
- 总结:
- 主从模式: 读写分离,负载均衡,一个Master可以有多个Slaves
- 哨兵模式: 监控,自动转移,哨兵发现主服务器挂了后,就会从slave中重新选举一个主服务器
- 分片集群: 将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,提高并发量和数据容量
主从复制
- 定义: 将一台Redis服务器的数据复制到其他Redis服务器的过程
- 数据同步
- 步骤:
- 全量复制与部分复制:
- 全量复制: 主节点发送所有数据给从节点
- 部分复制: 在网络闪断后,主节点只发送丢失的数据给从节点
- 使用:
- 配置:
- 主:
bind 0.0.0.0 protected-mode no port 6379 daemonize yes requirepass "123456" logfile "/usr/local/redis/logs/redis_6379.log" dbfilename "redis_6379.rdb" dir "/usr/local/redis/data" appendonly yes appendfilename "appendonly_6379.aof" masterauth "123456"
- 从1:
bind 0.0.0.0 protected-mode no port 6380 daemonize yes requirepass "123456" logfile "/usr/local/redis/logs/redis_6380.log" dbfilename "redis_6380.rdb" dir "/usr/local/redis/data" appendonly yes appendfilename "appendonly_6380.aof" masterauth "123456" replicaof 192.168.200.130 6379
- 从2:
bind 0.0.0.0 protected-mode no port 6381 daemonize yes requirepass "123456" logfile "/usr/local/redis/logs/redis_6381.log" dbfilename "redis_6381.rdb" dir "/usr/local/redis/data" appendonly yes appendfilename "appendonly_6381.aof" masterauth "123456" replicaof 192.168.200.130 6379
- 主:
- 查看:
info replication
- 配置:
- 实践:
- 读写分离: 在主从复制的基础上,可以将读操作分散到多个从节点上执行,而写操作则在主节点上执行
- 持久化优化: 从节点上开启持久化、在主节点关闭持久化
- 主节点宕机的解决: 将从节点提升为主节点
- 选择一从节点,提升为主节点:
slaveof no one
- 将其他节点的主节点设置为新的主节点:
slaveof 192.168.200.130 6380
- 选择一从节点,提升为主节点:
- 主节点宕机的解决: 将从节点提升为主节点
- 作用:
- 读写分离: 主写从读,提高服务器的读写负载能力
- 负载均衡: 分担服务器负载,提高服务器的并发量,尤其是在读多写少的场景下
- 数据冗余: 实现数据的热备份
- 故障恢复: 当主节点出现问题,从节点仍然可以提供服务,实现快速的故障恢复
- 高可用基石: 基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案
哨兵模式
- 定义: 哨兵(sentinel)系统可以监控Redis主节点的状态,并且在主节点发生故障时自动进行故障转移,将一个从节点提升为新的主节点
- 任务:
- 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常
- 提醒(Notification): 当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知
- 自动故障迁移(Automatic failover):
- 当一个主服务器不能正常工作时,Sentinel会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器
- 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址
- 哨兵需求:
- 个数: 在实际部署中,哨兵模式需要至少三个哨兵实例以实现故障转移
- 类型: 哨兵实例本身也是Redis服务器,但不提供数据服务
- 配置: 哨兵模式的配置包括设置监控的主节点信息、密码、超时参数等
- 集群节点哨兵模式工作原理:
- 哨兵集群组成: 基于pub/sub机制
- 哨兵间发现: 主库频道
__sentinel__:hello
,不同哨兵通过它相互发现 - 哨兵发现从库: 向主库发送
INFO
命令
- 哨兵间发现: 主库频道
- 基于
pub/sub
机制的客户端事件通知- 事件: 主库下线事件
+sdown
: 实例进入“主观下线”状态-sdown
: 实例退出“主观下线”状态+odown
: 实例进入“客观下线”状态-odown
: 实例退出“客观下线”状态
- 事件: 从库重新配置事件
+slave-reconf-sent
: 哨兵发送 SLAVEOF 命令重新配置从库+slave-reconf-inprog
: 从库配置了新主库,但尚未进行同步+slave-reconf-done
: 从库配置了新主库,且完成同步
- 事件: 新主库切换
+switch-master
: 主库地址发生变化
- 事件: 主库下线事件
- 步骤:
- 监控阶段: 哨兵不断检查主从节点是否正常运行
- 通知阶段: 当主节点出现问题时,哨兵会向其他哨兵和客户端发送通知
- 故障转移阶段: 哨兵通过投票机制选举出一个从节点作为新的主节点,并将其他从节点连接到新的主节点
- 下线判断:
- 主观下线(Subjectively Down, SDOWN): 主库或从库对某个Sentinel的PING响应超时
- 客观下线(Objectively Down, ODOWN): 多个Sentinel在对同一个服务器做出SDOWN判断
- 只用于主服务器
- Sentinel之前会相互交换对某个主服务器的下线意见,达到一定数量(
quorum
)后才会将SDOWN转为ODOWN
- 新主服务器选举:
- 两个条件:
- 拿到半数以上的赞成票
- 拿到的票数>=
quorum
- 如果未选出,则集群会等待一段时间(
failover-timeout
的2倍),再重新选举
- 两个条件:
- 哨兵集群组成: 基于pub/sub机制
- 使用:
- 配置:
#不限制ip bind 0.0.0.0 # 让sentinel服务后台运行 daemonize yes # 配置监听的主服务器,mymaster代表服务器的名称,自定义,192.168.200.130 代表监控的主服务器,6379代表端口, #2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。 # 计算规则:哨兵个数/2 +1 sentinel monitor mymaster 192.168.200.130 6379 2 # sentinel auth-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码 sentinel auth-pass mymaster 123456 #超过5秒master还没有连接上,则认为master已经停止 sentinel down-after-milliseconds mymaster 5000 #如果该时间内没完成failover操作,则认为本次failover失败 sentinel failover-timeout mymaster 30000
- 启动: 添加
--sentinel
参数
- 配置:
- 优缺点:
- 优点:
- 主从集群间可以实现自动切换,可用性更高
- 数据更大限度的防止丢失
- 解决哨兵的集群高可用问题,减少误判率
- 缺点:
- 海量数据存储问题
- 高并发写的问题
- 优点:
分片集群(Redis Cluster)模式
- 原理:
- 数据切片和实例的对应分布关系
- Redis Cluster方案: 无中心化
- 采用哈希槽(Hash Slot)来处理数据和实例之间的映射关系
- 一个切片集群共有16384个哈希槽,只给Master分配
- 具体的映射过程
- 根据键值对的key,按照CRC16算法计算一个16bit的值
- 再用这个16bit值对16384取模,每个模数代表一个相应编号的哈希槽
- 16384的由来:
- 正常的心跳数据包携带节点的完整配置,它能以幂等方式来更新配置
- 如果采用16384个插槽,占空间2KB;如果采用65536个插槽,占空间8KB
- Redis Cluster不太可能扩展到超过1000个主节点,太多可能导致网络拥堵
- 16384个插槽范围比较合适,当集群扩展到1000个节点时,也能确保每个master节点有足够的插槽,也能保证心跳包的大小合理
- 哈希槽映射方法
- 用
cluster create
命令创建集群,Redis会自动把这些槽平均分布在集群实例上 - 也可以使用
cluster meet
命令手动建立实例间的连接,形成集群,再使用cluster addslots
命令,指定每个实例上的哈希槽个数
- 用
- Redis Cluster方案: 无中心化
- 客户端如何定位数据:
- Redis实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散【流言协议】
- 集群中,实例和哈希槽的对应关系并不是一成不变的【实例新增或删除、master宕机】,实例之间可以通过相互传递消息,获得最新的哈希槽分配信息,但客户端是无法主动感知这些变化
- 客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端,客户端会把哈希槽信息缓存在本地
- 重定向机制: 如果实例上没有该键值对映射的哈希槽,就会返回
MOVED
命令,客户端会更新本地缓存
- 数据切片和实例的对应分布关系
- 使用:【至少3主3从】
- 配置:
- 启动:
- 配置各个节点
bind 0.0.0.0 protected-mode no port 7001 daemonize yes requirepass "123456" logfile "/usr/local/redis/logs/redis_7001.log" dbfilename "redis_7001.rdb" dir "/usr/local/redis/data" appendonly yes appendfilename "appendonly_7001.aof" masterauth "123456" #是否开启集群 cluster-enabled yes # 生成的node文件,记录集群节点信息,默认为nodes.conf,防止冲突,改为nodes-7001.conf cluster-config-file nodes-7001.conf #节点连接超时时间 cluster-node-timeout 20000 #集群节点的ip,当前节点的ip, 如果是阿里云是可以写内网ip cluster-announce-ip 192.168.200.130 #集群节点映射端口 cluster-announce-port 7001 #集群节点总线端口,节点之间互相通信,常规端口+1万 cluster-announce-bus-port 17001
- 逐个启动实例
- 加入Redis集群(任意一个Redis实例即可)
redis-cli -a 123456 --cluster create 192.168.200.130:7001 192.168.200.130:7002 192.168.200.130:7003 192.168.200.130:7004 192.168.200.130:7005 192.168.200.130:7006 --cluster-replicas 1
--cluster
: 构建集群全部节点信息--cluster-replicas 1
: 主从节点的比例,1表示1主1从的方式
- 配置各个节点
- 修改集群:
- 集群扩容:
- 添加新节点:
redis-cli -a 123456 --cluster add-node 192.168.200.130:7007 192.168.200.130:7001
- 重新分配哈希槽: 将7001, 7002, 7003中的100个哈希槽挪给7007
redis-cli -a 123456 --cluster reshard 192.168.200.130:7001 --cluster-from 968e39e094e4109c45f99b1da6a157d7e41d54be,aa04c1e93d23d1e7394acb6bf7560dd2d9b5ace0,b81a6bdffaaf52be1e3522249aad6cda00ae3674 --cluster-to 9b9ad250c589b30e9ff7a4fbd10c8bbc9eb94793 --cluster-slots 100
--cluster-from
: 表示slot目前所在的节点的node ID,多个ID用逗号分隔--cluster-to
: 表示需要新分配节点的node ID--cluster-slots
: 分配的slot数量
- 添加从节点: 将7008作为7007的从节点
redis-cli -a 123456 --cluster add-node 192.168.200.130:7008 192.168.200.130:7007 --cluster-slave --cluster-master-id 9b9ad250c589b30e9ff7a4fbd10c8bbc9eb94793
add-node
: 后面的分别跟着新加入的slave和slave对应的mastercluster-slave
: 表示加入的是slave节点--cluster-master-id
: 表示slave对应的master的node ID
- 添加新节点:
- 集群缩容:
- 删除从节点:
redis-cli -a 123456 --cluster del-node 192.168.200.130:7008 bf12b1df363f139911b83f73038313951855e468
del-node
: 删除节点,后面跟着slave节点的ip:port
和node ID
- 归还哈希槽:
redis-cli -a 123456 --cluster reshard 192.168.200.130:7007 --cluster-from 9b9ad250c589b30e9ff7a4fbd10c8bbc9eb94793 --cluster-to 968e39e094e4109c45f99b1da6a157d7e41d54be --cluster-slots 33 --cluster-yes
- 删除主节点:
redis-cli -a 123456 --cluster del-node 192.168.200.130:7007 9b9ad250c589b30e9ff7a4fbd10c8bbc9eb94793
- 删除从节点:
- 集群扩容:
- 启动:
- 查看:
- 访问集群
# 连接到指定redis节点 bin/redis-cli -a 123456 -c -h 192.168.200.130 -p 7001 # 密码 集群命令 检查状态 节点地址 bin/redis-cli -a 123456 --cluster check 192.168.200.130:7001
- 集群相关命令
CLUSTER INFO // 打印集群的信息 CLUSTER NODES // 列出集群当前已知的所有节点(node),以及这些节点的相关信息。 //节点 CLUSTER MEET // 将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。 CLUSTER FORGET // 从集群中移除 node_id 指定的节点。 CLUSTER REPLICATE // 将当前节点设置为 node_id 指定的节点的从节点。 CLUSTER SAVECONFIG // 将节点的配置文件保存到硬盘里面。 CLUSTER ADDSLOTS [slot ...] // 将一个或多个槽(slot)指派(assign)给当前节点。 CLUSTER DELSLOTS [slot ...] // 移除一个或多个槽对当前节点的指派。 CLUSTER FLUSHSLOTS // 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。 CLUSTER SETSLOT NODE // 将槽 slot 指派给 node_id 指定的节点。 CLUSTER SETSLOT MIGRATING // 将本节点的槽 slot 迁移到 node_id 指定的节点中。 CLUSTER SETSLOT IMPORTING // 从 node_id 指定的节点中导入槽 slot 到本节点。 CLUSTER SETSLOT STABLE // 取消对槽 slot 的导入(``import``)或者迁移(migrate)。 //键 CLUSTER KEYSLOT // 计算键 key 应该被放置在哪个槽上。 CLUSTER COUNTKEYSINSLOT // 返回槽 slot 目前包含的键值对数量。 CLUSTER GETKEYSINSLOT // 返回 count 个 slot 槽中的键。 //新增 CLUSTER SLAVES node-id // 返回一个master节点的slaves 列表
- 访问集群
- 配置:
- 评价: 在哨兵模式的基础上解决了海量数据存储的问题,并且提升了写并发量
过期删除策略
- 定时删除
- 方法: 它会在设置键的过期时间的同时,创建定时器,当键到了过期时间,定时器会立即对键进行删除
- 评价:
- 能够保证过期键的尽快删除,快速释放内存空间 => 内存空间友好
- 大量定时器执行,严重降低系统性能 => CPU非常不友好
- 惰性删除
- 方法: 在获取key时,才会检查key是否过期,如果过期则删除该key
- 评价: 对CPU足够友好,但是对内存空间非常不友好
- 定期删除
- 默认每秒运行10次会对具有过期时间的key进行一次扫描
- 每次默认只会随机扫描20个key,同时删除这20个key中已经过期的key
- 如果这20个key中过期key的比例达超过25%,则继续扫描
- 参数设置:
hz [次数]
【一般不超过100】
内存淘汰策略
- 目的: 进一步解决空间不足问题【除了修改
maxmemory
(一般为物理内存的四分之三左右)】 - 策略:
- 对于LRU和TTL相关策略,每次触发时,redis会默认从5个key中一个key符合条件的key进行删除。如果要修改的话,可以修改
maxmemory-samples
属性值
- 对于LRU和TTL相关策略,每次触发时,redis会默认从5个key中一个key符合条件的key进行删除。如果要修改的话,可以修改
- 使用:
- Redis只做缓存,不做DB持久化,使用
allkeys
【如状态性信息,经常被访问,但数据库不会修改】 - 同时用于缓存和DB持久化,使用
volatile
【如商品详情页】 - 存在冷热数据区分,则选择LRU或LFU【如热点新闻,热搜话题等】
- 每个key被访问概率基本相同,选择使用random【如企业内部系统,访问量不大,删除谁对数据库也造成太大压力】
- 根据超时时间长久淘汰数据,选择选用ttl【如微信过期好友请求】
- Redis只做缓存,不做DB持久化,使用