Skip to content
清晨的一缕阳光
返回

Redis 最佳实践总结

Redis 最佳实践总结

经过多个项目的实战积累,我总结了 Redis 在生产环境中的最佳实践。本文涵盖架构设计、性能优化、运维监控、故障处理等方面,帮助你构建稳定高效的 Redis 系统。

一、架构设计最佳实践

1.1 选型建议

单机版

主从复制

哨兵模式

Cluster 集群

1.2 内存规划

内存分配建议:
┌─────────────────────────────────────┐
│ 总内存:16GB                        │
├─────────────────────────────────────┤
│ 数据区:12GB (75%)                  │
│ - 实际数据存储                       │
├─────────────────────────────────────┤
│ 缓冲区:2GB (12.5%)                 │
│ - 客户端输入/输出缓冲               │
│ - 复制缓冲                           │
├─────────────────────────────────────┤
│ 预留:2GB (12.5%)                   │
│ - 内存碎片                           │
│ - fork 开销                          │
└─────────────────────────────────────┘

配置示例

# 最大内存(预留 25%)
maxmemory 12gb

# 内存淘汰策略(推荐 allkeys-lru)
maxmemory-policy allkeys-lru

# 淘汰比例(默认 5%)
maxmemory-samples 10

1.3 Key 设计规范

命名规范

# 推荐格式
业务名:模块名:ID:字段
user:profile:12345:name
order:detail:202511050001:status

# 使用冒号分隔(Redis 内部优化)
# 避免使用特殊字符:空格、换行、引号

# 统一前缀(便于管理)
app:user:*
app:order:*
app:cache:*

长度控制

# Key 长度建议
# 推荐:10-50 字符
# 上限:512 MB(但不建议过长)

# 过长的 key 浪费内存
# user:profile:information:detail:content:12345 (45 字符)
# 优化:user:profile:12345:detail (25 字符)

过期时间设置

// 避免同时过期
// 错误:所有 key 在同一时间过期
redis.setex("cache:key", 3600, value);

// 正确:添加随机时间
int expireTime = 3600 + random.nextInt(300); // 3600-3900 秒
redis.setex("cache:key", expireTime, value);

1.4 数据类型选择

场景推荐类型说明
简单缓存String最基本、性能最好
计数器String (INCR)原子操作
排行榜ZSet自动排序
消息队列List/Stream先进先出
购物车Hash字段操作
标签系统Set去重、交集
签到统计Bitmap节省空间
UV 统计HyperLogLog基数统计
地理位置GEO附近的人

二、性能优化最佳实践

2.1 批量操作

Pipeline 批量读写

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import java.util.*;

public class PipelineExample {
    private Jedis jedis;
    
    public PipelineExample(Jedis jedis) {
        this.jedis = jedis;
    }
    
    /**
     * 批量写入(Pipeline)
     */
    public void batchSet(Map<String, String> data) {
        try (Pipeline pipeline = jedis.pipelined()) {
            for (Map.Entry<String, String> entry : data.entrySet()) {
                pipeline.set(entry.getKey(), entry.getValue());
            }
            pipeline.sync(); // 一次性发送
        }
    }
    
    /**
     * 批量读取(Pipeline)
     */
    public Map<String, String> batchGet(List<String> keys) {
        Map<String, String> result = new HashMap<>();
        
        try (Pipeline pipeline = jedis.pipelined()) {
            List<Response<String>> responses = new ArrayList<>();
            
            for (String key : keys) {
                responses.add(pipeline.get(key));
            }
            
            pipeline.sync();
            
            for (int i = 0; i < keys.size(); i++) {
                result.put(keys.get(i), responses.get(i).get());
            }
        }
        
        return result;
    }
}

// 性能对比
// 逐个写入 10000 条:~10 秒
// Pipeline 写入 10000 条:~0.5 秒
// 提升:20 倍

MGet/MSet 批量操作

// 批量读取(MGET)
List<String> values = jedis.mget("key1", "key2", "key3");

