Redis 与 MySQL 共用服务器时的内存优化策略
核心挑战
Redis(全内存数据库)和 MySQL(磁盘为主、缓冲池为辅)共享同一物理内存,竞争资源可能导致:
- OOM(Out of Memory) 导致服务崩溃
- Swap 交换 导致性能急剧下降
- GC/缓冲竞争 影响响应时间
一、总体架构原则
┌─────────────────────────────────────────────┐
│ 物理服务器 (总内存: e.g. 32GB) │
├──────────────────┬──────────────────────────┤
│ Redis 内存池 │ MySQL Buffer Pool │
│ + 系统预留 │ + OS Cache │
├──────────────────┴──────────────────────────┤
│ Linux Kernel / Swap / Other Apps │
└─────────────────────────────────────────────┘
黄金法则:永远不要将 Redis 数据全部放入内存而不留余量给 MySQL 和操作系统。
二、Redis 侧优化
1. 合理设置 maxmemory
# 示例:32GB 服务器,分配给 Redis 8-10GB
maxmemory 8gb
# 淘汰策略选择(根据业务场景)
maxmemory-policy allkeys-lru # 适合缓存场景
# maxmemory-policy volatile-lru # 只淘汰有 TTL 的键
# maxmemory-policy noeviction # 生产环境慎用,可能 OOM
2. 使用高效数据结构
# ❌ 避免:大量小 String 对象(每个都有 RedisObject 开销 ~32-48B)
redis.set("user:1:name", "Alice")
redis.set("user:2:name", "Bob")
# ✅ 推荐:Hash 压缩多个字段
redis.hset("user:1", "name", "Alice", "age", 25, "email", "a@b.com")
# ✅ 更优:使用 Pipeline + 批量操作减少网络往返和序列化开销
pipeline = redis.pipeline()
for i in range(1000):
pipeline.hset(f"user:{i}", "name", f"User{i}")
pipeline.execute()
3. 启用压缩(Redis 6+)
# Redis 6.2+ 支持主动/被动压缩
activedefrag yes
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
4. 控制 Key 大小和数量
# ❌ 避免超大 Value
redis.set("big_data", large_binary_string) # > 1KB 考虑分片或存 MySQL
# ✅ 限制单个 key 的大小
def set_with_size_check(key, value, max_size=1024):
if len(value.encode()) > max_size:
raise ValueError("Value too large for Redis")
redis.set(key, value)
5. 监控内存碎片率
# 查看内存碎片率
redis-cli INFO memory | grep mem_fragmentation_ratio
# 理想值: 1.0 - 1.5
# > 1.5: 碎片严重,启用 activedefrag
# < 1.0: 可能使用了 overcommit_memory=0,需调整
三、MySQL 侧优化
1. 精确配置 innodb_buffer_pool_size
[mysqld]
# 公式:Buffer Pool ≈ (可用内存 × 70%) - Redis 内存 - 系统预留
# 例如:32GB 服务器,Redis 用 8GB,系统预留 4GB
# Buffer Pool ≈ (32 - 8 - 4) × 0.7 ≈ 14GB → 设为 12-14GB
innodb_buffer_pool_size = 12G
innodb_buffer_pool_instances = 8
# 关键表单独分配 buffer pool(MySQL 8.0+)
innodb_buffer_pool_instances = 8
2. 禁用不必要的日志和缓冲
# 如果不需要 binlog(如只读从库)
skip-log-bin
# 减小 sort/buffer 临时空间
sort_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 4M
join_buffer_size = 1M
# 关闭 query cache(MySQL 8.0 已移除,5.7 建议关闭)
query_cache_type = 0
query_cache_size = 0
3. 优化 InnoDB 页大小和刷盘策略
# 减少 fsync 频率,牺牲少量持久性换取内存效率
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
四、操作系统层面优化
1. 内存隔离与控制组(cgroups)
# CentOS/RHEL: 使用 systemd slice 限制
cat <<EOF > /etc/systemd/system/redis.service.d/memory.conf
[Service]
MemoryMax=9G
MemoryHigh=8.5G
EOF
# MySQL 同样限制
cat <<EOF > /etc/systemd/system/mysqld.service.d/memory.conf
[Service]
MemoryMax=14G
MemoryHigh=13G
EOF
2. 调整 Overcommit 内存策略
# Redis 要求此值为 1,否则可能因 OOM Killer 被终止
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf
sysctl -p
# 减少 swap 使用倾向
echo "vm.swappiness = 10" >> /etc/sysctl.conf
sysctl -p
3. NUMA 亲和性(多 NUMA 节点服务器)
# 绑定 Redis 到特定 NUMA 节点
numactl --membind=0 --cpus=0-7 redis-server --port 6379
# MySQL 同样处理
numactl --membind=1 --cpus=8-15 mysqld_safe &
五、动态内存分配方案
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 静态划分 | 简单可控 | 资源利用率低 |
| 动态协商 | 利用率高 | 实现复杂 |
| 优先级抢占 | 保证关键服务 | 可能饿死另一方 |
动态内存管理伪代码
import time
import psutil
class MemoryManager:
def __init__(self, total_mem, redis_target=0.25, mysql_target=0.55):
self.total_mem = total_mem
self.redis_max = int(total_mem * redis_target)
self.mysql_max = int(total_mem * mysql_target)
self.system_reserve = int(total_mem * 0.10)
def get_available_memory(self):
return psutil.virtual_memory().available
def adjust_redis_maxmemory(self):
avail = self.get_available_memory()
current_redis_usage = self.get_redis_used_memory()
if current_redis_usage > self.redis_max:
# 触发 Redis 淘汰
redis_cli.execute_command("CONFIG SET maxmemory", self.redis_max)
# 如果 MySQL 压力小,可临时释放部分内存给 Redis
if self.get_mysql_buffer_pool_usage() < self.mysql_max * 0.6:
new_limit = min(self.redis_max + 1024*1024*512,
self.total_mem - self.mysql_max - self.system_reserve)
redis_cli.execute_command("CONFIG SET maxmemory", new_limit)
def monitor_loop(self):
while True:
self.adjust_redis_maxmemory()
time.sleep(30)
六、监控与告警体系
关键监控指标
monitoring_metrics:
redis:
- used_memory_human # 当前使用内存
- maxmemory_human # 最大允许内存
- mem_fragmentation_ratio # 碎片率
- evicted_keys # 被驱逐的 key 数
- keyspace_hits/misses # 缓存命中率
mysql:
- innodb_buffer_pool_pages_free # 空闲页
- innodb_buffer_pool_read_requests # 逻辑读
- innodb_buffer_pool_reads # 物理读
- QPS / TPS # 负载
system:
- available_memory # 可用内存
- swap_usage # swap 使用率
- page_faults # 缺页中断
- oom_kill_count # OOM 事件
Grafana 告警规则示例
# Redis 内存接近上限
redis_memory_used_bytes / redis_maxmemory_bytes > 0.85
# MySQL Buffer Pool 命中率下降
1 - (mysql_innodb_buffer_pool_read_requests /
mysql_innodb_buffer_pool_reads) < 0.95
# 系统 swap 开始使用
system_swap_usage_bytes > 0
# OOM 事件
increase(node_vmstat_oom_kill[5m]) > 0
七、容量规划计算示例
假设服务器: 32GB RAM, 8 CPU, SSD
步骤1: 确定 Redis 数据规模
- 活跃数据: 5GB
- 峰值并发缓存: 3GB
- 安全系数: 1.5x
→ Redis maxmemory = 12GB × 1.5 = 18GB? 不!
步骤2: 保守分配
- Redis: 8GB (25%)
- MySQL: 12GB (37.5%) — innodb_buffer_pool_size
- OS + 其他: 4GB (12.5%)
- 预留弹性: 8GB (25%) ← 动态调节空间
步骤3: 验证
Redis 峰值使用: ~6.5GB < 8GB ✓
MySQL 热点数据: ~10GB < 12GB ✓
剩余 8GB 应对突发流量 ✓
八、最佳实践总结
✅ 必须做 ❌ 绝对避免
─────────────────────────────────────────────────
设定明确的 maxmemory 让 Redis 无限制增长
使用 LRU/LFU 淘汰策略 依赖 swap 作为扩展
cgroups 限制各进程内存上限 所有组件都设默认值
监控碎片率和淘汰计数 忽略 OOM 风险
定期清理过期和无效 key 存储 >1MB 的大对象
分离冷热数据 在同一台机器跑所有中间件
最终建议:如果业务持续增长,优先考虑拆分。Redis 和 MySQL 分离到不同服务器是更可靠的长期方案。共用服务器仅适用于中小规模、预算受限的场景,且必须有完善的监控和应急预案。
CLOUD技术博