中间件专题:Redis
1. Redis 数据结构
# String
set `key` `value`
setnx `key` `value` # 不存在才set
setex `key` `value` `ttl`
incrby `key` `increment` # 自增
# Hash 哈希表
hset `key` `field` `value` `field` `value`
hget `key` `field`
hsetnx `key` `value` # 不存在才set
# List 双向链表
LPUSH `key` `element1` `element2` ... # 从左侧插入一个或多个元素
LPOP `key` # 移除左侧第一个元素,没有返回null
RPUSH `key` `element1` `element2` ...
RPOP `key`
LRANGE `key` `start` `end` # 返回start到end之间的元素
# Set 无序集合
SADD `key` `member1` `member2` # 添加
SREM `key` `member1` `member2` # 删除
SCARD `key` # 返回set中元素的个数
SISMEMBER `key` `membern` # 判断是否是key成员
SmemberS `key` # 查询key的所有成员
SINNER `key1` `key2` # 获取交集
SDIFF `key1` `key2` # 求差集, key1有,key2没有
SUNION `key1` `key2` # 求并集
# Zset/SortedSet ### 跳表 + Hash表, 有序集合,查询效率高
ZADD `key` `score1` `member1` `score2` `member2` ... # 添加一个或多个元素到sorted set,如果存在更新score
ZREM `key` `member` # 删除
ZSCORE `key` `member` # 获取元素的score
ZRANK `key` `member` # 获取元素排名
ZCARD `key` # 获取元素个数
ZCOUNT `key` `min` `max` # 求指定范围score的元素个数
ZINCRBY `key` `increment` `member` # 对指定member自增
ZRANGE `key` `min` `max` # 获取指定排名范围的元素
ZRANGEBYSCORE `key` `min` `max` # 获取指定score范围内的元素
2. Redis 线程模型
3. 缓存解决方案
3.1 缓存击穿、缓存雪崩、缓存穿透
| 缓存击穿 | 缓存雪崩 | 缓存穿透 | |
|---|---|---|---|
| 定义 | 单个热点 key 过期时,大量并发请求瞬间击穿缓存直达数据库 | 大量缓存 key 同时失效或缓存服务宕机,导致请求洪涌压垮数据库 | 请求不存在的数据,缓存与数据库均未命中,反复穿透缓存 |
| 核心解决方案 | 互斥锁 | 1. 随机过期时间:基础 TTL + 随机偏移量 2. 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis) 3. 熔断限流:Hystrix/Sentinel 保护数据库 |
1. 布隆过滤器:拦截非法请求(误判率可控) 2. 缓存空值: NULL 结果短时缓存(如 5 分钟) 3. 参数校验:过滤非法 ID(如 ID≤0) |
3.2 Redis 内存淘汰策略
- noeviction: 默认不淘汰,新增数据时直接报错
- volatile-TTL: 如果设置了过期时间,淘汰即将过期的数据
- volatile-random: 从设置了过期时间中的数据随机淘汰
- allkeys-random: 从所有数据中随机淘汰
- allkeys-lru: 在所有数据中,按照最近最少使用的数据进行淘汰
- volatile-lru: 在设置了过期时间中的数据,按照最近最少使用的数据进行淘汰。
3.3 全局唯一ID
全局唯一Id生成策略:
- UUID
- Redis自增
INCR key - 雪花算法
- 数据库自增
redis自增策略:
- 每天一个key,方便统计订单量
- id构造是:时间戳+计数器
4. 分布式缓存
Redis 主要通过三种模式实现分布式缓存,每种模式适用于不同的场景和需求。
| 特性 | 主从复制 (Replication) | 哨兵模式 (Sentinel) | 集群模式 (Cluster) |
|---|---|---|---|
| 核心目标 | 数据备份、读写分离 | 高可用、自动故障转移 | 水平扩展、高可用、数据分片 |
| 数据分布 | 全量复制,所有节点数据相同 | 全量复制,所有节点数据相同 | 数据分片到16384个槽,每个节点负责部分槽 |
| 高可用性 | 手动故障转移 | 自动故障转移 | 自动故障转移 |
| 扩展性 | 读扩展(添加从节点) | 读扩展(添加从节点) | 读写扩展(添加主节点) |
| 适用场景 | 数据备份、读多写少 | 对可用性有要求的业务 | 大数据量、高并发、需水平扩展 |
4.1 主从模式
4.1.1 主从结构