// 批量写入(MSET)
jedis.mset("key1", "value1", "key2", "value2", "key3", "value3");

// 注意:MGET/MSET 是原子操作,Pipeline 不是

2.2 连接池优化

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolExample {
    public static JedisPool createOptimizedPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        
        // 最大连接数(根据并发调整)
        config.setMaxTotal(50);
        
        // 最大空闲连接
        config.setMaxIdle(20);
        
        // 最小空闲连接
        config.setMinIdle(5);
        
        // 获取连接最大等待时间(毫秒)
        config.setMaxWaitMillis(3000);
        
        // 空闲连接检查
        config.setTestOnBorrow(false); // 性能考虑
        config.setTestWhileIdle(true);
        config.setTimeBetweenEvictionRunsMillis(30000);
        
        // 连接超时
        int timeout = 5000;
        
        return new JedisPool(config, "127.0.0.1", 6379, timeout, "password");
    }
}

2.3 命令优化

避免的命令

# KEYS 命令(阻塞主线程)
# 错误:KEYS user:*
# 正确:SCAN 0 MATCH user:* COUNT 1000

# HGETALL(大 Hash 时慢)
# 错误:HGETALL large:hash
# 正确:HSCAN large:hash 0 COUNT 100

# SMEMBERS(大 Set 时慢)
# 错误:SMEMBERS large:set
# 正确:SSCAN large:set 0 COUNT 100

# LRANGE 0 -1(大 List 时慢)
# 错误:LRANGE large:list 0 -1
# 正确:LRANGE large:list 0 100

推荐的替代方案

// 使用 SCAN 代替 KEYS
public List<String> scanKeys(String pattern) {
    List<String> keys = new ArrayList<>();
    String cursor = ScanParams.SCAN_POINTER_START;
    ScanParams scanParams = new ScanParams();
    scanParams.match(pattern);
    scanParams.count(1000);
    
    while (true) {
        ScanResult<String> result = jedis.scan(cursor, scanParams);
        keys.addAll(result.getResult());
        cursor = result.getCursor();
        
        if (ScanParams.SCAN_POINTER_START.equals(cursor)) {
            break;
        }
    }
    
    return keys;
}

// 使用 HSCAN 代替 HGETALL
public Map<String, String> scanHash(String key) {
    Map<String, String> result = new HashMap<>();
    String cursor = ScanParams.SCAN_POINTER_START;
    ScanParams scanParams = new ScanParams();
    scanParams.count(1000);
    
    while (true) {
        ScanResult<Map.Entry<String, String>> result = jedis.hscan(key, cursor, scanParams);
        for (Map.Entry<String, String> entry : result.getResult()) {
            // 处理 entry
        }
        cursor = result.getCursor();
        
        if (ScanParams.SCAN_POINTER_START.equals(cursor)) {
            break;
        }
    }
    
    return result;
}

2.4 Lua 脚本优化

使用 Lua 减少网络往返

-- 限流脚本
-- KEYS[1]: rate_limit_key
-- ARGV[1]: max_count
-- ARGV[2]: window_size

local key = KEYS[1]
local max_count = tonumber(ARGV[1])
local window_size = tonumber(ARGV[2])

local current = redis.call('GET', key)

if current == false then
    redis.call('SET', key, 1, 'EX', window_size)
    return 1
end

current = tonumber(current)

if current < max_count then
    redis.call('INCR', key)
    return 1
else
    return 0
end
// Java 调用 Lua 脚本
public class RateLimiter {
    private Jedis jedis;
    private String scriptSha;
    
    public RateLimiter(Jedis jedis) {
        this.jedis = jedis;
        // 预加载脚本
        String script = loadScript();
        this.scriptSha = jedis.scriptLoad(script);
    }
    
    public boolean tryAcquire(String key, int maxCount, int windowSize) {
        Object result = jedis.evalsha(
            scriptSha,
            Collections.singletonList(key),
            Arrays.asList(String.valueOf(maxCount), String.valueOf(windowSize))
        );
        return (Long) result == 1;
    }
}

