你是否曾经担忧过:Redis 在保存 RDB 文件的时候会不会导致服务卡顿?负责核心业务的 Redis 突然变慢,用户投诉接踵而至,开发团队紧急排查,最终发现是 RDB 持久化进行时。这种场景让许多开发者对 Redis 持久化心存疑虑。但 Redis 真的会在生成 RDB 时拒绝服务请求吗?
Redis RDB 持久化的基本原理
RDB(Redis Database Backup)是 Redis 提供的一种持久化方案,它通过创建数据库的时间点快照(snapshot)来保存数据。与 AOF(Append Only File)持久化记录每条写命令不同,RDB 是将内存中的完整数据集保存到磁盘文件中。
Redis 提供了两个命令来创建 RDB 文件:
SAVE:在主线程中执行,会阻塞所有其他操作BGSAVE:创建子进程来执行 RDB 保存,不阻塞主线程
在实际使用中,BGSAVE几乎是唯一的选择,无论是通过配置文件的自动触发(本质是BGSAVE),还是主从复制时的 RDB 生成,均使用后台子进程方式。而手动执行SAVE命令仍会阻塞主线程(不推荐在生产环境使用)。
关键问题:RDB 生成如何与请求处理并存?
Redis 在生成 RDB 文件时如何继续处理客户端请求?这主要依赖于两项关键技术:进程 fork() 和 写时复制(Copy-On-Write,简称 COW)。
步骤 1:进程 fork()
当执行BGSAVE命令时,Redis 服务器会调用操作系统的fork()系统调用来创建一个子进程。这个过程的关键特点是:
- 子进程是父进程的完整复制,包括内存数据
- 子进程与父进程共享物理内存页,直到其中一个进程尝试修改内容
- fork()操作本身很快,但内存页表复制会占用一定时间
在 fork()操作期间,Redis 父进程会短暂阻塞,这个时间与内存页表大小相关(而非内存使用量绝对值)。在大多数系统上,对于几 GB 内存的 Redis 实例,这一阻塞通常在毫秒级别。
从内核层面看,Linux 的 fork()调用会为子进程创建虚拟内存页表的副本,但物理内存页面仍由两个进程共享。某些系统提供 vfork()优化,可减少内存复制开销,但 Redis 使用的是标准 fork(),依赖内核的 COW 机制。
步骤 2:写时复制(Copy-On-Write)
fork()完成后,父进程(Redis 主进程)会继续处理客户端请求,而子进程开始创建 RDB 文件。这时关键的机制是写时复制(COW):
- 初始状态下,父子进程共享所有内存页
- 注意:子进程只读取内存数据而不修改,因此不会触发 COW
- 当父进程需要修改某个内存页的内容时,操作系统会创建该页的副本
- 父进程在副本上进行修改,子进程继续使用原始页
上图展示了写时复制的原理:当父进程需要修改内存页 3 时,操作系统创建了一个副本,父进程修改副本,而子进程继续使用原始页。
从内核层面看,当 fork()创建子进程时,父子进程共享的页面会被标记为只读(通过mprotect系统调用)。当父进程尝试写入时,会触发页错误(page fault),内核随后会创建该页面的拷贝,并将父进程的页表指向这个新的可写页面。
这就解释了为什么在极端情况下(如果所有共享内存页都被修改),系统内存使用量可能会临时翻倍:原始内存页(子进程使用)+ 副本内存页(父进程修改)= 2 倍内存使用。如果 Redis 使用了 10GB 内存,理论上在 RDB 生成期间,物理内存使用量可能增加到 20GB。
RDB 生成过程中请求处理的影响
虽然理论上 RDB 生成不会阻塞 Redis 主进程,但在实际环境中,它确实会对性能产生一定影响:
1. fork()过程中的短暂阻塞
如前所述,fork()系统调用本身会导致 Redis 父进程短暂阻塞。Redis 日志中的"Background saving started"消息出现前,所有请求都会被阻塞。
27734:M 15 Aug 2023 10:15:32.729 * Background saving started by pid 27735
监控这一阻塞的方法是查看 Redis INFO 命令输出中的latest_fork_usec指标,它表示最近一次 fork()操作的耗时(微秒):
# Server
...
latest_fork_usec:985
...
2. 内存使用增加导致的系统压力
由于写时复制机制,当父进程修改共享数据时,系统需要复制内存页。这意味着:
- 写操作越多,内存使用增长越快
- 数据修改越分散(涉及更多内存页),写时复制导致的内存增长越显著
在极端情况下,如果在 RDB 生成期间有大量写入,Redis 的内存使用可能会临时翻倍(原始内存+所有页面的副本),可能触发系统的内存交换(swap),严重影响性能。
关键监控指标:
used_memory:Redis 逻辑内存(不反映写时复制的影响)used_memory_rss:实际物理内存使用(会增加,反映写时复制的影响)used_memory_peak:Redis 峰值内存使用
3. CPU 和 IO 竞争
子进程在生成 RDB 文件时会消耗:
- CPU 资源:遍历和组织数据
- IO 资源:将数据写入磁盘
这些资源消耗可能与父进程争抢系统资源,间接影响请求处理性能。
实际案例:测量 RDB 生成对请求延迟的影响
下面,我们通过一个 Java 程序,测量 RDB 保存过程中 Redis 请求延迟的变化,同时对比读写操作的差异:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.ThreadLocalRandom;
public class RdbSaveLatencyTest {
private static final Logger logger = LoggerFactory.getLogger(RdbSaveLatencyTest.class);
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int THREAD_COUNT = 10;
private static final int TEST_DURATION_SECONDS = 60;
private static final int KEY_SPACE = 1000000; // 使用更大的键空间避免热点
// 使用连接池而非单个连接
private static final JedisPool jedisPool;
static {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(THREAD_COUNT * 2);
poolConfig.setMaxIdle(THREAD_COUNT);
poolConfig.setMinIdle(1);
poolConfig.setTestOnBorrow(true);
// 增加连接池健康检查,定期清理无效连接
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
}
public static void main(String[] args) throws Exception {
// 预热Redis(生成测试数据)
warmupRedis(100000);
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
// 延迟统计 - 分离读写操作统计
final AtomicLong totalWriteRequests = new AtomicLong(0);
final AtomicLong slowWriteRequests = new AtomicLong(0); // >5ms视为慢请求
final AtomicLong totalReadRequests = new AtomicLong(0);
final AtomicLong slowReadRequests = new AtomicLong(0);
final List<Long> writeLatencies = new ArrayList<>();
final List<Long> readLatencies = new ArrayList<>();
// 启动工作线程
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
executorService.submit(() -> {
try {
// 使用线程本地随机数生成器,避免争用
ThreadLocalRandom random = ThreadLocalRandom.current();
while (!Thread.interrupted()) {
try (Jedis jedis = jedisPool.getResource()) {
// 交替执行读和写操作
if (threadId % 2 == 0) {
// 写操作 - 使用UUID的部分作为前缀避免热点
String prefix = UUID.randomUUID().toString().substring(0, 4);
String key = prefix + ":key:" + random.nextInt(KEY_SPACE);
String value = "value:" + System.nanoTime();
long startTime = System.nanoTime();
jedis.set(key, value);
long endTime = System.nanoTime();
long latencyMs = (endTime - startTime) / 1_000_000;
synchronized (writeLatencies) {
writeLatencies.add(latencyMs);
}
totalWriteRequests.incrementAndGet();
if (latencyMs > 5) {
slowWriteRequests.incrementAndGet();
}
} else {
// 读操作
String key = "key:" + random.nextInt(KEY_SPACE);
long startTime = System.nanoTime();
jedis.get(key);
long endTime = System.nanoTime();
long latencyMs = (endTime - startTime) / 1_000_000;
synchronized (readLatencies) {
readLatencies.add(latencyMs);
}
totalReadRequests.incrementAndGet();
if (latencyMs > 5) {
slowReadRequests.incrementAndGet();
}
}
// 动态调整睡眠时间,保持稳定的压力
long sleepTime = calculateAdaptiveSleepTime(threadId % 2 == 0);
Thread.sleep(sleepTime);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("线程" + threadId + "异常", e);
}
});
}
// 监控线程
Thread monitorThread = new Thread(() -> {
try {
long startTime = System.currentTimeMillis();
long lastReport = 0;
long lastWriteTotal = 0;
long lastReadTotal = 0;
// 监控JVM状态,排除GC对测试结果的干扰
long lastGcCount = getGcCount();
while ((System.currentTimeMillis() - startTime) < TEST_DURATION_SECONDS * 1000) {
long now = System.currentTimeMillis();
long currentWriteTotal = totalWriteRequests.get();
long currentReadTotal = totalReadRequests.get();
// 每秒报告一次
if (now - lastReport >= 1000) {
long writeQps = currentWriteTotal - lastWriteTotal;
long readQps = currentReadTotal - lastReadTotal;
double writeSlowPercentage = (slowWriteRequests.get() * 100.0) / Math.max(1, currentWriteTotal);
double readSlowPercentage = (slowReadRequests.get() * 100.0) / Math.max(1, currentReadTotal);
logger.info("写QPS: {} (慢:{}%), 读QPS: {} (慢:{}%)",
writeQps, String.format("%.2f", writeSlowPercentage),
readQps, String.format("%.2f", readSlowPercentage));
// 检查GC情况
long currentGcCount = getGcCount();
if (currentGcCount > lastGcCount) {
logger.info("检测到GC发生 {} 次,可能影响延迟测试结果", (currentGcCount - lastGcCount));
lastGcCount = currentGcCount;
}
lastReport = now;
lastWriteTotal = currentWriteTotal;
lastReadTotal = currentReadTotal;
// 获取Redis内存和RDB状态
try (Jedis jedis = jedisPool.getResource()) {
String info = jedis.info();
String rdbInProgress = extractInfo(info, "rdb_bgsave_in_progress");
String usedMemory = extractInfo(info, "used_memory_human");
String usedMemoryRss = extractInfo(info, "used_memory_rss_human");
// 计算内存比例
double logicalMemory = parseMemoryToMB(extractInfo(info, "used_memory"));
double physicalMemory = parseMemoryToMB(extractInfo(info, "used_memory_rss"));
double memoryRatio = (logicalMemory > 0) ? physicalMemory / logicalMemory : 0;
logger.info("内存: {} (逻辑), {} (物理RSS), 比例: {}",
usedMemory, usedMemoryRss, String.format("%.2f", memoryRatio));
if ("1".equals(rdbInProgress)) {
logger.info(" *** RDB保存正在进行 ***");
}
} catch (Exception e) {
logger.error("监控异常", e);
}
}
Thread.sleep(100);
}
} catch (Exception e) {
logger.error("监控线程异常", e);
}
});
// 触发BGSAVE的线程
Thread bgsaveThread = new Thread(() -> {
try {
// 等待10秒后执行BGSAVE
Thread.sleep(10000);
try (Jedis jedis = jedisPool.getResource()) {
logger.info("*** 执行BGSAVE开始 ***");
String result = jedis.bgsave();
logger.info("BGSAVE结果: {}", result);
// 监控RDB生成状态
boolean rdbInProgress = true;
while (rdbInProgress) {
Thread.sleep(1000);
String info = jedis.info();
rdbInProgress = "1".equals(extractInfo(info, "rdb_bgsave_in_progress"));
if (rdbInProgress) {
logger.info("RDB生成中...");
}
}
logger.info("*** RDB生成完成 ***");
}
} catch (Exception e) {
logger.error("BGSAVE线程异常", e);
}
});
// 启动监控和BGSAVE线程
monitorThread.start();
bgsaveThread.start();
// 等待测试完成
monitorThread.join();
// 停止所有工作线程
executorService.shutdownNow();
executorService.awaitTermination(5, TimeUnit.SECONDS);
// 计算写操作统计信息
logger.info("\n===== 写操作统计 =====");
reportLatencyStats(writeLatencies, totalWriteRequests.get(), slowWriteRequests.get());
// 计算读操作统计信息
logger.info("\n===== 读操作统计 =====");
reportLatencyStats(readLatencies, totalReadRequests.get(), slowReadRequests.get());
// 关闭连接池
jedisPool.close();
// 建议使用redis-benchmark进行进一步对比测试
logger.info("\n建议使用redis-benchmark进行标准化测试,例如:");
logger.info("redis-benchmark -t set,get -n 100000 -c 50 --csv");
logger.info("redis-benchmark -t set,get -n 100000 -c 50 --load-ramp-up 5 -r 1000000");
logger.info("同时使用 redis-cli monitor 观察请求处理情况");
}
// 获取JVM的GC次数,用于监控GC对测试的影响
private static long getGcCount() {
long count = 0;
for (java.lang.management.GarbageCollectorMXBean gc :
java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) {
count += gc.getCollectionCount();
}
return count;
}
private static void reportLatencyStats(List<Long> latencies, long totalRequests, long slowRequests) {
synchronized (latencies) {
if (latencies.isEmpty()) {
logger.info("无数据");
return;
}
long sum = 0;
long max = 0;
long min = Long.MAX_VALUE;
for (long latency : latencies) {
sum += latency;
max = Math.max(max, latency);
min = Math.min(min, latency);
}
double avg = (double) sum / latencies.size();
logger.info("总请求数: {}", totalRequests);
logger.info("延迟 - 最小: {} ms, 最大: {} ms, 平均: {} ms",
min, max, String.format("%.2f", avg));
logger.info("慢请求比例: {}%", String.format("%.2f", (slowRequests * 100.0) / totalRequests));
}
}
private static String extractInfo(String info, String key) {
if (info == null || key == null) {
logger.warn("INFO输出或键为null");
return "N/A";
}
String pattern = key + ":";
for (String line : info.split("\n")) {
if (line.startsWith(pattern)) {
return line.substring(pattern.length()).trim();
}
}
logger.debug("未在INFO输出中找到键: {}", key);
return "N/A"; // 返回默认值,避免返回null造成异常
}
private static long calculateAdaptiveSleepTime(boolean isWrite) {
// 写操作间隔略长,减少写压力
return isWrite ? 15 : 5;
}
private static void warmupRedis(int keyCount) {
logger.info("预热Redis...");
try (Jedis jedis = jedisPool.getResource()) {
// 批量写入测试数据
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < keyCount; i++) {
pipeline.set("key:" + i, "value:" + i);
// 每1000条执行一次,避免客户端内存压力
if (i > 0 && i % 1000 == 0) {
pipeline.sync();
if (i % 10000 == 0) {
logger.info("已预热 {} 个键", i);
}
pipeline = jedis.pipelined();
}
}
// 处理剩余命令
pipeline.sync();
} catch (Exception e) {
logger.error("预热异常", e);
}
logger.info("预热完成");
}
private static double parseMemoryToMB(String memoryStr) {
try {
if (memoryStr == null || memoryStr.equals("N/A")) {
return 0;
}
// 尝试作为纯数字解析(单位为字节)
try {
return Double.parseDouble(memoryStr) / (1024 * 1024);
} catch (NumberFormatException e) {
// 如果包含单位,则提取数字和单位
String number = memoryStr.replaceAll("[^\\d.]", "");
String unit = memoryStr.replaceAll("[^a-zA-Z]", "").toUpperCase();
try {
double value = Double.parseDouble(number);
switch (unit) {
case "B": return value / (1024 * 1024);
case "KB": return value / 1024;
case "MB": return value;
case "GB": return value * 1024;
case "TB": return value * 1024 * 1024;
default: return value / (1024 * 1024); // 默认假设为字节
}
} catch (NumberFormatException ex) {
logger.error("无法解析内存值数字部分: {}", number);
return 0;
}
}
} catch (Exception e) {
logger.error("内存解析错误", e);
return 0;
}
}
}
运行上述代码,可以清晰地观察 BGSAVE 过程中 Redis 请求延迟的变化情况。典型的观察结果会显示:
- BGSAVE 开始的瞬间,由于 fork()操作,会出现一个短暂的延迟峰值
- RDB 生成过程中,写操作延迟增长比读操作更明显
- 高写入负载下,延迟增长会更明显,主要是写时复制机制导致的内存压力
RDB 与 AOF 的请求处理对比
当讨论 Redis 持久化对请求处理的影响时,值得对比 RDB 和 AOF 两种机制的不同:
有趣的是,AOF 重写(BGREWRITEAOF)使用与 RDB 生成相同的 fork+写时复制机制,因此对请求处理的影响模式相似。但 AOF 的日常追加写入会触发更多的磁盘 I/O,可能导致短暂的延迟峰值。
Redis 4.0+的混合持久化方案在 RDB 文件后追加 AOF 日志,既减少了 RDB 的全量快照频率,也降低了 AOF 的日志体积。对请求处理的影响介于纯 RDB 和纯 AOF 之间,是生产环境常用的折中方案。
如何优化 RDB 生成过程中的请求处理
既然我们了解了 RDB 生成时请求处理的机制和潜在影响,下面介绍一些优化技巧:
1. 合理配置 RDB 保存频率
Redis 配置文件中的save指令控制了自动触发 BGSAVE 的条件:
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
在生产环境中,应根据数据重要性和变更频率,合理设置这些参数,避免过于频繁的 RDB 生成。
2. 内存优化以减少 fork()耗时
fork()操作的耗时与 Redis 实例的内存页表大小直接相关。优化内存使用可以显著减少 fork()阻塞:
- 使用
maxmemory配置限制 Redis 最大内存使用 - 选择适当的键过期策略(
maxmemory-policy) - 优化数据结构(如使用整数集合、压缩列表等)
maxmemory 4gb
maxmemory-policy allkeys-lru
3. 系统层面的优化
操作系统级别的优化也能显著改善 RDB 生成性能:
- 使用 Linux 的
vm.overcommit_memory=1配置,避免 fork()失败 - 关闭透明大页(Transparent Huge Pages)功能,减少内存碎片
- 使用高性能存储设备降低 I/O 等待时间
- 确保系统有足够的空闲内存,避免 swap 交换
在/etc/sysctl.conf中添加:
vm.overcommit_memory=1
关闭透明大页:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
4. 使用监控工具及时发现问题
监控 RDB 生成过程的关键指标:
latest_fork_usec:最近一次 fork 操作的耗时(微秒)rdb_last_bgsave_time_sec:最近一次 BGSAVE 操作耗时(秒)rdb_last_bgsave_status:最近一次 BGSAVE 状态used_memory:Redis 逻辑内存使用量used_memory_rss:实际物理内存使用量(会反映写时复制影响)used_memory_peak:Redis 峰值内存使用量
下面是一个监控这些指标的 Java 工具,在生产环境中,建议将监控间隔设置为 1 秒,以便实时捕捉 fork 阻塞和内存变化:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.ConsoleHandler;
import java.util.logging.SimpleFormatter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import com.sun.management.OperatingSystemMXBean;
public class RedisBgsaveMonitor {
private static final Logger logger = Logger.getLogger(RedisBgsaveMonitor.class.getName());
private static final JedisPool jedisPool;
static {
// 配置日志
configureLogger();
// 配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(5);
poolConfig.setMaxIdle(2);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
jedisPool = new JedisPool(poolConfig, "localhost", 6379);
}
private static void configureLogger() {
ConsoleHandler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
logger.addHandler(handler);
logger.setLevel(Level.INFO);
}
public static void main(String[] args) {
// 设置监控间隔(秒)
int monitorInterval = 1; // 生产环境建议1秒,捕捉瞬时阻塞
if (args.length > 0) {
try {
monitorInterval = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
logger.warning("无效的监控间隔,使用默认值1秒");
}
}
try {
logger.info("开始Redis RDB监控 (间隔: " + monitorInterval + "秒)");
double previousRssMemory = 0;
while (true) {
try (Jedis jedis = jedisPool.getResource()) {
String info = jedis.info();
// 解析关键指标
String forkTime = extractInfo(info, "latest_fork_usec");
String rdbInProgress = extractInfo(info, "rdb_bgsave_in_progress");
String lastSaveTime = extractInfo(info, "rdb_last_bgsave_time_sec");
String lastSaveStatus = extractInfo(info, "rdb_last_bgsave_status");
String usedMemory = extractInfo(info, "used_memory_human");
String usedMemoryRss = extractInfo(info, "used_memory_rss_human");
// 获取数值型指标,用于计算变化
double rssMemory = parseMemoryToMB(extractInfo(info, "used_memory_rss"));
double logicalMemory = parseMemoryToMB(extractInfo(info, "used_memory"));
double memoryDelta = rssMemory - logicalMemory;
double memoryRatio = logicalMemory > 0 ? rssMemory / logicalMemory : 0;
double rssDelta = rssMemory - previousRssMemory;
previousRssMemory = rssMemory;
logger.info("===== " + getCurrentTimeStamp() + " =====");
logger.info("最近fork耗时: " + forkTime + " 微秒");
logger.info("RDB生成进行中: " + ("1".equals(rdbInProgress) ? "是" : "否"));
logger.info("最近RDB生成耗时: " + lastSaveTime + " 秒");
logger.info("最近RDB生成状态: " + lastSaveStatus);
logger.info("当前内存使用: " + usedMemory + " (逻辑), " + usedMemoryRss + " (物理RSS)");
logger.info(String.format("内存比例: 物理/逻辑 = %.2f (Delta: %.2f MB, RSS变化: %+.2f MB)",
memoryRatio, memoryDelta, rssDelta));
// 检测潜在内存问题
if (memoryRatio > 1.5) {
logger.warning("注意: 物理内存使用量显著高于逻辑内存,可能是写时复制导致!");
}
// 使用JMX获取系统内存信息(跨平台兼容)
checkSystemMemoryWithJmx();
// 备用方法:使用free命令(仅Linux系统)
if (System.getProperty("os.name").toLowerCase().contains("linux")) {
checkSystemMemoryWithFree();
}
logger.info("");
} catch (Exception e) {
logger.severe("监控异常: " + e.getMessage());
// 发生异常时短暂休眠,避免过于频繁的错误日志
TimeUnit.SECONDS.sleep(1);
}
TimeUnit.SECONDS.sleep(monitorInterval);
}
} catch (InterruptedException e) {
logger.info("监控被中断: " + e.getMessage());
Thread.currentThread().interrupt();
} finally {
jedisPool.close();
}
}
private static void checkSystemMemoryWithJmx() {
try {
OperatingSystemMXBean osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
long totalPhysicalMemory = osBean.getTotalPhysicalMemorySize() / (1024 * 1024);
long freePhysicalMemory = osBean.getFreePhysicalMemorySize() / (1024 * 1024);
long usedPhysicalMemory = totalPhysicalMemory - freePhysicalMemory;
double usedPercentage = (double) usedPhysicalMemory / totalPhysicalMemory * 100;
logger.info(String.format("系统内存: 总计 %d MB, 已用 %d MB (%.1f%%), 空闲 %d MB",
totalPhysicalMemory, usedPhysicalMemory, usedPercentage, freePhysicalMemory));
if (usedPercentage > 90) {
logger.warning("系统内存使用率超过90%,可能影响Redis性能!");
}
// 检查swap
long totalSwap = osBean.getTotalSwapSpaceSize() / (1024 * 1024);
long freeSwap = osBean.getFreeSwapSpaceSize() / (1024 * 1024);
long usedSwap = totalSwap - freeSwap;
if (totalSwap > 0 && usedSwap > 0) {
double swapUsedPercentage = (double) usedSwap / totalSwap * 100;
logger.warning(String.format("Swap使用: %d MB (%.1f%%),可能导致性能下降!", usedSwap, swapUsedPercentage));
}
} catch (Exception e) {
logger.fine("使用JMX检查系统内存失败: " + e.getMessage());
}
}
private static void checkSystemMemoryWithFree() {
try {
Process p = Runtime.getRuntime().exec("free -m");
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
// 跳过表头
reader.readLine();
// 解析输出,忽略无效行
while ((line = reader.readLine()) != null) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2) {
if (parts[0].equalsIgnoreCase("Mem:") && parts.length >= 4) {
try {
int total = Integer.parseInt(parts[1]);
int used = Integer.parseInt(parts[2]);
int free = Integer.parseInt(parts[3]);
double usedPercentage = (double) used / total * 100;
logger.info(String.format("系统内存(free): 总计 %d MB, 已用 %d MB (%.1f%%), 空闲 %d MB",
total, used, usedPercentage, free));
} catch (NumberFormatException e) {
logger.fine("无法解析内存值: " + line);
}
} else if (parts[0].equalsIgnoreCase("Swap:") && parts.length >= 4) {
try {
int total = Integer.parseInt(parts[1]);
int used = Integer.parseInt(parts[2]);
if (total > 0 && used > 0) {
double usedPercentage = (double) used / total * 100;
logger.warning(String.format("Swap使用(free): %d MB (%.1f%%)", used, usedPercentage));
}
} catch (NumberFormatException e) {
logger.fine("无法解析Swap值: " + line);
}
}
}
}
reader.close();
} catch (Exception e) {
logger.fine("无法检查系统内存状态: " + e.getMessage());
}
}
private static String extractInfo(String info, String key) {
if (info == null || key == null) {
logger.warning("INFO输出或键为null");
return "N/A";
}
String pattern = key + ":";
for (String line : info.split("\n")) {
if (line.startsWith(pattern)) {
return line.substring(pattern.length()).trim();
}
}
logger.fine("未在INFO输出中找到键: " + key);
return "N/A";
}
private static double parseMemoryToMB(String memoryStr) {
try {
if (memoryStr == null || memoryStr.equals("N/A")) {
return 0;
}
// 尝试作为纯数字解析(单位为字节)
try {
return Double.parseDouble(memoryStr) / (1024 * 1024);
} catch (NumberFormatException e) {
// 如果包含单位,则提取数字和单位
Pattern pattern = Pattern.compile("([\\d.]+)([BKMGT]?)");
Matcher matcher = pattern.matcher(memoryStr);
if (matcher.find()) {
try {
double value = Double.parseDouble(matcher.group(1));
String unit = matcher.group(2).toUpperCase();
switch (unit) {
case "B": return value / (1024 * 1024);
case "K": return value / 1024;
case "M": return value;
case "G": return value * 1024;
case "T": return value * 1024 * 1024;
default: return value / (1024 * 1024); // 默认假设为字节
}
} catch (NumberFormatException ex) {
logger.warning("无法解析内存值: " + matcher.group(1));
return 0;
}
}
logger.warning("无法提取内存数值: " + memoryStr);
return 0;
}
} catch (Exception e) {
logger.log(Level.SEVERE, "内存解析错误", e);
return 0;
}
}
private static String getCurrentTimeStamp() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
}
5. 在负载较低时执行 BGSAVE
如果可能,最好在系统负载较低的时段触发 RDB 持久化操作。可以通过 Cron 任务在指定时间执行 BGSAVE,而不是依赖 Redis 的自动触发条件:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.Properties;
import java.io.FileInputStream;
public class ScheduledBgsave {
private static final Logger logger = Logger.getLogger(ScheduledBgsave.class.getName());
// 从配置文件加载阈值
private static final int CLIENT_THRESHOLD;
private static final int OPS_THRESHOLD;
static {
// 默认值
int clientDefault = 100;
int opsDefault = 1000;
// 尝试从配置加载
try {
Properties props = new Properties();
props.load(new FileInputStream("redis-backup.properties"));
clientDefault = Integer.parseInt(props.getProperty("max.clients", "100"));
opsDefault = Integer.parseInt(props.getProperty("max.ops", "1000"));
} catch (Exception e) {
logger.info("未找到配置文件或解析错误,使用默认阈值");
}
CLIENT_THRESHOLD = clientDefault;
OPS_THRESHOLD = opsDefault;
logger.info("使用负载阈值 - 客户端连接: " + CLIENT_THRESHOLD + ", 每秒操作: " + OPS_THRESHOLD);
}
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(3);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 6379;
try (JedisPool jedisPool = new JedisPool(poolConfig, host, port)) {
try (Jedis jedis = jedisPool.getResource()) {
// 检查是否已有RDB生成进行中
String info = jedis.info();
String rdbInProgress = extractInfo(info, "rdb_bgsave_in_progress");
if ("1".equals(rdbInProgress)) {
logger.warning("已有RDB生成进行中,跳过");
return;
}
// 检查当前负载
String connectedClients = extractInfo(info, "connected_clients");
String instantaneousOpsPerSec = extractInfo(info, "instantaneous_ops_per_sec");
int clients = parseIntSafe(connectedClients);
int ops = parseIntSafe(instantaneousOpsPerSec);
// 记录执行前的内存使用情况
String usedMemoryBefore = extractInfo(info, "used_memory_human");
String usedMemoryRssBefore = extractInfo(info, "used_memory_rss_human");
// 如果负载较低,执行BGSAVE
if (clients < CLIENT_THRESHOLD && ops < OPS_THRESHOLD) {
logger.info("当前负载低,执行BGSAVE");
logger.info("执行前 - 连接客户端: " + clients + ", 每秒操作: " + ops);
logger.info("执行前 - 内存使用: " + usedMemoryBefore + " (逻辑), " +
usedMemoryRssBefore + " (物理)");
long startTime = System.currentTimeMillis();
String result = jedis.bgsave();
logger.info("BGSAVE结果: " + result);
// 等待RDB生成完成,最多等待30分钟
boolean completed = waitForRdbCompletion(jedis, 30 * 60 * 1000);
long duration = System.currentTimeMillis() - startTime;
if (completed) {
logger.info("BGSAVE完成,耗时: " + (duration / 1000) + " 秒");
// 检查完成后的内存情况
info = jedis.info();
String usedMemoryAfter = extractInfo(info, "used_memory_human");
String usedMemoryRssAfter = extractInfo(info, "used_memory_rss_human");
logger.info("执行后 - 内存使用: " + usedMemoryAfter + " (逻辑), " +
usedMemoryRssAfter + " (物理)");
// 发送邮件通知(如果配置了SMTP)
sendNotification("Redis BGSAVE 完成",
"Redis实例 " + host + ":" + port + " BGSAVE备份完成,耗时 " +
(duration / 1000) + " 秒");
} else {
logger.warning("BGSAVE在30分钟内未完成");
sendNotification("Redis BGSAVE 超时",
"Redis实例 " + host + ":" + port + " BGSAVE备份超时,已执行 " +
(duration / 1000) + " 秒");
}
} else {
logger.info("当前负载高,跳过BGSAVE");
logger.info("连接客户端: " + clients + ", 每秒操作: " + ops);
logger.info("阈值: 客户端 " + CLIENT_THRESHOLD + ", 操作数 " + OPS_THRESHOLD);
}
}
} catch (Exception e) {
logger.log(Level.SEVERE, "执行BGSAVE时发生异常", e);
try {
sendNotification("Redis BGSAVE 执行异常",
"Redis实例 " + host + ":" + port + " BGSAVE执行失败:" + e.getMessage());
} catch (Exception ex) {
logger.fine("发送通知失败: " + ex.getMessage());
}
}
}
private static boolean waitForRdbCompletion(Jedis jedis, int timeoutMillis) throws InterruptedException {
long startTime = System.currentTimeMillis();
boolean inProgress = true;
while (inProgress && (System.currentTimeMillis() - startTime < timeoutMillis)) {
String info = jedis.info();
String rdbInProgress = extractInfo(info, "rdb_bgsave_in_progress");
inProgress = "1".equals(rdbInProgress);
if (inProgress) {
// 每10秒检查一次
Thread.sleep(10000);
}
}
return !inProgress;
}
private static String extractInfo(String info, String key) {
String pattern = key + ":";
for (String line : info.split("\n")) {
if (line.startsWith(pattern)) {
return line.substring(pattern.length()).trim();
}
}
return "0";
}
private static int parseIntSafe(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return 0;
}
}
// 简单邮件通知方法(实际实现需添加SMTP配置)
private static void sendNotification(String subject, String message) {
// 实际应用中实现邮件或短信通知
logger.info("通知: " + subject + " - " + message);
}
}
限制与适用场景
虽然 Redis 的 RDB 持久化在大多数情况下表现良好,但在特定场景下可能面临挑战:
- 超大内存实例:当 Redis 实例内存超过 10GB 时,fork()操作可能导致较长时间的阻塞
- 高写入率场景:频繁的写操作会触发大量的写时复制,增加内存压力
- 内存受限环境:如果系统没有足够的空闲内存应对写时复制的内存增长,可能触发 swap
- 实时性要求极高的场景:即使毫秒级的阻塞也无法接受的应用
在这些场景下,可以考虑的替代方案:
- 使用无持久化模式(作为纯缓存使用)
- 使用 Redis 4.0+的混合持久化(RDB+AOF)
- 采用 Redis 集群,将数据分散到多个较小的实例
- 考虑 Redis 企业版的无分叉(fork-less)RDB 特性
总结
Redis 在生成 RDB 文件期间依然能够处理客户端请求,这归功于 fork()系统调用和写时复制机制。虽然 BGSAVE 操作不会完全阻塞 Redis 服务,但确实会对性能产生一定影响。下表总结了 RDB 生成过程中的请求处理情况:
| 阶段 | 请求处理状态 | 潜在影响 | 优化方向 | Redis 版本差异 |
|---|---|---|---|---|
| fork()调用期间 | 短暂阻塞 | 毫秒级阻塞,与内存页表大小相关 | 优化内存使用,vm.overcommit_memory=1 | Redis 5.0+优化了 fork 性能 |
| 子进程生成 RDB | 正常处理 | 写操作触发 COW 增加内存使用 | 控制写入速率,增加系统内存 | Redis 4.0+混合持久化减少 COW 开销 |
| 子进程磁盘写入 | 正常处理 | I/O 压力可能影响整体性能 | 使用高性能存储,错峰执行 BGSAVE | Redis 6.0+多线程 I/O 提升性能 |
| 子进程退出通知 | 正常处理 | 几乎无影响 | 无需特别优化 | 所有版本相似 |
| 企业版无分叉 RDB | 正常处理 | 无 fork()阻塞,内存增长更小 | 适用于超大内存实例 | 仅 Redis 企业版支持 |