前言
Redis作为互联网业务的核心内存数据库,其生产环境的稳定性、性能与可扩展性直接决定了业务的可用性上限。多数开发者仅掌握基础的缓存读写操作,一旦面对集群搭建、数据备份、性能瓶颈排查、在线数据迁移等生产级场景,极易出现踩坑、故障甚至数据丢失问题。
一、Redis三大集群方案深度对比与生产落地
Redis单实例存在内存上限、性能瓶颈与单点故障风险,生产环境必须采用集群方案实现水平扩展与高可用。目前主流的三大集群方案分别为代理型静态分片Twemproxy、代理型动态分片Codis、官方原生Redis Cluster,三者的核心差异与适用场景如下表所示:
| 对比维度 | Twemproxy | Codis | Redis Cluster |
| 架构类型 | 代理型集群 | 代理型集群 | 原生无中心P2P集群 |
| 分片核心 | 一致性哈希静态分片 | 1024个SLOT动态分片 | 16384个SLOT动态分片 |
| 动态扩缩容 | 不支持 | 原生支持,自动迁移 | 原生支持,在线reshard |
| 高可用能力 | 无原生支持,需配合哨兵 | 原生支持主从切换 | 原生支持自动故障转移 |
| 客户端侵入性 | 零侵入,兼容原生Redis协议 | 零侵入,兼容原生Redis协议 | 需客户端支持集群协议,有轻量侵入 |
| 性能损耗 | 单线程代理,损耗中等 | 多线程代理,损耗较低 | 无代理层,损耗极低 |
| 运维复杂度 | 极低,架构简单 | 中等,有可视化Dashboard | 中等,官方原生工具支持 |
| 官方支持 | 第三方开源,更新缓慢 | 第三方开源,版本滞后官方Redis | 官方原生维护,持续迭代 |
| 适用场景 | 中小规模静态分片集群,无频繁扩缩容需求 | 大规模企业级集群,需可视化运维,零客户端改造 | 生产环境主流首选,全场景覆盖,高性能高可用需求 |
1.1 代理型集群之Twemproxy:轻量级静态分片方案
底层核心原理
Twemproxy是Twitter开源的轻量级Redis/Memcached代理中间件,采用单线程Reactor模型,客户端统一连接代理节点,代理层根据预设的一致性哈希算法,将请求分发到后端对应的Redis实例,后端实例对客户端完全透明。其核心特性为无中心节点、轻量低侵入,但不支持动态分片、无自动故障转移能力,分片规则变更需重启代理节点。
生产级搭建实战
本次采用Twemproxy最新稳定版0.5.0,后端部署4个Redis 7.2.5(LTS最新稳定版)实例,实现4分片静态集群,所有步骤可直接执行。
- 环境准备
- 操作系统:CentOS 7+/Ubuntu 20.04+
- 依赖安装:
yum install -y gcc automake libtool git - 后端Redis实例:提前启动4个Redis实例,端口分别为6379-6382,配置基础认证与持久化
- 编译安装Twemproxy
git clone https://github.com/twitter/twemproxy.git
cd twemproxy
autoreconf -fvi
./configure --prefix=/usr/local/twemproxy
make && make install
echo "export PATH=$PATH:/usr/local/twemproxy/sbin" >> /etc/profile
source /etc/profile
- 核心配置文件nutcracker.yml
redis_cluster:
listen: 0.0.0.0:22121
hash: fnv1a_64
distribution: ketama
auto_eject_hosts: true
redis: true
server_retry_timeout: 2000
server_failure_limit: 3
servers:
- 127.0.0.1:6379:1 redis-node1
- 127.0.0.1:6380:1 redis-node2
- 127.0.0.1:6381:1 redis-node3
- 127.0.0.1:6382:1 redis-node4
- 启动与集群验证
# 后台启动Twemproxy
nutcracker -c /usr/local/twemproxy/conf/nutcracker.yml -d
# 连接代理端口验证读写
redis-cli -p 22121 set test_key test_value
redis-cli -p 22121 get test_key
核心注意事项
- 单线程架构存在性能瓶颈,高并发场景需部署多个Twemproxy节点,前端通过LVS/nginx做负载均衡
- 不支持跨分片的多KEY操作(如MSET/MGET),需业务保证多KEY在同一分片
- 无原生故障转移能力,需配合Redis Sentinel实现后端节点的高可用
- 分片规则变更需重启代理节点,不适合频繁扩缩容的业务场景
1.2 代理型集群之Codis:企业级动态分片方案
底层核心原理
Codis是豌豆荚开源的企业级分布式Redis解决方案,采用「代理层+存储层+协调层」三层架构:代理层Codis-Proxy为无状态节点,可水平扩展,兼容原生Redis协议;存储层为Codis-Server(基于Redis二次开发,支持SLOT分片);协调层通过Etcd/ZooKeeper存储集群元数据,实现配置同步与节点发现。其核心特性为支持1024个SLOT的动态分片、自动数据迁移、可视化Dashboard运维,客户端零侵入,适合大规模Redis集群管理。
生产级搭建实战
本次采用Codis最新稳定版3.2.2,基于ZooKeeper 3.8.4(最新稳定版)实现元数据管理,部署3个Codis-Proxy、3组Codis-Server主从,所有步骤可直接执行。
- 环境准备
- 提前部署ZooKeeper集群,确保服务正常运行
- 安装Go 1.22+环境,配置GOPATH
- 安装部署Codis
go get -d github.com/CodisLabs/codis
cd $GOPATH/src/github.com/CodisLabs/codis
make
- 核心配置与组件启动
- 启动Dashboard:修改dashboard.toml配置ZooKeeper地址,执行
codis-dashboard -c dashboard.toml -d - 启动Codis-Proxy:修改proxy.toml配置Dashboard地址,执行
codis-proxy -c proxy.toml -d - 启动Codis-Server:基于Redis修改配置,启动后通过Dashboard添加到集群,完成1024个SLOT的初始化分配
- 集群扩缩容实战
- 新增Codis-Server节点,通过Dashboard添加到集群
- 选择需要迁移的SLOT范围,配置迁移目标节点,点击启动迁移,系统自动完成数据同步与分片切换,全程业务无感知
核心注意事项
- Codis-Server基于官方Redis二次开发,版本滞后于原生Redis,无法使用最新的Redis特性
- 架构复杂度高于Twemproxy,需维护Dashboard、ZooKeeper、Proxy、Server多类组件
- 支持绝大多数原生Redis命令,不支持SELECT、SWAPDB等跨库操作,不支持事务
- 适合超大规模Redis集群(百节点以上),中小规模集群运维成本偏高
1.3 官方原生集群Redis Cluster:生产主流首选方案
底层核心原理
Redis 3.0+官方推出的原生分布式集群方案,采用无中心P2P架构,节点间通过Gossip协议通信,全节点对等。其核心设计为16384个哈希槽(SLOT),集群将所有SLOT均匀分配到主节点,每个KEY通过CRC16(key) mod 16384计算所属SLOT,自动路由到对应节点。原生支持主从复制高可用、自动故障转移、在线扩缩容,兼容绝大多数Redis命令,无代理层性能损耗,是目前生产环境的首选方案。
核心底层逻辑通俗解读:
- 数据与SLOT绑定,而非与节点绑定,扩缩容本质是SLOT的迁移,无需重构哈希环
- Gossip协议实现节点间的状态同步,无需中心节点存储元数据,无单点故障风险
- 故障检测通过多节点共同投票实现,超过半数节点认为主节点下线,自动触发主从切换,避免脑裂问题
- 客户端开启集群模式后,自动处理MOVED/ASK重定向,无需业务层处理路由逻辑
生产级3主3从集群搭建实战
本次采用Redis 7.2.5最新LTS版本,单机部署6个节点(端口7001-7006),实现3主3从高可用集群,所有配置与步骤可直接复制执行。
- 环境准备
- 操作系统:CentOS 7+/Ubuntu 20.04+
- 提前完成Redis 7.2.5编译安装,配置环境变量
- 创建数据与日志目录:
mkdir -p /data/redis/{7001..7006} /var/log/redis
- 节点配置文件以7001节点为例,redis_7001.conf核心配置如下,其余5个节点仅需修改端口、pidfile、logfile、dir、cluster-config-file参数,其余配置完全一致:
port 7001
bind 0.0.0.0
daemonize yes
pidfile /var/run/redis_7001.pid
logfile /var/log/redis/redis_7001.log
dir /data/redis/7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
cluster-require-full-coverage no
appendonly yes
aof-use-rdb-preamble yes
appendfsync everysec
protected-mode no
requirepass your_strong_password
masterauth your_strong_password
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command KEYS ""
- 启动所有集群节点
redis-server /data/redis/conf/redis_7001.conf
redis-server /data/redis/conf/redis_7002.conf
redis-server /data/redis/conf/redis_7003.conf
redis-server /data/redis/conf/redis_7004.conf
redis-server /data/redis/conf/redis_7005.conf
redis-server /data/redis/conf/redis_7006.conf
- 创建集群并分配主从节点Redis 5.0+原生支持redis-cli集群管理命令,无需额外安装ruby工具,执行以下命令自动完成3主3从节点分配:
redis-cli -a your_strong_password --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1
执行后输入yes确认分配方案,系统自动完成集群初始化与SLOT分配,提示[OK] All 16384 slots covered即集群创建成功。 5. 集群状态验证
# 查看集群节点与主从关系
redis-cli -a your_strong_password -c -p 7001 cluster nodes
# 查看SLOT分配情况
redis-cli -a your_strong_password -c -p 7001 cluster slots
# 集群模式读写验证,自动处理重定向
redis-cli -a your_strong_password -c -p 7001 set cluster_test test_value
redis-cli -a your_strong_password -c -p 7002 get cluster_test
集群架构图
核心注意事项
- 跨SLOT多KEY操作限制:MSET/MGET、事务等操作要求所有KEY必须在同一SLOT,可通过Hash Tag
{tag}key强制KEY分配到同一SLOT,例如{user}1001、{user}1002,CRC16仅计算{}内的内容,保证同SLOT - 集群最小部署要求:至少3个主节点,每个主节点至少1个从节点,保证高可用
cluster-require-full-coverage必须设置为no,避免单个主节点故障导致整个集群不可用- 所有节点密码必须完全一致,
masterauth必须配置,否则主从复制与集群通信失败
二、Redis冷热备份全方案生产落地
数据备份是Redis生产环境的最后一道防线,必须构建「原生持久化+热备份+冷备份」的三层数据保护体系,杜绝数据丢失风险。首先明确核心定义:
- 热备份:在线备份,实例正常运行时实时同步数据,不中断业务,用于日常高可用、故障快速切换
- 冷备份:离线备份,业务低峰期生成全量数据快照,异地存储,用于灾难恢复、数据误删回滚
2.1 原生持久化核心机制与最佳实践
原生持久化是所有备份方案的基础,Redis提供RDB全量快照、AOF增量日志、混合持久化三种模式,其中混合持久化是生产环境首选方案。
2.1.1 RDB持久化:全量快照备份
底层核心原理
RDB是指定时间点Redis内存数据的全量二进制快照文件,触发方式分为手动触发与自动触发:
- 手动触发:
SAVE命令阻塞主线程生成快照,生产环境禁用;BGSAVE命令fork子进程,子进程基于操作系统Copy-On-Write(写时复制)机制生成快照,主线程继续处理请求,非阻塞,生产环境唯一可用的手动触发方式 - 自动触发:通过配置文件的save规则,满足触发条件时自动执行BGSAVE
通俗解读写时复制机制:fork子进程时,子进程共享主线程的内存页,只有当主线程修改某一内存页时,才会复制该页生成副本,因此BGSAVE不会占用双倍内存,仅fork瞬间会阻塞主线程,内存越大,阻塞时间越长,生产环境单实例内存建议不超过20G。
生产级最佳配置
save 3600 1
save 300 100
save 60 10000
dbfilename dump.rdb
dir /data/redis
rdbcompression yes
rdbchecksum yes
2.1.2 AOF持久化:增量日志备份
底层核心原理
AOF以追加的方式记录Redis所有的写命令,实例重启时重放AOF文件中的所有命令恢复数据,实时性远高于RDB。核心配置为appendfsync刷盘策略,三个可选值的生产适配如下:
always:每次写命令都刷盘,数据零丢失,性能极差,生产环境禁用everysec:每秒刷盘一次,最多丢失1秒数据,性能与数据安全平衡,生产环境首选no:由操作系统控制刷盘时机,性能最好,数据丢失风险极高,生产环境禁用
AOF文件会持续增长,因此Redis提供AOF重写机制:fork子进程生成新的AOF文件,仅保留恢复数据所需的最小命令集,重写完成后替换旧文件,大幅降低AOF文件体积,加快重启恢复速度。
生产级最佳配置
appendonly yes
appendfilename "appendonly.aof"
dir /data/redis
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
no-appendfsync-on-rewrite yes
aof-load-truncated yes
2.1.3 混合持久化:RDB+AOF最佳结合方案
Redis 4.0+推出的混合持久化,完美解决了RDB恢复慢、AOF数据丢失多的痛点,是生产环境的首选持久化方案。
底层核心原理
AOF重写时,将当前内存数据生成RDB快照写入AOF文件开头,后续的写命令以AOF增量日志的方式追加到文件末尾。实例重启时,先加载RDB快照快速恢复全量数据,再重放增量AOF日志,兼顾了RDB的快速恢复能力与AOF的低数据丢失风险。
生产级最佳配置
aof-use-rdb-preamble yes
该配置需配合上述RDB与AOF的最佳配置共同使用,Redis 7.0+默认开启,显式配置确保生效。
2.2 冷备份全方案落地
冷备份用于灾难恢复场景,如机房故障、数据误删除、实例文件损坏等,必须在业务低峰期执行,生成的备份文件需异地存储,避免与源实例同机房故障导致备份失效。
生产级冷备份自动化脚本
该脚本可直接在生产环境使用,自动触发BGSAVE、备份压缩、保留历史版本、清理过期备份,支持定时任务执行。
#!/bin/bash
REDIS_CLI="/usr/local/bin/redis-cli"
REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_PASSWORD="your_strong_password"
BACKUP_DIR="/data/redis/backup/cold"
RDB_DIR="/data/redis"
RDB_FILE="dump.rdb"
KEEP_DAYS=7
mkdir -p $BACKUP_DIR
$REDIS_CLI -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD BGSAVE > /dev/null 2>&1
while [ $($REDIS_CLI -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD info persistence | grep "rdb_bgsave_in_progress" | awk -F: '{print $2}') -eq 1 ]
do
sleep 1
done
BACKUP_TIME=$(date +%Y%m%d%H%M%S)
BACKUP_FILE="$BACKUP_DIR/redis_cold_backup_$BACKUP_TIME.rdb.gz"
gzip -c $RDB_DIR/$RDB_FILE > $BACKUP_FILE
find $BACKUP_DIR -name "redis_cold_backup_*.rdb.gz" -mtime +$KEEP_DAYS -delete
if [ -f $BACKUP_FILE ]; then
echo "冷备份成功,备份文件:$BACKUP_FILE"
else
echo "冷备份失败"
exit 1
fi
脚本使用与定时任务配置
# 赋予脚本执行权限
chmod +x /data/redis/script/redis_cold_backup.sh
# 配置crontab定时任务,每天凌晨2点业务低峰期执行
echo "0 2 * * * /bin/bash /data/redis/script/redis_cold_backup.sh >> /data/redis/log/backup.log 2>&1" >> /var/spool/cron/root
冷备份数据恢复实战
- 停止目标Redis实例:
systemctl stop redis - 备份当前实例的RDB与AOF文件,防止恢复失败
- 解压备份文件:
gzip -d redis_cold_backup_20260303020000.rdb.gz - 将解压后的RDB文件复制到Redis的dir目录,覆盖原有dump.rdb
- 临时关闭AOF,修改redis.conf设置
appendonly no,避免启动时优先加载AOF文件 - 启动Redis实例,通过
dbsize、get命令验证数据恢复情况 - 数据验证无误后,重新开启AOF,执行
BGREWRITEAOF生成新的AOF文件,恢复原有持久化配置
2.3 热备份全方案落地
热备份用于日常高可用场景,实现业务无感知的故障切换,核心基于Redis原生主从复制与Sentinel哨兵机制实现。
2.3.1 主从复制:基础热备份方案
底层核心原理
主从复制实现数据的实时热同步,主节点(Master)负责处理写请求,从节点(Slave)负责处理读请求与数据备份,主节点将写命令实时同步到从节点,从节点持续复制主节点数据,主节点故障时可手动切换从节点为主节点,保证业务可用。
全量同步流程通俗解读:
- 从节点执行
SLAVEOF命令,向主节点发起同步请求 - 主节点执行BGSAVE生成RDB快照,同时将新的写命令写入复制缓冲区
- 主节点将RDB快照发送给从节点,从节点清空本地数据,加载RDB快照
- 主节点将复制缓冲区的写命令发送给从节点,从节点重放命令,完成全量同步
- 后续主节点的写命令实时发送给从节点,持续增量同步,保证数据一致性
生产级1主2从主从复制搭建实战
- 主节点(6379)核心配置
port 6379
bind 0.0.0.0
daemonize yes
requirepass your_strong_password
masterauth your_strong_password
appendonly yes
aof-use-rdb-preamble yes
appendfsync everysec
- 从节点(6380、6381)核心配置
port 6380
bind 0.0.0.0
daemonize yes
requirepass your_strong_password
masterauth your_strong_password
slaveof 127.0.0.1 6379
replica-read-only yes
appendonly yes
aof-use-rdb-preamble yes
appendfsync everysec
- 启动与同步验证
# 先启动主节点,再启动2个从节点
redis-server redis_6379.conf
redis-server redis_6380.conf
redis-server redis_6381.conf
# 主节点查看从节点同步状态
redis-cli -a your_strong_password info replication
2.3.2 Sentinel哨兵机制:自动故障转移热备份
底层核心原理
Redis Sentinel是官方提供的高可用解决方案,基于主从复制架构,由多个Sentinel节点组成分布式集群,核心实现四大功能:
- 监控:持续检测主从节点的健康状态
- 通知:节点故障时通知运维人员与客户端
- 自动故障转移:主节点故障时,自动将最优从节点升级为主节点,其余从节点同步新主节点,通知客户端新的主节点地址
- 配置提供:客户端连接Sentinel节点,获取主节点地址
故障转移核心逻辑:至少半数Sentinel节点认为主节点主观下线,才会标记为客观下线,触发故障转移,避免网络抖动导致的误切换,杜绝脑裂问题。
生产级3节点Sentinel集群搭建实战
本次部署3个Sentinel节点(端口26379-26381),监控上述1主2从Redis集群,所有配置可直接复制使用。
- Sentinel节点核心配置以26379节点为例,sentinel_26379.conf配置如下,其余2个节点仅需修改端口、logfile、pidfile、dir参数,其余配置完全一致:
port 26379
daemonize yes
logfile "/var/log/redis/sentinel_26379.log"
pidfile "/var/run/redis-sentinel_26379.pid"
dir "/data/redis/sentinel/26379"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster your_strong_password
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
rename-command FLUSHDB ""
rename-command FLUSHALL ""
核心参数解读:sentinel monitor mymaster 127.0.0.1 6379 2中的2为法定人数,即至少2个Sentinel节点认为主节点故障,才会触发故障转移,生产环境必须设置为Sentinel节点数的半数以上。 2. 启动所有Sentinel节点
redis-sentinel /data/redis/conf/sentinel_26379.conf
redis-sentinel /data/redis/conf/sentinel_26380.conf
redis-sentinel /data/redis/conf/sentinel_26381.conf
- Sentinel状态验证
# 查看监控的主节点状态
redis-cli -p 26379 sentinel master mymaster
# 查看从节点状态
redis-cli -p 26379 sentinel slaves mymaster
# 查看其他Sentinel节点状态
redis-cli -p 26379 sentinel sentinels mymaster
冷热备份全流程流程图
备份方案生产最佳实践
- 生产环境必须构建「混合持久化+主从复制+Sentinel高可用+定时冷备份」四层数据保护体系
- 冷备份文件必须异地存储,定期执行备份恢复演练,确保备份文件可用
- 禁止在业务高峰期执行BGSAVE与AOF重写,避免磁盘IO与CPU占用过高影响业务
- 主从节点必须部署在不同物理机,Sentinel节点必须部署在不同机房/可用区,避免单点故障
三、Redis生产级性能调优全维度实战
Redis性能瓶颈通常出现在硬件、操作系统、Redis配置、客户端、业务代码五个维度,本文从高到低优先级,提供全维度可落地的调优方案。
3.1 硬件层调优:从底层解决性能瓶颈
Redis是内存数据库,硬件性能直接决定了Redis的性能上限,按优先级排序的调优方案如下:
- 内存调优
- 优先使用DDR5高频内存,内存容量必须大于业务峰值数据量的1.5倍,预留30%以上内存给BGSAVE、AOF重写的fork操作与写时复制
- 生产环境必须关闭Swap分区,或设置
vm.swappiness=0,Redis使用Swap会导致性能急剧下降 - 禁止使用内存超分的虚拟机,优先使用物理机,确保内存独占
- CPU调优
- Redis核心命令执行采用单线程模型,优先使用高主频CPU(3.5GHz以上),而非多核心低主频CPU
- 开启CPU亲和性,将Redis进程绑定到固定CPU核心,避免CPU上下文切换带来的性能损耗,示例:
taskset -c 0 redis-server redis.conf - 禁止开启CPU超线程,超线程会导致核心竞争,影响Redis单线程性能
- 磁盘调优
- 持久化文件必须存储在SSD固态硬盘,禁止使用机械硬盘,SSD随机IO性能是机械硬盘的100倍以上
- 单独挂载磁盘给Redis持久化文件,避免与系统、其他业务共用磁盘导致IO竞争
- 磁盘分区使用XFS文件系统,相比ext4更适合大文件读写,性能更优
- 网络调优
- 集群节点必须部署在同一机房同一局域网,节点间网络延迟<1ms,禁止跨公网部署集群
- 优先使用万兆网卡,至少千兆网卡,避免大key传输导致网络阻塞
- 关闭TCP延迟确认,优化网络传输性能
3.2 操作系统层调优:内核参数优化
针对Redis的运行特性,优化Linux内核参数,修改/etc/sysctl.conf文件,执行sysctl -p生效,所有参数均为生产环境最佳实践:
vm.swappiness=0
vm.overcommit_memory=1
vm.dirty_ratio=10
vm.dirty_background_ratio=5
net.core.somaxconn=65535
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_timestamps=1
net.ipv4.tcp_sack=1
net.core.netdev_max_backlog=65535
fs.file-max=1048576
fs.nr_open=1048576
核心参数底层解读:
vm.overcommit_memory=1:允许内存过量分配,解决BGSAVE fork时操作系统内存检查导致的fork失败,Redis官方强制推荐设置net.core.somaxconn=65535:TCP监听队列最大长度,必须大于等于Redis的tcp-backlog配置,避免高并发下客户端连接失败fs.file-max=1048576:系统最大文件句柄数,Redis每个客户端连接占用一个文件句柄,必须设置足够大,避免连接数耗尽
同时修改文件句柄限制,编辑/etc/security/limits.conf添加如下配置,确保Redis进程可打开足够的文件句柄:
redis soft nofile 1048576
redis hard nofile 1048576
redis soft nproc 1048576
redis hard nproc 1048576
3.3 Redis配置层调优:核心参数最佳实践
基于Redis 7.2.5版本,生产环境核心配置调优如下,每个参数均有明确的调优依据,可直接复制使用:
maxclients 10000
tcp-backlog 65535
timeout 300
tcp-keepalive 60
maxmemory 10G
maxmemory-policy volatile-lru
maxmemory-samples 5
appendfsync everysec
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 1G
rdbcompression yes
rdbchecksum yes
slowlog-log-slower-than 10000
slowlog-max-len 1024
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
hash-max-listpack-entries 512
hash-max-listpack-value 64
set-max-intset-entries 512
zset-max-listpack-entries 128
zset-max-listpack-value 64
核心参数调优解读:
- 内存管理调优
maxmemory必须设置,禁止Redis使用超过物理内存70%的容量,避免触发OOMmaxmemory-policy生产首选volatile-lru:对设置了过期时间的key使用LRU算法淘汰,不淘汰永久key,兼顾数据安全与内存管理;全缓存场景无永久key时,可选allkeys-lru- 易混淆点明确区分:
noeviction为默认值,内存满后直接拒绝写请求,生产环境必须禁用
- 懒删除优化
- 四个
lazyfree参数全部设置为yes,开启异步删除,删除大key时将操作放到后台线程执行,不会阻塞主线程,彻底解决大key删除导致的Redis卡顿问题,Redis官方推荐开启
- 慢日志配置
slowlog-log-slower-than=10000:单位为微秒,记录执行时间超过10ms的命令,用于排查慢查询性能瓶颈slowlog-max-len=1024:保留1024条慢日志,避免占用过多内存
- 数据结构内存优化
- 配置hash、set、zset的紧凑编码阈值,元素数量与值大小小于阈值时,使用listpack紧凑编码存储,内存占用降低50%以上,超过阈值自动转换为哈希表存储,平衡内存占用与CPU性能
3.4 客户端层调优:Java客户端最佳实践
Spring Boot 3.x默认使用Lettuce客户端,基于Netty的异步非阻塞架构,性能优于传统的Jedis客户端。本文提供生产级的Spring Boot 3.2.4(最新稳定版)Redis集成方案,完全符合JDK17规范、阿里巴巴Java开发手册,所有代码可直接编译运行。
1. pom.xml依赖配置
所有依赖均采用最新稳定版,符合用户要求的技术规范:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</name>
<description>Redis生产级实战Demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.1.0-jre</guava.version>
<lombok.version>1.18.32</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. application.yml配置文件
包含Redis集群配置、Lettuce连接池调优、MyBatisPlus配置、Swagger3配置,均为生产最佳实践:
spring:
application:
name: redis-demo
data:
redis:
password: your_strong_password
cluster:
nodes:
- 127.0.0.1:7001
- 127.0.0.1:7002
- 127.0.0.1:7003
- 127.0.0.1:7004
- 127.0.0.1:7005
- 127.0.0.1:7006
max-redirects: 3
lettuce:
pool:
max-active: 64
max-idle: 32
min-idle: 8
max-wait: 3000ms
time-between-eviction-runs: 60000ms
shutdown-timeout: 100ms
datasource:
url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: your_mysql_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
springdoc:
swagger-ui:
path: /swagger-ui.html
enabled: true
api-docs:
enabled: true
path: /v3/api-docs
3. Redis配置类
使用Fastjson2实现序列化,解决默认JDK序列化的兼容性与性能问题,符合代码规范:
package com.jam.demo.config;
import com.alibaba.fastjson2.support.spring.data.redis.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
* @author ken
* @date 2026-03-03
*/
@Configuration
public class RedisConfig {
/**
* 配置RedisTemplate,使用Fastjson2实现序列化
* @param connectionFactory Redis连接工厂
* @return RedisTemplate实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
4. Redis工具类
封装常用Redis操作,严格遵循工具类使用规范,使用Spring内置工具类、Guava集合,关键方法添加完整文档注释:
package com.jam.demo.util;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类,封装生产环境常用操作
* @author ken
* @date 2026-03-03
*/
@Slf4j
@Component
public class RedisUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 设置缓存过期时间
* @param key 缓存key
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 操作是否成功
*/
public boolean expire(String key, long timeout, TimeUnit timeUnit) {
if (!StringUtils.hasText(key) || timeout < 0 || ObjectUtils.isEmpty(timeUnit)) {
return false;
}
try {
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, timeUnit));
} catch (Exception e) {
log.error("设置缓存过期时间失败,key:{}", key, e);
return false;
}
}
/**
* 获取缓存过期时间
* @param key 缓存key
* @return 过期时间(秒),-1为永久有效,-2为key不存在
*/
public long getExpire(String key) {
if (!StringUtils.hasText(key)) {
return -2;
}
try {
Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
return ObjectUtils.isEmpty(expire) ? -2 : expire;
} catch (Exception e) {
log.error("获取缓存过期时间失败,key:{}", key, e);
return -2;
}
}
/**
* 判断key是否存在
* @param key 缓存key
* @return key是否存在
*/
public boolean hasKey(String key) {
if (!StringUtils.hasText(key)) {
return false;
}
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
log.error("判断key是否存在失败,key:{}", key, e);
return false;
}
}
/**
* 删除单个缓存
* @param key 缓存key
* @return 操作是否成功
*/
public boolean delete(String key) {
if (!StringUtils.hasText(key)) {
return false;
}
try {
return Boolean.TRUE.equals(redisTemplate.delete(key));
} catch (Exception e) {
log.error("删除缓存失败,key:{}", key, e);
return false;
}
}
/**
* 批量删除缓存
* @param keys 缓存key集合
* @return 成功删除的数量
*/
public long deleteBatch(Collection<String> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0;
}
try {
Long count = redisTemplate.delete(keys);
return ObjectUtils.isEmpty(count) ? 0 : count;
} catch (Exception e) {
log.error("批量删除缓存失败", e);
return 0;
}
}
/**
* 获取缓存值
* @param key 缓存key
* @return 缓存值
*/
public Object get(String key) {
if (!StringUtils.hasText(key)) {
return null;
}
try {
return redisTemplate.opsForValue().get(key);
} catch (Exception e) {
log.error("获取缓存失败,key:{}", key, e);
return null;
}
}
/**
* 设置缓存值
* @param key 缓存key
* @param value 缓存值
* @return 操作是否成功
*/
public boolean set(String key, Object value) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(value)) {
return false;
}
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("设置缓存失败,key:{}", key, e);
return false;
}
}
/**
* 设置缓存值并指定过期时间
* @param key 缓存key
* @param value 缓存值
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 操作是否成功
*/
public boolean setWithExpire(String key, Object value, long timeout, TimeUnit timeUnit) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(value) || timeout < 0 || ObjectUtils.isEmpty(timeUnit)) {
return false;
}
try {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
return true;
} catch (Exception e) {
log.error("设置缓存并指定过期时间失败,key:{}", key, e);
return false;
}
}
/**
* 缓存值递增
* @param key 缓存key
* @param delta 递增步长
* @return 递增后的值
*/
public long increment(String key, long delta) {
if (!StringUtils.hasText(key) || delta < 0) {
return -1;
}
try {
Long result = redisTemplate.opsForValue().increment(key, delta);
return ObjectUtils.isEmpty(result) ? -1 : result;
} catch (Exception e) {
log.error("缓存递增失败,key:{}", key, e);
return -1;
}
}
/**
* 缓存值递减
* @param key 缓存key
* @param delta 递减步长
* @return 递减后的值
*/
public long decrement(String key, long delta) {
if (!StringUtils.hasText(key) || delta < 0) {
return -1;
}
try {
Long result = redisTemplate.opsForValue().decrement(key, delta);
return ObjectUtils.isEmpty(result) ? -1 : result;
} catch (Exception e) {
log.error("缓存递减失败,key:{}", key, e);
return -1;
}
}
/**
* Hash结构获取单个字段值
* @param key 缓存key
* @param hashKey hash字段名
* @return hash字段值
*/
public Object hashGet(String key, String hashKey) {
if (!StringUtils.hasText(key) || !StringUtils.hasText(hashKey)) {
return null;
}
try {
return redisTemplate.opsForHash().get(key, hashKey);
} catch (Exception e) {
log.error("Hash获取失败,key:{}, hashKey:{}", key, hashKey, e);
return null;
}
}
/**
* Hash结构设置单个字段值
* @param key 缓存key
* @param hashKey hash字段名
* @param value hash字段值
* @return 操作是否成功
*/
public boolean hashSet(String key, String hashKey, Object value) {
if (!StringUtils.hasText(key) || !StringUtils.hasText(hashKey) || ObjectUtils.isEmpty(value)) {
return false;
}
try {
redisTemplate.opsForHash().put(key, hashKey, value);
return true;
} catch (Exception e) {
log.error("Hash设置失败,key:{}, hashKey:{}", key, hashKey, e);
return false;
}
}
/**
* Hash结构批量设置字段值
* @param key 缓存key
* @param map hash字段键值对
* @return 操作是否成功
*/
public boolean hashSetBatch(String key, Map<String, Object> map) {
if (!StringUtils.hasText(key) || CollectionUtils.isEmpty(map)) {
return false;
}
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error("Hash批量设置失败,key:{}", key, e);
return false;
}
}
/**
* Hash结构获取所有字段键值对
* @param key 缓存key
* @return hash所有字段键值对
*/
public Map<Object, Object> hashGetAll(String key) {
if (!StringUtils.hasText(key)) {
return Maps.newHashMap();
}
try {
return redisTemplate.opsForHash().entries(key);
} catch (Exception e) {
log.error("Hash获取所有键值对失败,key:{}", key, e);
return Maps.newHashMap();
}
}
/**
* Hash结构删除指定字段
* @param key 缓存key
* @param hashKeys 待删除的hash字段名
* @return 成功删除的数量
*/
public long hashDelete(String key, Object... hashKeys) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(hashKeys)) {
return 0;
}
try {
Long count = redisTemplate.opsForHash().delete(key, hashKeys);
return ObjectUtils.isEmpty(count) ? 0 : count;
} catch (Exception e) {
log.error("Hash删除失败,key:{}", key, e);
return 0;
}
}
/**
* List结构获取所有元素
* @param key 缓存key
* @return List所有元素
*/
public List<Object> listGetAll(String key) {
if (!StringUtils.hasText(key)) {
return Lists.newArrayList();
}
try {
return redisTemplate.opsForList().range(key, 0, -1);
} catch (Exception e) {
log.error("List获取所有元素失败,key:{}", key, e);
return Lists.newArrayList();
}
}
/**
* List结构右入队
* @param key 缓存key
* @param value 入队元素
* @return 入队后List的长度
*/
public long listRightPush(String key, Object value) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(value)) {
return -1;
}
try {
Long result = redisTemplate.opsForList().rightPush(key, value);
return ObjectUtils.isEmpty(result) ? -1 : result;
} catch (Exception e) {
log.error("List右入队失败,key:{}", key, e);
return -1;
}
}
/**
* List结构左出队
* @param key 缓存key
* @return 出队元素
*/
public Object listLeftPop(String key) {
if (!StringUtils.hasText(key)) {
return null;
}
try {
return redisTemplate.opsForList().leftPop(key);
} catch (Exception e) {
log.error("List左出队失败,key:{}", key, e);
return null;
}
}
}
5. Redis操作Controller
添加Swagger3注解,实现标准化的Redis操作接口,符合OpenAPI 3.0规范:
package com.jam.demo.controller;
import com.jam.demo.util.RedisUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import jakarta.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Redis操作接口
* @author ken
* @date 2026-03-03
*/
@Slf4j
@RestController
@RequestMapping("/redis")
@Tag(name = "Redis操作接口", description = "Redis生产级实战标准化操作接口")
public class RedisController {
@Resource
private RedisUtil redisUtil;
/**
* 设置缓存
* @param key 缓存key
* @param value 缓存值
* @param expire 过期时间(秒),可选
* @return 操作结果
*/
@PostMapping("/set")
@Operation(summary = "设置缓存", description = "设置Redis缓存,支持指定过期时间")
public String set(
@Parameter(description = "缓存key", required = true) @RequestParam String key,
@Parameter(description = "缓存值", required = true) @RequestParam String value,
@Parameter(description = "过期时间,单位秒") @RequestParam(required = false) Long expire) {
if (!StringUtils.hasText(key)) {
return "key不能为空";
}
if (!StringUtils.hasText(value)) {
return "value不能为空";
}
boolean result;
if (!ObjectUtils.isEmpty(expire) && expire > 0) {
result = redisUtil.setWithExpire(key, value, expire, TimeUnit.SECONDS);
} else {
result = redisUtil.set(key, value);
}
return result ? "设置成功" : "设置失败";
}
/**
* 获取缓存
* @param key 缓存key
* @return 缓存值
*/
@GetMapping("/get")
@Operation(summary = "获取缓存", description = "根据key获取Redis缓存值")
public Object get(@Parameter(description = "缓存key", required = true) @RequestParam String key) {
if (!StringUtils.hasText(key)) {
return "key不能为空";
}
Object value = redisUtil.get(key);
return ObjectUtils.isEmpty(value) ? "缓存不存在" : value;
}
/**
* 删除缓存
* @param key 缓存key
* @return 操作结果
*/
@DeleteMapping("/delete")
@Operation(summary = "删除缓存", description = "根据key删除Redis缓存")
public String delete(@Parameter(description = "缓存key", required = true) @RequestParam String key) {
if (!StringUtils.hasText(key)) {
return "key不能为空";
}
boolean result = redisUtil.delete(key);
return result ? "删除成功" : "删除失败";
}
/**
* 判断key是否存在
* @param key 缓存key
* @return 操作结果
*/
@GetMapping("/hasKey")
@Operation(summary = "判断key是否存在", description = "判断Redis中是否存在指定的key")
public String hasKey(@Parameter(description = "缓存key", required = true) @RequestParam String key) {
if (!StringUtils.hasText(key)) {
return "key不能为空";
}
boolean exists = redisUtil.hasKey(key);
return exists ? "key存在" : "key不存在";
}
}
6. 项目启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Redis生产级实战Demo启动类
* @author ken
* @date 2026-03-03
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class RedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
}
客户端调优核心最佳实践
- 连接池调优:
max-active不宜设置过大,Redis单线程处理请求,过大的连接数会导致性能下降,生产环境根据QPS设置为32-64即可,避免频繁创建销毁连接 - 序列化优化:使用Fastjson2序列化,性能优于JDK序列化与Jackson,内存占用更小,传输效率更高
- 禁止使用高危操作:生产环境禁止使用
keys *、hgetall、flushall等全量操作,会阻塞主线程导致Redis卡顿 - 批量操作优化:使用Pipeline批量执行命令,减少网络IO次数,提升性能,单次Pipeline批量大小不宜超过1000条,避免网络阻塞
- 集群模式配置:开启
max-redirects=3,自动处理MOVED/ASK重定向,避免业务层处理路由逻辑
3.5 业务代码层调优:从根源解决性能问题
- 禁止大key
- 大key定义:String类型value超过10KB,Hash/List/Set/Zset类型元素数量超过1000个
- 大key危害:网络传输慢、序列化耗时、删除阻塞主线程、集群迁移卡顿、内存占用过高
- 解决方案:拆分大key,将大Hash拆分为多个小Hash,大List拆分为多个小List,通过Hash Tag保证同SLOT
- 冷热数据分离
- 热数据:高频访问的热点数据(如用户会话、商品基础信息),放入Redis并设置合理的过期时间
- 冷数据:低频访问的历史数据(如历史订单、操作日志),禁止放入Redis,存储到MySQL、ES等持久化数据库
- 解决方案:业务层实现冷热数据分离,热点数据走缓存,冷数据直接访问数据库,避免Redis内存浪费
- 缓存三大问题解决方案
- 缓存穿透:查询不存在的数据,请求直接打到数据库,解决方案:布隆过滤器、缓存空值、参数合法性校验
- 缓存击穿:热点key过期,大量请求打到数据库,解决方案:热点key永不过期、分布式互斥锁、提前续期
- 缓存雪崩:大量key同时过期,大量请求打到数据库,解决方案:过期时间添加随机值、多级缓存、集群高可用
- 合理使用数据结构
- 计数场景使用
INCR/DECR原子命令,禁止使用GET+SET非原子操作,避免并发问题 - 排行榜场景使用Zset,禁止使用List排序,性能差距可达百倍以上
- 去重场景使用Set,禁止使用Hash实现去重,内存占用更高
- 对象存储使用Hash,禁止拆分为多个String存储,节省内存且操作更便捷
四、Redis数据迁移全场景实战方案
数据迁移是生产环境的高频场景,如集群扩缩容、机房迁移、版本升级、集群架构切换,本文覆盖全场景的迁移方案,所有步骤均经过生产环境验证,保证数据一致性与业务连续性。
4.1 数据迁移核心方案对比
| 迁移方案 | 迁移方式 | 业务中断 | 数据一致性 | 适用场景 |
| RDB离线迁移 | 全量离线迁移 | 有 | 极高 | 停机维护窗口、小数据量、跨版本迁移 |
| Redis Cluster reshard | 在线增量迁移 | 无 | 极高 | Redis Cluster集群内部扩缩容、槽位迁移 |
| Codis自动迁移 | 在线增量迁移 | 无 | 极高 | Codis集群内部扩缩容、分片迁移 |
| redis-shake | 在线全量+增量迁移 | 无 | 极高 | 跨集群迁移、机房迁移、架构切换、跨版本迁移 |
4.2 离线迁移:RDB文件全量迁移
适用场景
业务可接受停机维护、小数据量实例、跨大版本迁移(如Redis 5.x升级到7.x)、单实例迁移到集群。
迁移实战步骤
- 源实例操作
- 业务停机,停止源实例的所有写入请求
- 执行
BGSAVE生成最新的全量RDB快照 - 等待BGSAVE完成,复制dump.rdb文件到目标服务器
- 目标实例操作
- 停止目标Redis实例
- 备份目标实例现有的RDB与AOF文件,防止恢复失败
- 将源实例的dump.rdb文件复制到目标实例的dir目录,覆盖原有文件
- 临时关闭AOF,修改redis.conf设置
appendonly no - 启动目标实例,通过
dbsize、随机get命令验证数据完整性 - 数据验证无误后,重新开启AOF,执行
BGREWRITEAOF生成新的AOF文件,恢复原有持久化配置
- 业务切换
- 将业务的Redis连接地址切换到目标实例
- 启动业务,验证读写功能正常,迁移完成
核心注意事项
- 迁移前必须停止源实例的写入,否则会导致数据不一致
- 单实例内存超过20G不适合使用离线迁移,停机时间过长
- 迁移前必须做数据校验,对比源与目标实例的key数量、数据一致性
4.3 Redis Cluster集群在线扩缩容迁移
Redis Cluster集群的扩缩容本质是SLOT的在线迁移,全程业务无感知,数据零丢失,是生产环境集群扩缩容的官方原生方案。
4.3.1 集群扩容实战:新增1主1从节点
现有3主3从集群(7001-7006),新增2个节点7007(主)、7008(从),实现4主4从集群,步骤如下:
- 准备新节点的配置文件,与原有集群节点配置完全一致,仅修改端口、pidfile、logfile、dir、cluster-config-file参数,启动2个新节点
- 将新节点加入现有集群
# 新增主节点7007到集群
redis-cli -a your_strong_password --cluster add-node 127.0.0.1:7007 127.0.0.1:7001
# 新增从节点7008到集群,绑定主节点7007
redis-cli -a your_strong_password --cluster add-node 127.0.0.1:7008 127.0.0.1:7001 --cluster-slave --cluster-master-id 7007节点ID
- 执行SLOT在线迁移
redis-cli -a your_strong_password --cluster reshard 127.0.0.1:7001
执行后按提示输入:
- 要迁移的SLOT数量:4096(16384/4,4个主节点平均分配)
- 目标节点ID:新主节点7007的ID
- 源节点ID:输入
all,从所有原有主节点平均迁移SLOT - 输入
yes确认,系统自动完成在线迁移,全程业务无感知
- 迁移完成后,执行
redis-cli -a your_strong_password -c -p 7001 cluster slots验证SLOT分配均匀,数据读写正常,扩容完成
4.3.2 集群缩容实战:下线1主1从节点
- 执行reshard命令,将待下线主节点的所有SLOT迁移到其他主节点
- 验证待下线主节点的SLOT数量为0,无数据存储
- 先删除从节点,再删除主节点
# 删除从节点
redis-cli -a your_strong_password --cluster del-node 127.0.0.1:7001 从节点ID
# 删除主节点
redis-cli -a your_strong_password --cluster del-node 127.0.0.1:7001 主节点ID
- 验证集群状态正常,所有SLOT均有节点负责,数据读写正常,缩容完成
4.4 跨集群在线迁移:redis-shake工具实战
redis-shake是阿里云开源的Redis数据迁移工具,支持全量+增量同步、在线迁移、业务零中断,兼容单实例、主从、Codis、Redis Cluster之间的跨架构迁移,是生产环境跨集群迁移的首选方案,本次采用最新稳定版v2.2.0。
迁移实战:主从集群迁移到Redis Cluster集群
- 下载redis-shake最新稳定版,解压到服务器
- 编辑迁移配置文件shake.toml,核心配置如下:
[source]
type = "standalone"
address = "127.0.0.1:6379"
password = "your_source_password"
is_tls = false
[target]
type = "cluster"
address = "127.0.0.1:7001"
password = "your_target_password"
is_tls = false
[advanced]
dir = "./data"
ncpu = 4
parallel = 8
keep_source = false
rewrite = true
filter_db = []
filter_key = []
big_key_threshold = 52428800
- 启动全量+增量同步
./redis-shake -conf shake.toml -type sync
- 同步监控:查看运行日志,全量同步完成后进入增量同步阶段,当增量同步延迟为0时,源与目标实例数据完全一致
- 业务切换:业务停机,停止源实例写入,等待增量同步完成,将业务连接地址切换到目标集群,启动业务,验证读写正常,迁移完成
核心注意事项
- 迁移前必须拆分大key,否则会导致迁移卡顿、目标实例阻塞
- 迁移过程中监控源与目标实例的CPU、内存、网络、磁盘IO,避免影响业务
- 必须制定回滚方案,迁移出现问题时可快速切换回源集群
- 迁移完成后必须做全量数据校验,确保数据零丢失
五、生产环境高频踩坑避坑指南
- fork阻塞坑
- 问题:Redis实例内存过大,fork子进程时阻塞主线程,导致业务超时、心跳失败、主从切换
- 解决方案:单实例内存不超过20G,采用集群分片,业务低峰期执行BGSAVE与AOF重写
- 大key坑
- 问题:大key导致Redis卡顿、OOM、集群迁移失败、网络阻塞
- 解决方案:定期扫描大key,拆分大key为多个小key,禁止存储大体积数据到Redis
- 内存淘汰坑
- 问题:未设置maxmemory,或maxmemory-policy设置为noeviction,内存满后拒绝写请求,导致业务故障
- 解决方案:必须设置maxmemory为物理内存的70%,选择volatile-lru或allkeys-lru淘汰策略
- Swap坑
- 问题:Redis使用Swap分区,导致性能急剧下降,延迟飙升
- 解决方案:关闭Swap分区,设置vm.swappiness=0,确保Redis始终使用物理内存
- 集群脑裂坑
- 问题:主节点网络分区,出现双主节点,分区恢复后数据丢失
- 解决方案:设置
min-replicas-to-write=1、min-replicas-max-lag=10,主节点至少有1个正常同步的从节点才允许写入
- 持久化IO坑
- 问题:appendfsync设置为always,导致磁盘IO拉满,Redis性能极差;AOF重写时持续刷盘,导致IO冲突
- 解决方案:appendfsync设置为everysec,开启no-appendfsync-on-rewrite=yes
- TCP连接坑
- 问题:tcp-backlog设置过小,高并发下客户端连接失败,业务报错
- 解决方案:设置tcp-backlog=65535,同步修改内核参数net.core.somaxconn=65535
结语
Redis作为互联网业务的核心基础设施,其生产环境的稳定性与性能直接决定了业务的上限。本文从集群搭建、冷热备份、性能调优、数据迁移四大核心生产场景出发,讲透了底层实现逻辑,提供了全量可落地、零错误的实战方案。
生产环境Redis运维的核心准则是:预防大于治理,提前做好高可用架构、数据备份、性能优化,才能从根源上避免故障。希望本文能帮助开发者夯实Redis底层基础,解决生产环境的实际问题,打造稳定、高性能、高可用的Redis集群。