中间件专题: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 内存淘汰策略

  1. noeviction: 默认不淘汰,新增数据时直接报错
  2. volatile-TTL: 如果设置了过期时间,淘汰即将过期的数据
  3. volatile-random: 从设置了过期时间中的数据随机淘汰
  4. allkeys-random: 从所有数据中随机淘汰
  5. allkeys-lru: 在所有数据中,按照最近最少使用的数据进行淘汰
  6. volatile-lru: 在设置了过期时间中的数据,按照最近最少使用的数据进行淘汰。

3.3 全局唯一ID

全局唯一Id生成策略:

  • UUID
  • Redis自增 INCR key
  • 雪花算法
  • 数据库自增

redis自增策略:

  • 每天一个key,方便统计订单量
  • id构造是:时间戳+计数器

4. 分布式缓存

Redis 主要通过三种模式实现分布式缓存,每种模式适用于不同的场景和需求。

特性 主从复制 (Replication) 哨兵模式 (Sentinel) 集群模式 (Cluster)
核心目标 数据备份、读写分离 高可用、自动故障转移 水平扩展、高可用、数据分片
数据分布 全量复制,所有节点数据相同 全量复制,所有节点数据相同 数据分片到16384个槽,每个节点负责部分槽
高可用性 手动故障转移 自动故障转移 自动故障转移
扩展性 读扩展(添加从节点) 读扩展(添加从节点) 读写扩展(添加主节点)
适用场景 数据备份、读多写少 对可用性有要求的业务 大数据量、高并发、需水平扩展

4.1 主从模式

4.1.1 主从结构

image

4.1.2 数据同步原理

(1)全量同步

  1. slave执行replicaof命令,建立连接
    1. slave请求数据同步
    2. master判断是否时第一次请求同步,是第一次请求则slave获取master数据版本信息
    3. slave保持版本信息
  2. master异步执行bgsave,生成RDB
    1. master发送RDB文件给slave
    2. slave清空本地数据,加载RDB文件
  3. master记录生成RDB后的所有命令到repl_baklog
    1. 发送repl_baklog中的命令到slave
    2. 循环同步

master如何判断slave是否是第一次来同步数据的?

slave做数据同步,必须向master声明自己的replication idoffset, master才可以判断到底需要同步哪些数据。判断是否是第一次来,只需要判断master和slave的replid是否一致即可。

Replication Id: 是数据集的标识,id一致说明是同一个数据集。每一个master都有唯一的replid, slave 则会继承master的replid。

offset: 偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完全同步时也会记录当前同步的offset。如果slave的offset小于master的offset。说明slave数据落后于master,需要更新。

(4)增量同步

  1. slave 重启
    1. slave请求数据同步
    2. 判断replid一致,回复slave:continue
  2. 去repl_baklog中获取slave offset后的数据
    1. 发送offset后的命令

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

image

(3)主从同步优化

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

image

(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客户端。

image

(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实现了节点的感知和自动切换。

  1. 在pom文件中引入redis的starter依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在配置文件中配置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
  1. 配置主从读写分离
@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}作为前缀

(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

(4)故障转移

当集群中有一个master节点宕机会发生什么?

  • 首先是该实例与其他实例断开连接
  • 确定是下线后,自动提升一个slave作为新的master节点

如何实现数据迁移?

  • 利用cluster failover命令可以手动让集群中的某个master节点宕机,切换到cluster failover命令的这个slave节点。实现无感知的数据迁移。
    1. slave节点告诉master,该master节点拒绝任何客户端请求
    2. master返回当前的数据offset给slave
    3. 等到数据offset和master一致,开始故障转移
    4. slave标记自己为master,广播故障转移结果

(5)RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用步骤与哨兵模式基本一致。

  1. 引入redis的start依赖
  2. 配置分片集群的地址
  3. 配置读写分离

与哨兵模式相比,分片集群需要配置每个节点的信息

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 memory
  • memory 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(超时下线时间)值
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua和事务问题

单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。

posted @ 2025-09-22 13:38  飞↑  阅读(16)  评论(0)    收藏  举报