“Redis 用不对,轻则性能下降,重则引发数据不一致。“
前言
Redis 在高并发业务中常承担热点数据缓存、分布式锁、会话存储、实时排行榜等职责。用错了,轻则响应变慢,重则出现数据不一致甚至生产事故。以下规范主要基于通用工程经验整理。
1. 键设计规范
1.1 命名规范
# 格式:业务:实体:唯一标识:属性
# 优点:语义清晰,通过 SCAN 扫描时方便过滤
# ✅ 推荐命名
payment:lock:order-12345
payment:session:user-1000:token-abc
cache:user:profile:1000
cache:product:detail:SKU-8888
rate:limit:api:/v1/payment:client-hsbc
dau:2026-03-21
sign:user-1000:2026-03
# ❌ 避免命名
mykey # 无意义
user1000info # 难以分割
UserProfile_1000 # 用了下划线,不一致
data # 过于通用,易冲突
1.2 键过期策略
// 1. 所有缓存类 key 必须设置 TTL
redis.opsForValue().set("cache:user:1000", userJson,
Duration.ofHours(1)); // 必须指定过期时间
// 2. 生产者-消费者模式:消息队列 key TTL = 处理时间 × 重试次数 + buffer
redis.opsForStream().add(StreamRecords.newRecord()
.in("task:queue")
.ofMap(payload));
// 同时设置 key 的 maxmemory-policy 为 LRU/LFU
// 3. 会话 key:TTL = 业务超时时间 × 1.5(网络延迟 buffer)
// 银行会话:30 分钟超时 → key TTL = 45 分钟
// 4. 分布式锁:TTL = 业务最大处理时间 + 30 秒
RLock lock = redisson.getLock("payment:lock:" + orderId);
lock.tryLock(0, 45, TimeUnit.SECONDS); // TTL = 45 秒
1.3 键大小控制
# ✅ 单个 key 的 value 大小建议 < 10MB
# ✅ 单个 key 包含的成员数:
# String: < 10MB
# Hash: 字段数 < 10000
# Set/List/ZSet: 成员数 < 50000
# ❌ 避免的 Big Key:
GET cache:file:large-document # 返回 50MB → 网络超时
SMEMBERS cache:tags:all # 返回百万成员 → 主线程阻塞
LRANGE notification:queue 0 -1 # 返回全量 → O(N) 阻塞
// Big Key 检测(SCAN + TYPE)
public void detectBigKeys() {
ScanOptions options = ScanOptions.scanOptions()
.match("*")
.count(1000)
.build();
try (Cursor<String> cursor = redis.scan(options)) {
cursor.forEachRemaining(key -> {
String type = redis.type(key).toString();
long size = estimateSize(key, type);
if (size > 10 * 1024 * 1024) { // > 10MB
log.warn("Big Key detected: {} (type={}, size={}MB)",
key, type, size / 1024 / 1024);
}
});
}
}
private long estimateSize(String key, String type) {
return switch (type) {
case "string" -> {
String val = redis.opsForValue().get(key);
yield val != null ? val.getBytes().length : 0;
}
case "hash" -> {
Long size = redis.opsForHash().size(key);
yield size != null ? size : 0;
}
case "list" -> {
Long size = redis.opsForList().size(key);
yield size != null ? size : 0;
}
default -> 0;
};
}
2. 命令使用规范
# ❌ 禁止在生产环境使用的命令
KEYS * # O(N) 全表扫描,阻塞 Redis
FLUSHDB / FLUSHALL # 删除所有数据(测试环境也慎用)
CONFIG SET/GET # 运行时修改配置(危险)
DEBUG SEGFAULT # 触发崩溃(绝对禁止)
SLOWLEN 0 # 记录所有命令(影响性能)
# ⚠️ 谨慎使用的命令(确认数据量小)
SMEMBERS set # 返回全部成员
LRANGE list 0 -1 # 返回全部元素
SORT set # 排序(非 O(log N))
KEYS user:* # 匹配扫描(用 SCAN 替代)
# ✅ 推荐替代方案
SCAN cursor MATCH user:* COUNT 1000 # 渐进式遍历
SSCAN myset 0 COUNT 100 # Set 遍历
HSCAN myhash 0 COUNT 100 # Hash 遍历
L_RANGE list 0 99 # 分页获取
3. 内存管理规范
# 1. 设置 maxmemory(物理内存的 70-80%)
CONFIG SET maxmemory 8gb
# 2. 选择合适的淘汰策略
# noeviction(默认):不淘汰,返回错误 → 适合写入不多的场景
# allkeys-lru:所有 key 中淘汰最近最少使用 → 适合缓存场景 ✅
# allkeys-lfu:所有 key 中淘汰最不常用 → 适合热点数据 ✅
# volatile-lru:在设置了过期时间的 key 中淘汰 LRU → 适合混合场景
# allkeys-random:随机淘汰 → 不推荐
CONFIG SET maxmemory-policy allkeys-lru
# 3. 开启内存碎片整理
CONFIG SET activedefrag yes
CONFIG SET active-defrag-ignore-bybytes 100mb
CONFIG SET active-defrag-threshold-lower 10
// 内存水位监控
public boolean isMemoryPressure() {
String info = redis.getConnectionFactory().getConnection().info("memory");
Map<String, String> memory = parseInfo(info);
long used = parseLong(memory.get("used_memory"));
long maxmemory = parseLong(memory.get("maxmemory"));
double ratio = (double) used / maxmemory;
if (ratio > 0.85) {
log.error("Redis 内存使用率过高: {}%", String.format("%.1f", ratio * 100));
return true;
}
return false;
}
4. 连接池规范
# Spring Boot 配置示例(生产环境)
spring:
redis:
host: redis.prod.internal
port: 6379
password: ${REDIS_PASSWORD}
database: 0
timeout: 2000ms
lettuce:
pool:
enabled: true
max-active: 200 # 最大连接数(按并发量调整)
max-idle: 20 # 最大空闲连接
min-idle: 10 # 最小空闲连接(预热)
max-wait: 1000ms # 获取连接超时
# 连接池监控
metrics:
enabled: true
// 连接池使用规范
// 1. 使用完必须释放连接(Spring Data Redis 自动归还)
// 不用手动释放,但不要在方法内持有连接引用
// 2. 避免在单次操作中频繁创建连接
// ❌ 错误:循环内每次创建连接
for (String userId : userIds) {
redis.opsForValue().get("user:" + userId); // N 次网络往返
}
// ✅ 正确:使用 Pipeline 批量操作
List<Object> results = redis.executePipelined((RedisCallback<Object>) conn -> {
for (String userId : userIds) {
conn.stringCommands().get(("user:" + userId).getBytes());
}
return null;
});
// 3. 合理设置超时
redisTemplate.getConnectionFactory().getConnection()
.setTimeout(TimeUnit.SECONDS.toMillis(2));
5. 集群与高可用规范
# 1. 生产环境必须使用 Sentinel 或 Cluster
# 单节点 Redis 只用于开发/测试
# 2. Sentinel 最小配置:3 个节点(多数投票)
# sentinel monitor mymaster 127.0.0.1 6379 2
# 3. Cluster 最小配置:6 个节点(3 主 3 从)
redis-cli --cluster create 10.0.0.1:6379 10.0.0.2:6379 \
10.0.0.3:6379 10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
--cluster-replicas 1
# 4. 读写分离配置
# 读操作:优先读从节点(降低主节点压力)
# 写操作:必须写主节点
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// 配置读写分离
return new LettuceConnectionFactory();
}
}
6. 安全规范(银行必读)
# 1. 防火墙:只允许应用服务器访问 Redis 端口
# iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 6379 -j ACCEPT
# iptables -A INPUT -p tcp --dport 6379 -j DROP
# 2. 强密码(Redis 6.0+ ACL)
# requirepass <强密码>(≥32 位,包含特殊字符)
CONFIG SET requirepass "你的强密码"
# 3. 禁止公网访问
# bind 127.0.0.1 10.0.0.0/8 # 只绑定内网 IP
# 4. 禁止危险命令重命名
CONFIG SET rename-command FLUSHDB ""
CONFIG SET rename-command FLUSHALL ""
CONFIG SET rename-command KEYS ""
# 5. 使用 SSL/TLS(Redis 6.0+)
# tls-port 6379
# port 0
# requirepass <密码>
# 6. 审计日志(银行合规要求)
# 记录所有写操作到单独日志文件
# appendonly yes
# appendfsync everysec
// 应用层安全:敏感数据加密存储
@Service
public class SecureCacheService {
private final RedisTemplate<String, byte[]> redis;
// 敏感数据(如账号余额)存入前加密
public void cacheSecureData(String key, Object data) {
String json = JSON.toJSONString(data);
byte[] encrypted = encrypt(json); // AES-256 加密
redis.opsForValue().set(key, encrypted,
Duration.ofHours(1));
}
// 读取后解密
public Object getSecureData(String key) {
byte[] encrypted = redis.opsForValue().get(key);
if (encrypted == null) return null;
String json = decrypt(encrypted);
return JSON.parse(json);
}
}
7. 监控与告警规范
// Redis 监控指标(使用 Micrometer 上报 Prometheus)
@Component
@Slf4j
public class RedisMetricsCollector {
private final RedisTemplate<String, Object> redis;
private final MeterRegistry meterRegistry;
@Scheduled(fixedRate = 30000) // 每 30 秒采集
public void collectMetrics() {
Properties info = redis.getConnectionFactory()
.getConnection().serverCommands().info();
// 内存使用率
double memUsed = Double.parseDouble(
info.getProperty("used_memory"));
double memMax = Double.parseDouble(
info.getProperty("maxmemory"));
meterRegistry.gauge("redis.memory.used.ratio",
memUsed / memMax);
// 连接数
long connected = Long.parseLong(
info.getProperty("connected_clients"));
meterRegistry.gauge("redis.clients.connected",
connected);
// QPS
long cmdCount = Long.parseLong(
info.getProperty("total_commands_processed"));
meterRegistry.gauge("redis.qps",
cmdCount);
// 命中率
double hits = Double.parseDouble(
info.getProperty("keyspace_hits"));
double misses = Double.parseLong(
info.getProperty("keyspace_misses"));
double hitRate = (hits / (hits + misses)) * 100;
log.info("Redis 命中率: {}%", String.format("%.2f", hitRate));
}
}
8. 规范速查表
| 类别 | 规范 | 级别 |
|---|---|---|
| 键设计 | 使用冒号分隔的语义化命名 | 必须 |
| 键设计 | 所有缓存 key 必须设置 TTL | 必须 |
| 键设计 | 单个 value < 10MB | 必须 |
| 命令 | 禁止使用 KEYS/FLUSHDB/FLUSHALL | 必须 |
| 命令 | 大数据量用 SCAN 替代 KEYS | 必须 |
| 内存 | 设置 maxmemory 和淘汰策略 | 必须 |
| 连接 | 使用连接池(max-active 200) | 必须 |
| 连接 | 批量操作使用 Pipeline | 推荐 |
| 安全 | 设置强密码 + ACL | 必须 |
| 安全 | 禁止公网访问 + bind 内网 | 必须 |
| 监控 | 内存使用率 > 85% 告警 | 必须 |
| 监控 | QPS 异常下跌告警 | 必须 |
| 高可用 | 生产环境必须使用 Sentinel/Cluster | 必须 |
相关阅读:[Redis Scan 命令用法](/coding/Redis/Redis Scan 命令用法) · [Redis 实现分布式锁](/coding/Redis/Redis 实现分布式锁) · [Redis 配置文件解析](/coding/Redis/Redis 配置文件解析)