三、运维监控最佳实践

3.1 关键指标监控

#!/bin/bash
# redis_monitor.sh

REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
ALERT_EMAIL="admin@example.com"

# 获取 INFO 信息
INFO=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT INFO)

# 关键指标
USED_MEMORY=$(echo "$INFO" | grep "used_memory:" | cut -d: -f2 | tr -d '\r')
USED_MEMORY_PEAK=$(echo "$INFO" | grep "used_memory_peak:" | cut -d: -f2 | tr -d '\r')
CONNECTED_CLIENTS=$(echo "$INFO" | grep "connected_clients:" | cut -d: -f2 | tr -d '\r')
BLOCKED_CLIENTS=$(echo "$INFO" | grep "blocked_clients:" | cut -d: -f2 | tr -d '\r')
OPS_PER_SEC=$(echo "$INFO" | grep "instantaneous_ops_per_sec:" | cut -d: -f2 | tr -d '\r')
REJECTED_CONNECTIONS=$(echo "$INFO" | grep "rejected_connections:" | cut -d: -f2 | tr -d '\r')

# 告警检查
echo "=== Redis 监控 ==="
echo "内存使用:$(($USED_MEMORY / 1024 / 1024)) MB"
echo "内存峰值:$(($USED_MEMORY_PEAK / 1024 / 1024)) MB"
echo "连接数:$CONNECTED_CLIENTS"
echo "阻塞连接:$BLOCKED_CLIENTS"
echo "QPS: $OPS_PER_SEC"
echo "拒绝连接:$REJECTED_CONNECTIONS"

# 内存告警(> 80%)
MAX_MEMORY=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT CONFIG GET maxmemory | tail -1)
if [ -n "$MAX_MEMORY" ] && [ "$MAX_MEMORY" != "0" ]; then
    USAGE_PERCENT=$((USED_MEMORY * 100 / MAX_MEMORY))
    if [ $USAGE_PERCENT -gt 80 ]; then
        echo "⚠️  内存使用率超过 80%: ${USAGE_PERCENT}%"
    fi
fi

# 连接数告警
if [ $CONNECTED_CLIENTS -gt 1000 ]; then
    echo "⚠️  连接数过多:$CONNECTED_CLIENTS"
fi

# 阻塞连接告警
if [ $BLOCKED_CLIENTS -gt 100 ]; then
    echo "⚠️  阻塞连接过多:$BLOCKED_CLIENTS"
fi

3.2 慢查询日志

# 慢查询配置

# 记录超过 10ms 的命令
slowlog-log-slower-than 10000

# 最多保留 128 条
slowlog-max-len 128

# 查看慢查询
redis-cli SLOWLOG GET 10

# 清空慢查询
redis-cli SLOWLOG RESET

# 慢查询数量
redis-cli SLOWLOG LEN

3.3 定期巡检

每日检查

# 1. 检查服务状态
redis-cli ping

# 2. 检查内存使用
redis-cli INFO memory | grep used_memory_human

# 3. 检查连接数
redis-cli INFO clients | grep connected_clients

# 4. 检查持久化状态
redis-cli INFO persistence | grep -E "rdb_last_bgsave_status|aof_enabled"

# 5. 检查复制状态
redis-cli INFO replication

每周检查

# 1. 扫描大 Key
redis-cli --bigkeys

# 2. 分析内存
redis-cli MEMORY STATS

# 3. 检查慢查询
redis-cli SLOWLOG GET 128

# 4. 检查客户端连接
redis-cli CLIENT LIST

每月检查

# 1. 性能基准测试
redis-benchmark -q -t set,get -n 100000

# 2. 备份恢复测试
# - 备份数据
# - 恢复验证

# 3. 故障演练
# - 主从切换
# - 哨兵故障转移

四、安全最佳实践

4.1 访问控制

# 设置密码
requirepass YourStrongPassword123!

# 禁用危险命令
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command KEYS ""
rename-command DEBUG ""
rename-command SHUTDOWN ""