4.1.2 数据同步原理
(1)全量同步
- slave执行replicaof命令,建立连接
- slave请求数据同步
- master判断是否时第一次请求同步,是第一次请求则slave获取master数据版本信息
- slave保持版本信息
- master异步执行bgsave,生成RDB
- master发送RDB文件给slave
- slave清空本地数据,加载RDB文件
- master记录生成RDB后的所有命令到
repl_baklog- 发送repl_baklog中的命令到slave
- 循环同步
master如何判断slave是否是第一次来同步数据的?
slave做数据同步,必须向master声明自己的replication id和offset, master才可以判断到底需要同步哪些数据。判断是否是第一次来,只需要判断master和slave的replid是否一致即可。
Replication Id: 是数据集的标识,id一致说明是同一个数据集。每一个master都有唯一的replid, slave 则会继承master的replid。
offset: 偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完全同步时也会记录当前同步的offset。如果slave的offset小于master的offset。说明slave数据落后于master,需要更新。
(4)增量同步
- slave 重启
- slave请求数据同步
- 判断replid一致,回复slave:continue
- 去repl_baklog中获取slave offset后的数据
- 发送offset后的命令
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

(3)主从同步优化
- 在master中配置
repl-diskless-sync yes启用无磁盘复制,避免全量同步时写入RDB文件的磁盘IO,直接将RDB IO流直接发送给slave。 - Redis单节点上的内存占用要太大,减少RDB导致的过多磁盘IO。
- 适当提高
repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步 - 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主从-从链式结构,减少master压力

(4)总结
简述全量同步和增量同步的区别?
- 全量同步:master将完整的内存数据生成RDB,发送RDB文件到slave。在生成RDB文件之后的命令操作,则记录在repl_baklog, 逐个发送给slave;
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中offset偏移量之后的命令给slave
什么时候执行全量同步?
- slave初次同步
- slave的偏移量offset超过了repl_baklog的大小
什么时候执行增量同步?
- slave从故障中恢复并且offset在repl_baklog中能找到时
4.2. 哨兵模式
主从模式中slave宕机后恢复可以从master中同步数据,如果是master宕机呢?
哨兵的作用和原理
Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
- 监控:Sentinel会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也会以新的master为主,实现主从切换。
- 通知: Sentinel充当Redis客户端的服务发现来源。当Redis集群发送故障转移时,会将最新的信息推送到Redis客户端。

