一、项目背景与需求
在最近项目系统平台的系统架构优化中,我们面临一个关键挑战:平台初期采用Redis作为核心数据存储,随着业务发展,需要将部分持久化数据迁移到关系型数据库中以实现更复杂的数据分析和事务处理。由于平台要求7×24小时不间断服务,必须在保证系统高可用的前提下完成数据迁移。(下面以电商系统 Java代码 为例)。
二、Redis持久化底层机制深度解析
2.1 RDB持久化原理
// RDB创建过程的核心逻辑模拟
public class RDBPersistence {
// 创建RDB快照的核心方法
public void createRDBSnapshot() {
try {
// 1. 主进程fork子进程(Linux的Copy-On-Write机制)
ProcessBuilder pb = new ProcessBuilder();
Process childProcess = pb.start();
if (childProcess.pid() == 0) { // 子进程上下文
// 2. 遍历数据库字典,序列化键值对
try (FileOutputStream rdbFile = new FileOutputStream("dump.rdb")) {
Map<String, Object> database = getCurrentDatabase();
for (Map.Entry<String, Object> entry : database.entrySet()) {
// 3. 使用RDB格式序列化数据
byte[] serializedData = serializeToRDBFormat(
entry.getKey(),
entry.getValue()
);
rdbFile.write(serializedData);
}
}
System.exit(0);
} else { // 主进程
// 4. 继续处理客户端请求,利用COW机制保证数据一致性
continueServingClients();
}
} catch (IOException e) {
// 处理异常
}
}
private byte[] serializeToRDBFormat(String key, Object value) {
// RDB特定的二进制序列化逻辑
// 包含类型、过期时间、编码等信息
return new byte[0]; // 简化实现
}
}
RDB核心特点:
-
二进制压缩格式,恢复速度快
-
全量备份,适合灾难恢复
-
fork操作可能阻塞主进程(大数据量时)
-
数据安全性较低(可能丢失最后一次快照后的数据)
2.2 AOF持久化原理
// AOF持久化流程模拟
@Component
public class AOFMechanism {
private List<String> aofBuffer = new CopyOnWriteArrayList<>();
private String aofFsyncPolicy = "everysec";
@Async
public CompletableFuture<Void> executeCommand(String command, Object... args) {
// 1. 执行Redis命令
Object result = processCommand(command, args);
// 2. 将命令追加到AOF缓冲区
String aofCommand = formatToRedisProtocol(command, args);
aofBuffer.add(aofCommand);
// 3. 根据策略同步到磁盘
if (shouldSync()) {
fsyncAOFFile();
}
return CompletableFuture.completedFuture(null);
}
@Async
public void bgRewriteAOF() {
// AOF重写:创建新进程,遍历数据库生成最小命令集
// 使用子进程避免阻塞主线程
CompletableFuture.runAsync(() -> {
try {
rewriteAOFInBackground();
} catch (Exception e) {
// 处理重写异常
}
});
}
private boolean shouldSync() {
switch (aofFsyncPolicy) {
case "always":
return true; // 每个命令都同步
case "everysec":
// 每秒同步一次
return checkSecondElapsed();
case "no":
return false; // 由操作系统决定
default:
return checkSecondElapsed();
}
}
}
AOF工作模式:
-
appendfsync always:每个命令都同步,数据最安全,性能最低
-
appendfsync everysec:每秒同步,平衡安全性与性能(默认)
-
appendfsync no:由操作系统决定,性能最高,数据安全性最低
2.3 混合持久化(Redis 4.0+)
// 混合持久化配置示例
@Configuration
public class RedisPersistenceConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 开启混合持久化
// 1. 定期生成RDB快照
// 2. 两次RDB之间的增量命令写入AOF
// 3. 重启时先加载RDB,再重放AOF
return template;
}
}
三、不停机数据迁移方案
3.1 双写机制迁移方案
核心迁移服务
@Service
@Slf4j
public class DataMigrationService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@Value("${migration.enabled:false}")
private boolean migrationEnabled;
@Value("${migration.batch.size:1000}")
private int batchSize;
/**
* 双写机制:同时写入Redis和数据库
*/
@Async
public void dualWrite(String key, Object value) {
try {
// 1. 写入Redis(保持原有逻辑不变)
redisTemplate.opsForValue().set(key, value);
// 2. 同时写入数据库
if (migrationEnabled) {
writeToDatabase(key, value);
}
} catch (Exception e) {
log.error("双写失败 key: {}", key, e);
// 告警通知,但不影响主流程
}
}
/**
* 存量数据迁移
*/
@Async
public void migrateExistingData() {
log.info("开始存量数据迁移...");
try {
// 1. 使用SCAN命令避免阻塞(代替KEYS *)
Cursor<byte[]> cursor = redisTemplate.getConnectionFactory()
.getConnection()
.scan(ScanOptions.scanOptions().count(batchSize).build());
int total = 0;
List<CompletableFuture<Void>> futures = new ArrayList<>();
while (cursor.hasNext()) {
String key = new String(cursor.next());
// 2. 异步迁移每个键值对
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
writeToDatabase(key, value);
}
} catch (Exception e) {
log.warn("迁移失败 key: {}", key, e);
}
});
futures.add(future);
total++;
// 3. 批量控制,避免内存溢出
if (futures.size() >= batchSize) {
CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).join();
futures.clear();
log.info("已迁移 {} 条数据", total);
}
}
// 等待剩余任务完成
if (!futures.isEmpty()) {
CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).join();
}
log.info("存量数据迁移完成,总计: {} 条", total);
} catch (Exception e) {
log.error("存量数据迁移异常", e);
}
}
private void writeToDatabase(String key, Object value) {
// 根据业务逻辑写入数据库
String sql = "INSERT INTO redis_migration (r_key, r_value, created_time) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE r_value = ?, updated_time = ?";
jdbcTemplate.update(sql,
key,
serializeValue(value),
new Date(),
serializeValue(value),
new Date()
);
}
private String serializeValue(Object value) {
// 序列化逻辑,根据实际数据类型调整
return value.toString();
}
}
3.2 增量数据同步方案
基于Redis Stream的增量同步
@Component
@Slf4j
public class IncrementalSyncService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private DataMigrationService migrationService;
private volatile boolean running = false;
/**
* 启动增量数据同步
*/
public void startIncrementalSync() {
running = true;
CompletableFuture.runAsync(() -> {
String lastId = "0"; // 从开始读取
while (running) {
try {
// 监听Redis键空间通知
List<Object> results = redisTemplate.execute(new RedisCallback<List<Object>>() {
@Override
public List<Object> doInRedis(RedisConnection connection)
throws DataAccessException {
return connection.execute(
"XREAD",
"BLOCK", "1000", "STREAMS",
"__keyspace@0__:*",
lastId
);
}
});
if (results != null) {
processKeyEvents(results);
}
} catch (Exception e) {
log.error("增量同步异常", e);
// 异常处理与重试逻辑
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
});
}
private void processKeyEvents(List<Object> events) {
// 处理键空间事件,同步到数据库
for (Object event : events) {
// 解析事件类型(set、del、expire等)
// 同步到目标数据库
log.debug("处理键事件: {}", event);
}
}
public void stopIncrementalSync() {
running = false;
}
}
3.3 数据一致性验证
迁移数据校验服务
@Service
@Slf4j
public class DataConsistencyService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 抽样验证数据一致性
*/
public void verifyConsistency(double sampleRate) {
log.info("开始数据一致性验证,采样率: {}", sampleRate);
// 1. 随机采样Redis键
Set<String> sampleKeys = sampleRedisKeys(sampleRate);
int inconsistentCount = 0;
int totalChecked = 0;
for (String key : sampleKeys) {
try {
// 2. 对比Redis和数据库中的数据
Object redisValue = redisTemplate.opsForValue().get(key);
Object dbValue = getValueFromDatabase(key);
if (!isDataEqual(redisValue, dbValue)) {
inconsistentCount++;
log.warn("数据不一致 key: {}, Redis: {}, DB: {}",
key, redisValue, dbValue);
}
totalChecked++;
} catch (Exception e) {
log.error("验证数据异常 key: {}", key, e);
}
}
double inconsistencyRate = (double) inconsistentCount / totalChecked;
log.info("一致性验证完成: 检查 {} 条,不一致 {} 条,不一致率: {:.4f}%",
totalChecked, inconsistentCount, inconsistencyRate * 100);
if (inconsistencyRate > 0.01) { // 超过1%的不一致率需要告警
sendAlert(inconsistentCount, inconsistencyRate);
}
}
private Set<String> sampleRedisKeys(double rate) {
// 实现随机采样逻辑
Set<String> allKeys = redisTemplate.keys("*");
return allKeys.stream()
.filter(key -> Math.random() < rate)
.collect(Collectors.toSet());
}
private Object getValueFromDatabase(String key) {
String sql = "SELECT r_value FROM redis_migration WHERE r_key = ?";
return jdbcTemplate.queryForObject(sql, String.class, key);
}
private boolean isDataEqual(Object redisValue, Object dbValue) {
// 实现数据对比逻辑,考虑序列化差异
if (redisValue == null && dbValue == null) return true;
if (redisValue == null || dbValue == null) return false;
return redisValue.toString().equals(dbValue.toString());
}
}
四、迁移流程与监控
4.1 分阶段迁移方案
@Component
@Slf4j
public class MigrationOrchestrator {
@Autowired
private DataMigrationService migrationService;
@Autowired
private IncrementalSyncService syncService;
@Autowired
private DataConsistencyService consistencyService;
/**
* 执行完整迁移流程
*/
public void executeMigration() {
try {
// 阶段1: 开启双写,准备阶段
log.info("=== 迁移阶段1: 开启双写 ===");
enableDualWrite();
// 阶段2: 存量数据迁移
log.info("=== 迁移阶段2: 存量数据迁移 ===");
migrationService.migrateExistingData();
// 阶段3: 启动增量同步
log.info("=== 迁移阶段3: 增量数据同步 ===");
syncService.startIncrementalSync();
// 阶段4: 数据一致性验证
log.info("=== 迁移阶段4: 数据验证 ===");
consistencyService.verifyConsistency(0.1); // 10%采样
// 阶段5: 切换读流量(逐步)
log.info("=== 迁移阶段5: 流量切换 ===");
graduallySwitchReadTraffic();
// 阶段6: 清理Redis数据(可选)
log.info("=== 迁移阶段6: 清理完成 ===");
} catch (Exception e) {
log.error("迁移流程执行失败", e);
// 回滚逻辑
rollbackMigration();
}
}
private void enableDualWrite() {
// 通过配置中心动态开启双写
// 确保所有写操作都经过双写服务
}
private void graduallySwitchReadTraffic() {
// 逐步将读流量从Redis切换到数据库
// 可以按业务模块、用户分组等方式逐步切换
}
private void rollbackMigration() {
// 迁移失败时的回滚逻辑
syncService.stopIncrementalSync();
// 关闭双写,恢复原有逻辑
}
}
4.2 监控与告警
@Component
@Slf4j
public class MigrationMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(fixedRate = 60000) // 每分钟执行
public void monitorMigrationProgress() {
try {
// 监控Redis和数据库的数据量对比
Long redisCount = getRedisKeyCount();
Long dbCount = getDatabaseRecordCount();
double migrationProgress = (double) dbCount / redisCount;
log.info("迁移进度: Redis {} 条, 数据库 {} 条, 进度: {:.2f}%",
redisCount, dbCount, migrationProgress * 100);
// 发送监控指标
sendMetrics(redisCount, dbCount, migrationProgress);
if (migrationProgress > 0.95) {
log.info("迁移接近完成,准备最终切换");
}
} catch (Exception e) {
log.error("监控任务执行异常", e);
}
}
private Long getRedisKeyCount() {
return redisTemplate.execute(connection ->
connection.serverCommands().dbSize());
}
private Long getDatabaseRecordCount() {
String sql = "SELECT COUNT(*) FROM redis_migration";
return jdbcTemplate.queryForObject(sql, Long.class);
}
}
五、总结注意点
5.1 要点
-
理解Redis持久化机制:根据业务需求选择合适的持久化策略
-
迁移方案选择:双写+增量同步是最安全的不停机迁移方案
-
数据一致性:考虑建立完善的数据验证机制
-
监控告警:全流程监控确保迁移可控
5.2 风险控制
-
回滚方案:必须准备完善的回滚机制(开关管控)
-
性能影响:监控系统资源,避免迁移影响正常业务
5.3 适用场景
该方案适用于:
-
Redis到关系型数据库的数据迁移
-
需要保证零停机的数据架构调整
-
大数据量的实时数据同步场景

670

被折叠的 条评论
为什么被折叠?



