前言
之前完成了部门的千万量级的数据迁移工作,现在做一个总结。背景是公司产品需要升级,涉及到新的架构,业务功能也需要升级,对原表结构也进行调整。所以全量迁移工作不能简单的通过sql导入导出解决。
实现方案
首先迁移工作需要完成对存量数据的迁移,基于此当时有多种方案选择,最终确定的方案是通过dba将原数据库中数据导出成一个静态数据库,通过业务程序将对应数据同步到另一个新增数据库中。
由于原数据库是分库分表的结构,所以导出后的静态数据库中,同一个表实际上分为N个分表,为保证效率,所以使用多个队列同步消费,每个队列中配置多个分表并发消费。
启动加载
/**
* 执行同步方法入口
*
* @param args
*/
public void run(String... args) {
tableSyncQueue.run(queueConsumer);
}
/**
* 队列回调方法
*
* @param tableNames 执行队列传递的表名
*/
Consumer<String> queueConsumer = tableNames -> {
if (StringUtils.isBlank(tableNames)) {
return;
}
String[] arr = tableNames.split(",");
for (String tableName : arr) {
//redis的key,记录当前同步到那个表
String tempKey = CanalRedisKey.SYNC_ALL_DATA_TABLE_STATE + tableName;
if (StrUtil.isNotBlank(tableName) && tableName.startsWith("#")) {
tableName = tableName.substring(1);
tempKey = CanalRedisKey.SYNC_ALL_DATA_TABLE_STATE + tableName + podIndex;
}
final String cacheKey = tempKey;
Integer value = (Integer) canalCacheUtil.get(cacheKey);
if (CanalConstant.NUM_1.equals(value)) {
log.info("{}表,缓存中标记已同步完成,无需再同步!缓存KEY为:{}", tableName, cacheKey);
atomicCount.incrementAndGet();
continue;
}
//为每个表单独启动一个线程同步
String finalTableName = tableName;
taskExecutor.execute(() -> {
try {
LogRequestIdUtil.putLogId(IdUtil.fastSimpleUUID());
log.info("开始同步[{}]表数据", finalTableName);
//0 表示同步中
canalCacheUtil.set(cacheKey, CanalConstant.NUM_0);
StopWatch watch = new StopWatch();
watch.start();
Long count = executeSync(finalTableName);
watch.stop();
//1 表示同步已结束
canalCacheUtil.set(cacheKey, CanalConstant.NUM_1);
log.info("结束同步[{}]表数据;同步数据量:{}条,耗时:{}毫秒", finalTableName, count == null ? 0 : count, watch.getTotalTimeMillis());
} catch (Exception e) {
log.error("执行[{}]表同步失败,错误为:", finalTableName, e);
} finally {
atomicCount.incrementAndGet();
}
});
}
//循环任务,获取多线程是否执行完成,全部执行完成才跳出循环,执行下一个队列的任务
while (true) {
if (arr.length == atomicCount.get()) {
atomicCount.set(0);
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
};
/**
* 执行某个表的全量同步 采用多线程方式
*
* @param tableName 对应要同步的表名
*/
public Long executeSync(String tableName) {
String suffix = getSuffix(tableName);
if (StringUtils.isNotBlank(suffix)) {
tableName = tableName.substring(0, tableName.lastIndexOf("_"));
}
TableSyncAbstract syncAbstract = getBean(tableName, suffix);
return syncAbstract.execute(tableName, suffix);
}
队列消费
@Slf4j
public class TableSyncQueue {
static LinkedTransferQueue<String> lnkTransQueue = new LinkedTransferQueue<>();
@Resource
QueueParams queueParams;
@Qualifier("taskExecutor")
@Autowired
private TaskExecutor taskExecutor;
public void run(Consumer<String> queueFunction) {
//队列书面
Integer queueNum = (Integer) queueParams.getValue("queueNum");
//启动生产者线程队列
taskExecutor.execute(()->{
for (int i = 1; i <= queueNum; i++) {
try {
//每个队列中配置的分表名
lnkTransQueue.transfer((String) queueParams.getValue("queue" + i));
} catch (InterruptedException e) {
log.error("插入队列异常!", e);
}
}
});
//启动一个消费者线程队列
taskExecutor.execute(()->{
for (int i = 1; i <= queueNum; i++) {
try {
//分表名称列表
String tableNames = lnkTransQueue.take();
if(StringUtils.isBlank(tableNames)){
log.info("队列{}未配置表名信息,跳过该队列!", i);
}else{
log.info("开始执行队列{}中的{}表同步", i, tableNames);
queueFunction.accept(tableNames);
log.info("队列{}中的表同步完成,进行下一个队列调用", i, tableNames);
}
if(i == queueNum){
log.info("同步完成");
}
} catch (InterruptedException e) {
log.error("消费队列异常!", e);
}
}
});
}
}
队列配置属性
@Slf4j
@Component
public class QueueParams {
/**
* 队列
*/
@Value("${all.sync.queue.num:7}")
private Integer queueNum;
@Value("${all.sync.queue1}")
private String queue1;
@Value("${all.sync.queue2}")
private String queue2;
@Value("${all.sync.queue3}")
private String queue3;
@Value("${all.sync.queue4}")
private String queue4;
@Value("${all.sync.queue5}")
private String queue5;
@Value("${all.sync.queue6}")
private String queue6;
@Value("${all.sync.queue7}")
private String queue7;
private static Map<String,Object> properties = new HashMap<>();
/**
* 把变量值缓存到MAP对象中
*/
public void init(){
properties.put("queueNum",queueNum);
properties.put("queue1",queue1);
properties.put("queue2",queue2);
properties.put("queue3",queue3);
properties.put("queue4",queue4);
properties.put("queue5",queue5);
properties.put("queue6",queue6);
properties.put("queue7",queue7);
}
/**
* 获取对应属性值
* @param key
* @return
*/
public Object getValue(String key){
//为了达到动态读取效果,这里不做判断 每次都去注入一遍
init();
Object obj = properties.get(key);
if(obj == null){
log.error("获取配置参数{}失败,请查看是否配置该参数!",key);
}
return obj;
}
}
后续通过SpringContextUtil.getBean获取指定的实现类完成全量同步