在数据同步(如系统重构、分库分表、多源整合)场景中,“本地数据一致,生产环境条数对不上”是典型痛点。问题常源于并发处理失控、数据库性能瓶颈、字段映射错误、缓存脏数据等多维度缺陷。本文结合实战经验,从应用层、数据库层(源库/中间库/目标库)、缓存层、字段变更处理等维度,提供覆盖全链路的系统性解决方案。
核心问题:
Too many connections
异常)。解决方案:
// 8核服务器配置:核心线程=4,最大线程=8(避免超过数据库处理能力)
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(10000); // 任务队列缓冲,削峰填谷
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略:调用线程直接执行,避免任务丢失
@Transactional(propagation = Propagation.REQUIRES_NEW)
为每个批次创建独立事务,避免跨线程事务污染:@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processBatch(List<DataVO> batchData, CountDownLatch latch) {
batchInsert(batchData); // 独立事务内的批量插入
latch.countDown(); // 子线程完成后计数器减一
}
CountDownLatch
阻塞主线程,确保所有子线程执行完毕:CountDownLatch latch = new CountDownLatch(totalBatch);
// 提交子线程任务时传入latch
latch.await(); // 主线程等待所有批次完成
优化点:
List<DataVO> batch = new ArrayList<>(pageSize);
for (DataVO vo : cursor) {
batch.add(vo);
if (batch.size() == pageSize) {
processBatch(batch, latch);
batch.clear();
}
}
if (insertCount > 0) {
log.info("批次插入成功,休眠1秒");
Thread.sleep(1000); // 给数据库缓冲时间
}
核心挑战:
VARCHAR
转TEXT
)或新增字段,导致数据抽取失败。解决方案:
DatabaseMetaData
)获取源库与目标库字段信息,建立映射关系,处理类型不匹配:// 示例:处理源库新增字段(目标库无该字段时忽略)
Map<String, String> sourceColumns = getSourceTableColumns("source_table");
Map<String, String> targetColumns = getTargetTableColumns("target_table");
List<String> validColumns = sourceColumns.keySet().stream()
.filter(targetColumns::contains)
.collect(Collectors.toList());
Cursor
)流式读取数据,避免全量加载到内存:try (SqlSession session = sqlSessionFactory.openSession();
Cursor<DataVO> cursor = session.getMapper(SourceMapper.class).streamData()) {
cursor.forEach(vo -> handleData(vo));
}
核心问题:
解决方案:
source_id + sync_time
),结合Spring Retry实现幂等写入:@Retryable(value = SQLException.class, maxAttempts = 3)
public void writeToMiddleDB(DataVO data) {
middleMapper.insertOnDuplicateKeyUpdate(data); // 幂等插入(ON DUPLICATE KEY UPDATE)
}
NOT NULL
字段),缺失时填充默认值或记录错误:if (StringUtils.isBlank(data.getTargetRequiredField())) {
data.setTargetRequiredField("default_value"); // 填充默认值
log.warn("字段缺失,已填充默认值:{}", data.getId());
}
关键参数与配置:
rewriteBatchedStatements=true
,激活MySQL批量写入能力(需驱动5.1.13+):url: jdbc:mysql://target-host:3306/target-db?rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
my.cnf
):[mysqld]
max_allowed_packet=512M # 支持大批次数据传输
max_connections=60000 # 适应高并发写入
bulk_insert_buffer_size=512M # 优化批量插入性能
innodb_lock_wait_timeout=300 # 减少长事务锁等待超时
insertBatchSomeColumn
或原生INSERT INTO ... VALUES
批量语法,避免逐条执行:// 批量插入并过滤目标库不存在的字段
creditRecordMapper.insertBatchSomeColumn(
dataList,
columnList -> columnList.contains("id", "member_id", "create_time") // 显式指定目标库字段
);
处理策略:
INT
转目标库BIGINT
,DATETIME
转TIMESTAMP
):Map<Class<?>, Class<?>> typeMapping = new HashMap<>();
typeMapping.put(Integer.class, Long.class);
typeMapping.put(java.sql.Timestamp.class, LocalDateTime.class);
TypeMismatchException
,记录失败数据并跳过,避免全量任务中断:try {
convertField(sourceField, targetType);
} catch (TypeMismatchException e) {
log.error("字段类型转换失败:source={}, target={}, data={}",
sourceField, targetType, data.getId(), e);
errorData.add(data); // 收集错误数据后续处理
}
处理方案:
ALTER TABLE
语句,避免插入时字段不存在:ALTER TABLE target_table ADD COLUMN new_field VARCHAR(50) DEFAULT NULL;
List<String> targetColumnWhitelist = Arrays.asList("id", "name", "create_time");
dataVO.getColumns().keySet().removeIf(col -> !targetColumnWhitelist.contains(col));
双删策略+延迟失效:
// 示例:Redis双删实现
redisTemplate.delete("cache:user:" + userId);
syncDataToDatabase();
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500);
redisTemplate.delete("cache:user:" + userId);
} catch (InterruptedException e) { /* 处理中断 */ }
});
异步刷新机制:
// Canal监听新增数据,刷新缓存
if (event.getType() == INSERT) {
CacheKey key = generateCacheKey(event.getData());
cacheService.refresh(key, loadFromDatabase(key));
}
多维度校验:
COUNT(*)
,定位数据丢失环节;LEFT JOIN
或EXCEPT
语句找出源库有而目标库无的记录:-- MySQL查找差异数据
SELECT s.* FROM source_table s
LEFT JOIN target_table t ON s.id = t.id
WHERE t.id IS NULL;
String sourceHash = MD5Utils.md5Hex(sourceData.toString());
String targetHash = MD5Utils.md5Hex(targetData.toString());
if (!sourceHash.equals(targetHash)) {
log.error("数据内容不一致:id={}", data.getId());
}
分级处理策略:
@Retryable(value = SQLException.class, backoff = @Backoff(delay = 1000, multiplier = 2))
public void retryInsert(DataVO data) { /* 重试插入逻辑 */ }
-- 批量插入补偿数据
INSERT INTO target_table (id, name, create_time) VALUES
(1001, '补录数据', NOW()),
(1002, '补录数据', NOW());
批量删除策略:
SCAN
命令避免阻塞式删除,清理与同步数据相关的所有缓存:# Redis批量删除用户相关缓存(避免KEYS命令阻塞)
redis-cli --scan --pattern "user:123:*" | xargs redis-cli del
SHOW STATUS LIKE 'Threads_connected'
)和慢查询日志。start_time
、end_time
、data_count
、error_count
,通过Prometheus+Grafana可视化同步进度。在数据同步体系中,全量同步(首次初始化或重置数据)与增量同步(实时/定时更新变化数据)是两类核心场景。若发现数据未同步(如全量漏批、增量丢失),需针对两类场景的特性设计专项修复策略。
CREATE TABLE sync_breakpoint (
table_name VARCHAR(100) PRIMARY KEY,
last_processed_id BIGINT, -- 最后处理的记录ID
last_batch_time TIMESTAMP, -- 最后批次处理时间
status VARCHAR(20) -- 状态:RUNNING/PAUSED/FAILED
);
// 读取断点,确定本次同步起始ID
Long startId = breakpointMapper.getLastProcessedId(tableName);
startId = (startId == null) ? 0 : startId + 1; // 从下一条开始
// 分页查询:WHERE id >= startId LIMIT pageSize
List<DataVO> dataList = sourceMapper.selectByRange(startId, pageSize);
UUID+时间戳
),失败时记录到日志表,支持精准重传:// 批次处理
String batchNo = generateBatchNo();
try {
processBatch(dataList, batchNo);
breakpointMapper.updateLastProcessedId(tableName, maxId); // 成功后更新断点
} catch (Exception e) {
syncLogMapper.insertFailedBatch(batchNo, e.getMessage()); // 记录失败批次
throw e; // 触发重试
}
COUNT(*)
,若不一致则触发全量扫描:-- 源库与目标库总量差异
SELECT source_count - target_count AS diff FROM (
SELECT COUNT(*) AS source_count FROM source_table
) s, (
SELECT COUNT(*) AS target_count FROM target_table
) t;
ID IN (漏失ID列表)
)补录数据,避免全量重跑:// 获取漏失ID列表(通过LEFT JOIN)
List<Long> missingIds = sourceMapper.findMissingIds(targetTable);
if (!missingIds.isEmpty()) {
List<DataVO> missingData = sourceMapper.selectByIds(missingIds);
targetMapper.batchInsert(missingData); // 批量补录
}
修复增量标记:
update_time
时间戳,查询源库中update_time > 最后同步时间
的数据,重新同步:SELECT * FROM source_table
WHERE update_time > '2025-04-26 10:00:00' -- 最后成功同步时间
ORDER BY update_time ASC;
version
版本号(乐观锁字段),查找version > 最后同步版本
的记录:long lastVersion = incrementalConfig.getLastVersion();
List<DataVO> incrementalData = sourceMapper.selectByVersionGreaterThan(lastVersion);
幂等性处理:目标库使用ON DUPLICATE KEY UPDATE
避免重复插入(需定义唯一约束,如source_id
):
INSERT INTO target_table (source_id, data)
VALUES (1001, 'data')
ON DUPLICATE KEY UPDATE data = VALUES(data); -- 冲突时更新
binlog_file
和binlog_pos
);canalConnector.connectAndSync();
canalConnector.position(new Position(binlogFile, binlogPos)); // 重置解析位点
SHOW BINARY LOGS
获取历史日志文件,使用mysqlbinlog
工具解析指定时间范围的变更:# 解析2025-04-26 00:00:00到10:00:00的Binlog
mysqlbinlog --start-datetime="2025-04-26 00:00:00" --stop-datetime="2025-04-26 10:00:00" /var/lib/mysql/mysql-bin.000001 > binlog.sql
maxReconsumeTimes=3
),失败消息进入死信队列:// RocketMQ消费者配置
consumer.setMaxReconsumeTimes(3);
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
try {
processIncrementalData(msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
context.setDelayLevel(3); // 延迟5秒重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
// 死信队列消息重投
Message deadLetterMsg = deadLetterQueue.fetchMessage();
if (repairIncrementalData(deadLetterMsg)) { // 修复数据
normalQueue.sendMessage(deadLetterMsg); // 重新投递
}
当全量同步失败且已产生增量变更时,需采用“全量打底+增量追补”策略:
-- 临时表存储全量期间的增量数据
CREATE TABLE temp_incremental (
id BIGINT PRIMARY KEY,
operation VARCHAR(10), -- INSERT/UPDATE/DELETE
data JSON
);
-- 合并到目标库
INSERT INTO target_table (id, data)
SELECT id, data FROM temp_incremental
ON DUPLICATE KEY UPDATE data = VALUES(data);
last_sync_time
),若超过5分钟未更新则报警:SELECT table_name FROM incremental_config
WHERE last_sync_time < NOW() - INTERVAL 5 MINUTE;
canal_lag_seconds > 300 // Binlog解析延迟超过5分钟
rocketmq_queue_consumer_offset - rocketmq_queue_max_offset > 1000 // MQ积压量
long sourceSum = sourceMapper.sumAmount();
long targetSum = targetMapper.sumAmount();
if (Math.abs(sourceSum - targetSum) > sourceSum * 0.1%) {
triggerAutoRepair(); // 自动触发差异数据修复
}
数据同步的核心是“以终为始”——无论全量还是增量,最终目标是让目标库数据与源库“实时、准确、完整”。通过断点续传、幂等插入、Binlog补抓等专项技术,配合自动化监控与修复机制,可将数据未同步的风险降至最低,保障业务系统的稳定运行。