(1)服务状态监控
Sentinel基于心跳机制检测服务状态,每隔一秒向集群的每个实例发送一个ping命令:
- 主观下线:如果某个sentinel节点发现某个实例未在规定的时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(
quorum)的sentinel都认为该实例下线,则该实例客观下线。quorum最好超过实例数量的一半。
(2)选举master
一旦发现master故障,sentinel需要在slave中选择一个作为新的master。
- 断开时间:首先判断slave节点与master节点断开时间的长短,如果超过指定值(
down-after-milliseconds * 10)则会排除该slave节点 - 优先级:然后判断slave节点的
slave-priority值,越小的优先级越高,如果是0则永不参与选举。 - 偏移值
offset:如果slave-priority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高。 - 运行id:最后判断slave节点的运行id大小,越小优先级越高(运行id是redis自动生成的id,也就是说这里随机挑选一个slave作为master)
(3)故障转移
当选中了一个slave作为新的master节点之后,需要进行故障转移。
- sentinel给备选的slave发送
slaveof no one命令,让该节点成为master - sentinel给所有其他slave发送
slaveof 192.168.2.2 7002命令,让其他slave成为新master的从节点,开始从新的master上同步数据。 - 最后,sentinel将故障节点标记未slave,当故障节点恢复后会自动成为新master节点的slave节点。
(4)总结
-
sentinel的三个作用是什么?
- 状态监控
- 故障修复
- 故障恢复通知
-
Sentinel如何判断一个redis实例是否健康?
- 心跳机制:基于心跳机制,每隔一秒向每个实例发送一个ping命令
- 主观判断:如果某个实例没有在规定时间内响应,则认为该节点主观下线。
- 客观判断:如果超过指定数量的sentinel都认为该节点主观下线,则该实例客观下线。
-
故障转移步骤有哪些?
- master选举
- sentinel给备选slave发送
slaveof no one命令,让该节点成为master - sentinel给其他slave节点发送该节点的
ip + 端口信息,让其他slave节点成为新master节点的从节点。 - sentinel把故障master标记为slave,恢复后会成为新master的slave节点。
搭建哨兵集群
RedisTemplate的哨兵模式
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新信息。Spring的RedisTemplate底层利用了lettuce实现了节点的感知和自动切换。
- 在pom文件中引入redis的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 在配置文件中配置sentinel信息
spring:
redis:
sentinel:
master: mymaster # 指定master名称
nodes: # 指定redis-sentinel集群信息
- 192.168.56.2:27001
- 192.168.56.3:27001
- 192.168.56.4:27001
- 配置主从读写分离
@Bean
public LettuceClientConfigurationBuilderCustomer configurationBuilderCustomer(){
return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
这里ReadFrom是配置Redis的读写策略。
- MASTER:从节点读取
- MASTER_PREFERRED: 优先从master节点读取
- REPLICA: 仅从slave节点读取
- REPLICA_PREFERRED: 优先从slave节点读取
4.3. 分片集群模式
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此的健康状态
- 客户端请求可以访问集群中任意节点,最终都会被转发到正确节点。
(1)搭建分片集群
redis.conf
port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护,只需要指明其位置即可
cluster-config-file /tmp/6379/nodes.conf
# master节点心跳失败的超时时间
cluster-node-timeout 5000 # 5s
# 持久化文件存放目录
dir /tmp/6379
# 绑定地址
bind 0.0.0.0 # 任何ip都可以访问本节点
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.56.2 # master ip
# 保护模式
protected-mode no # 取消用户名和密码校验
# 数据库数量
database 1
# 日志
logfile /tmp/6379/run.log
redis-cli --cluster create / 创建集群
--cluster-replicas 1 / # 副本数量为1
192.168.56.2:7001 / # 自动判断: 副本数量为1,说明一主一从,有6个节点,则前面3个是master,后面3个是slave
192.168.56.2:7002 /
192.168.56.2:7003 /
192.168.56.2:8001 /
192.168.56.2:8002 /
192.168.56.2:8003
# 查看集群状态
redis-cli -p 7001 cluster nodes
(2)散列插槽
Redis会把每一个master节点映射到0~16383个插槽上(hash slot)上。
数据的key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含
{}, 且{}中至少包含一个字符,{}中的部分是有效部分。 - key中不包含
{},整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{momo}num, 则根据momo计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
- 总结
- Redis如何判断某个key应该在哪个实例?
- 集群初始化时会给每个节点分配不同的插槽值 0 ~ 16383
- 通过CRC-16算法计算hash值,用其对16384取余计算插槽位置
- 通过插槽位置获取对应的master节点ip和端口
- 如何将同一类数据固定的保存在同一个Redis实例中?
- 计算某一类数据相同的有效部分,例如key都以
{typeId}作为前缀
- 计算某一类数据相同的有效部分,例如key都以
- Redis如何判断某个key应该在哪个实例?
(3)集群伸缩
root@e8b5881fcf9b:/data# redis-cli --cluster help
Cluster Manager Commands:
reshard <host:port> or <host> <port> - separated by either colon or space
--cluster-from <arg>
--cluster-to <arg>
--cluster-slots <arg>
--cluster-yes
--cluster-timeout <arg>
--cluster-pipeline <arg>
--cluster-replace
add-node new_host:new_port existing_host:existing_port # 添加新节点需要标明原有的节点
--cluster-slave # 默认添加后是主节点,使用此标识表示添加的节点为从节点
--cluster-master-id <arg>
del-node host:port node_id
call host:port command arg arg .. arg
--cluster-only-masters
--cluster-only-replicas
set-timeout host:port milliseconds
import host:port
--cluster-from <arg>
--cluster-from-user <arg>
--cluster-from-pass <arg>
--cluster-from-askpass
--cluster-copy
--cluster-replace
backup host:port backup_directory
help
- 案例
- 向集群中添加一个新的master节点,并向其存储
num=10- 启动一个新的redis实例,端口7004
- 添加7004到之前的集群,并作为一个master节点
redis-cli --cluster add-node 192.168.56.2:7004 - 给7004节点分配插槽,使得num这个key可以存储到7004
redis-cli --cluster reshard 192.168.56.2:7004
- 向集群中添加一个新的master节点,并向其存储
(4)故障转移
当集群中有一个master节点宕机会发生什么?
- 首先是该实例与其他实例断开连接
- 确定是下线后,自动提升一个slave作为新的master节点
如何实现数据迁移?
- 利用cluster failover命令可以手动让集群中的某个master节点宕机,切换到
cluster failover命令的这个slave节点。实现无感知的数据迁移。- slave节点告诉master,该master节点拒绝任何客户端请求
- master返回当前的数据offset给slave
- 等到数据offset和master一致,开始故障转移
- slave标记自己为master,广播故障转移结果
(5)RedisTemplate访问分片集群
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用步骤与哨兵模式基本一致。
- 引入redis的start依赖
- 配置分片集群的地址
- 配置读写分离
与哨兵模式相比,分片集群需要配置每个节点的信息
spring:
redis:
cluster:
nodes: # 指定每一个节点的信息
- 192.168.56.2:7001
- 192.168.56.2:7002
- 192.168.56.2:7003
- 192.168.56.2:7004
- 192.168.56.2:7005
- 192.168.56.2:7006
6. 性能优化实践
6.1 键值对设计
(1)优雅的key结构
redis的key虽然可以自定义,单最好遵循下面的几个最佳实践约定:
- 基本格式:
业务名称(功能):数据名(类名):id(对象)- 可读性强
- 避免key冲突
- 方便管理
- 长度不超过44字节
- 节省内存:key是string类型,底层编码包含int, embstr和raw三种. embstr在小于44字节使用,采用连续内存空间,无内存碎片
- 不包含特殊字符
(2)拒绝BigKey
BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:
- 一个String类型的Key,它的值为10k(数据过大);
- 一个List类型的Key,它的列表数量为20000个(列表数量过多);
- 一个ZSet类型的Key,它的成员数量为10000个(成员数量过多);
- 一个Hash格式的Key,它的成员数量虽然只有1000个但这些成员的value总大小为100MB(成员体积过大)
- 推荐值:
- 单个key的value小于10KB
- 对于集合类型的key,建议元素数量小于1000
BigKey的危害
- 网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢 - 数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡 - Redis阻塞
对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞 - CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
如何发现BigKey
- redis-cli --bigkeys
利用redis-cli提供的--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key - scan扫描 (推荐)
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE) - 第三方工具(推荐)
利用第三方工具,如 Redis-Rdb-Tools分析RDB快照文件,全面分析内存使用情况 - 网络监控(云服务推荐)
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
如何删除BigKey
Redis在4.0后提供了异步删除的命令unlink: UNLINK key
(3)恰当的数据类型
# json字符串
{
"name": "jack",
"age" : 18
}
# 字段打散
user:1:name "jack"
user:1:age 18
#hash
user:1 name "jack" age 18
json字符串
字段打散
hash
(4)总结
-
key的最佳实践
-
固定格式
业务名:类名:id -
足够简短:44字节
-
不包含特殊字符
-
-
value的最佳实践
- 合理的数据拆分,拒绝bigkey
- 选择合适的数据类型
- Hash结构的entry数量不要超过1000(默认500)
- 设置合理的过期时间
6.2 批处理优化
Redis提供了很多Mxxx的命令,可以实现批量插入数据,例如:、
- mset
- hmset
注意⚠:不要再一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞
(1)Pipline
MSET虽然可以批处理,但是只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能
Pipline pipline = jedis.pipelined(); // 创建管道
for(int i = 1; i < 10000; i++){
pipeline.set("test:key_"+i);
if(i % 500){
pipeline.sync();
}
}
- 批处理的方案
- 原生M操作
- Pipeline处理
- 注意事项
- 批处理不建议一次携带太多命令
- Pipeline的多个命令之间不具备原子性
(2)集群下的批处理
如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。
在客户端计算每个key的slot(插槽), 将插槽一致的分为一个组, 每组利用pipeline进行批处理
6.3 服务端优化
(1)持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
- 用来做缓存的Redis实例尽量不要开启持久化功能
- 建议关闭RDB持久化功能,使用AOF持久化
- 利用脚本定期在slave节点做RDB,实现数据备份
- 设置合理的rewrite阈值,避免频繁的bgrewrite
- 配置no-appendfsync-on-rewrite=yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞
部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
(2)慢查询
慢查询: 在Redis执行时耗时超过某个值的命令,称为慢查询。
慢查询的阈值可以通过配置指定:
- slowlog-log-slower-than: 慢查询阈值,单位是微秒。默认是10000,建议1000
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定
- slowlog-max-len: 慢查询日志(本质是一个队列)的长度。默认是128,建议1000
修改以上两个配置可以在reids命令行中执行
config set slowlog-log-slower-than 1000
config get slowlog-log-slower-than
confit set slowlog-max-len 1000
config get slowlog-max-len
查看慢查询日志列表:
- slowlog len:查询慢查询日志长度
SLOWLOG LEN - slowlog get[n]:读取n条慢查询日志
SLOWLOG GET - slowlog reset:清空慢查询列表
SLOWLOG RESET
(3)命令及安全配置
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞
漏洞重现方式:https://cloud.tencent.com/developer/article/1039000
漏洞出现的核心原因:
- Redis未设置密码
- 利用了Redis的config set命令动态修改Redis配置
- 使用root账号权限启动redis
避免安全漏洞:
-
不要使用root用户启动redis
-
开启防火墙
-
Redis一定要设置密码
-
尽量不使用默认的端口
-
禁止线上使用下面命令: keys, flushall, flushdb, config set等。 可以利用rename-command禁用
-
bind:限制网卡,禁止外网访问
(4)内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、OPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。
Redis提供了一些命令,可以查看到Redis目前的内存分配状态
info memorymemory xxx
内存缓冲区配置:
- 复制缓冲区:主从复制的repl_baklog_buf,如果太小可能会导致频繁的全量复制,影响性能,通过
repl-backlog-size来设置,默认1M - AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓存区。无法设置容量上限。
- 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置
6.4 集群最佳实践
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
- 集群完整性问题
cluster-require-full-coverage yes这样的配置是插槽全覆盖,一旦某个插槽对应服务不可用,则整个集群无法使用- 建议将其配置为
no,以保证高可用特性
- 集群带宽问题
- 集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
- 插槽信息
- 集群状态信息:集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
- 解决途径:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
- 避免在单个物理机中运行太多Redis实例
- 配置合适的cluster-node-timeout(超时下线时间)值
- 集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
- 数据倾斜问题
- 客户端性能问题
- 命令的集群兼容性问题
- lua和事务问题
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。

浙公网安备 33010602011771号