# 绑定内网 IP
bind 192.168.1.100

# 修改默认端口
port 6380

4.2 网络安全

# 防火墙配置
# 只允许应用服务器访问
iptables -A INPUT -p tcp -s 192.168.1.0/24 --dport 6379 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -j DROP

# 或使用安全组(云服务)

4.3 数据加密

# Redis 6.0+ TLS 配置
tls-port 6379
port 0  # 禁用非 TLS 端口

tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt

# 客户端连接
redis-cli --tls -h redis.example.com -p 6379 \
  --cert /etc/redis/tls/redis.crt \
  --key /etc/redis/tls/redis.key \
  --cacert /etc/redis/tls/ca.crt

五、故障处理最佳实践

5.1 常见问题处理

问题 1:内存溢出

# 症状:Redis 拒绝写入,返回 OOM 错误

# 解决步骤:
# 1. 检查内存使用
redis-cli INFO memory

# 2. 查看内存淘汰策略
redis-cli CONFIG GET maxmemory-policy

# 3. 临时增加内存
redis-cli CONFIG SET maxmemory 16gb

# 4. 分析大 Key
redis-cli --bigkeys

# 5. 清理数据
redis-cli --scan --pattern "cache:*" | xargs redis-cli unlink

问题 2:CPU 过高

# 症状:CPU 使用率持续 100%

# 解决步骤:
# 1. 查看慢查询
redis-cli SLOWLOG GET 10

# 2. 检查是否有 KEYS 等危险命令
redis-cli MONITOR | grep -E "KEYS|SMEMBERS|HGETALL"

# 3. 查看客户端连接
redis-cli CLIENT LIST

# 4. 终止问题客户端
redis-cli CLIENT KILL ADDR 192.168.1.100:12345

问题 3:连接数过多

# 症状:连接数接近上限,新连接被拒绝

# 解决步骤:
# 1. 查看连接数
redis-cli INFO clients

# 2. 查看客户端列表
redis-cli CLIENT LIST

# 3. 检查应用连接池配置
# - 是否未关闭连接
# - 连接池大小是否合理

# 4. 临时增加最大连接数
redis-cli CONFIG SET maxclients 20000

问题 4:主从同步失败

# 症状:从节点无法同步主节点

# 解决步骤:
# 1. 检查主从状态
redis-cli INFO replication

# 2. 检查网络连通性
telnet <master-ip> 6379

# 3. 检查主节点是否可写
redis-cli -h <master-ip> SET __test__ 1

# 4. 重新配置主从
redis-cli -h <slave-ip> SLAVEOF <master-ip> 6379

# 5. 检查磁盘空间
df -h

5.2 故障转移演练

哨兵故障转移

# 1. 模拟主节点故障
redis-cli -h <master-ip> DEBUG sleep 1000

# 2. 观察哨兵日志
tail -f /var/log/redis/sentinel.log

# 3. 检查新主节点
redis-cli -h <slave-ip> INFO replication

# 4. 恢复原主节点
# 原主节点会自动变为从节点

# 5. 验证数据完整性
redis-cli keys "*" | wc -l

六、总结

6.1 核心要点

  1. 架构设计

    • 根据业务规模选择合适架构
    • 预留足够的内存和连接数
    • 设计合理的 Key 命名规范
  2. 性能优化

    • 使用 Pipeline 批量操作
    • 避免使用 KEYS 等危险命令
    • 合理配置连接池
  3. 运维监控

    • 监控关键指标(内存、连接、QPS)
    • 定期巡检(大 Key、慢查询)
    • 建立告警机制
  4. 安全保障

    • 设置强密码
    • 禁用危险命令
    • 使用内网访问
  5. 故障处理

    • 建立标准处理流程
    • 定期故障演练
    • 完善备份恢复机制

6.2 检查清单

上线前检查

日常运维检查


参考资料


分享这篇文章到:

上一篇文章
RocketMQ 监控体系与可观测性实战
下一篇文章
Redis 内存管理与优化