1. 核心需求复述
完全解耦、可复用的数据同步断点续传方案核心目标是让断点续传的通用逻辑(分布式锁、断点读写、批次控制、重试、幂等)与具体业务逻辑(订单/商品/用户数据同步)分离,新增任意业务数据同步时,仅需实现差异化的查询和同步逻辑,无需重复开发通用的断点续传框架。
2. 通用解耦版断点续传完整方案
2.1 整体架构设计
采用「接口抽象 + 模板方法模式 + 策略模式」实现解耦:
- 抽象层 :定义
SyncDataSource(数据源)和SyncHandler(同步处理器)接口,封装业务差异化逻辑; - 模板层 :
GenericSyncTemplate封装所有通用断点续传逻辑(锁、断点、重试、批次),依赖抽象接口而非具体业务; - 业务层 :订单/商品等业务实现抽象接口,Spring自动收集所有实现类,通过
bizCode动态路由;
css
flowchart TB
A[控制器] -->|传入bizCode| B[GenericSyncTemplate(通用模板)]
B -->|1. 分布式锁| C[Redisson]
B -->|2. 断点读写| D[BreakPointManager]
B -->|3. 任务状态| E[SyncTaskService]
B -->|4. 路由到业务实现| F{bizCode匹配}
F -->|order_sync| G[OrderSyncDataSource + OrderSyncHandler]
F -->|product_sync| H[ProductSyncDataSource + ProductSyncHandler]
F -->|user_sync| I[UserSyncDataSource + UserSyncHandler]
G/H/I -->|实现| J[SyncDataSource(查数据)+ SyncHandler(同步数据)]
2.2 完整可运行代码
2.2.1 环境依赖(pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>sync-resume-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sync-resume-generic</name>
<description>通用解耦版数据同步断点续传</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
</properties>
<dependencies>
<!-- SpringCloud核心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 分布式锁/Redis -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2.2 核心配置(application.yml)
yaml
server:
port: 8080
spring:
application:
name: sync-resume-generic
# Nacos注册中心
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 数据库配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/sync_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# Redis配置
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.sync.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 同步通用配置
sync:
batch-size: 1000 # 默认批次大小
lock-prefix: sync_lock # 分布式锁前缀
break-point-prefix: sync_break_point_ # 断点前缀
retry-count: 3 # 默认重试次数
2.2.3 核心抽象接口
通用数据源接口(SyncDataSource)
java
package com.example.sync.core;
import java.util.List;
/**
* 通用同步数据源接口(定义"查什么数据"的差异化逻辑)
* @param <T> 业务数据类型(如OrderDO、ProductDO)
*/
public interface SyncDataSource<T> {
/**
* 按断点查询待同步数据
* @param lastBreakPoint 最后同步的断点(ID)
* @param batchSize 批次大小
* @return 待同步数据列表
*/
List<T> selectWaitSyncData(Long lastBreakPoint, Integer batchSize);
/**
* 批量更新同步状态(幂等保障)
* @param dataIds 业务数据ID列表
* @param syncStatus 同步状态(0-未同步,1-已同步)
*/
void batchUpdateSyncStatus(List<Long> dataIds, Integer syncStatus);
/**
* 获取单条数据的ID(统一断点标识)
* @param data 业务数据
* @return 数据ID
*/
Long getDataId(T data);
/**
* 获取业务编码(与SyncHandler一致)
* @return 业务编码(如order_sync)
*/
String getBizCode();
}
通用同步处理器接口(SyncHandler)
java
package com.example.sync.core;
import java.util.List;
/**
* 通用同步处理器接口(定义"怎么同步数据"的差异化逻辑)
* @param <T> 业务数据类型(如OrderDO、ProductDO)
*/
public interface SyncHandler<T> {
/**
* 同步数据到目标服务/数据库
* @param dataList 待同步数据列表
*/
void syncToTarget(List<T> dataList);
/**
* 获取业务编码(唯一标识,如order_sync、product_sync)
* @return 业务编码
*/
String getBizCode();
}
通用同步上下文(SyncContext)
arduino
package com.example.sync.core;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 通用同步上下文(封装任务参数,避免方法参数过多)
*/
@Data
@Accessors(chain = true)
public class SyncContext {
/** 业务编码(核心:路由到具体业务实现) */
private String bizCode;
/** 批次大小(默认取配置) */
private Integer batchSize;
/** 重试次数(默认取配置) */
private Integer retryCount;
/** 分布式锁前缀(默认取配置) */
private String lockPrefix;
/** 断点前缀(默认取配置) */
private String breakPointPrefix;
}
2.2.4 通用模板类(核心封装)
ini
package com.example.sync.core;
import com.example.sync.service.SyncTaskService;
import com.example.sync.util.BreakPointManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 通用断点续传同步模板类(封装所有通用逻辑,业务无需关心)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GenericSyncTemplate {
// 通用配置
@Value("${sync.batch-size}")
private Integer defaultBatchSize;
@Value("${sync.lock-prefix}")
private String defaultLockPrefix;
@Value("${sync.break-point-prefix}")
private String defaultBreakPointPrefix;
@Value("${sync.retry-count}")
private Integer defaultRetryCount;
// 通用组件
private final BreakPointManager breakPointManager;
private final SyncTaskService syncTaskService;
private final RedissonClient redissonClient;
// Spring自动收集所有业务实现(key=bean名称,需手动适配bizCode)
private final Map<String, SyncDataSource<?>> syncDataSourceMap;
private final Map<String, SyncHandler<?>> syncHandlerMap;
/**
* 通用同步方法(启动/恢复断点续传)
* @param context 同步上下文
*/
@SuppressWarnings("unchecked")
public void startOrResumeSync(SyncContext context) {
// 1. 补全上下文默认值
String bizCode = context.getBizCode();
Integer batchSize = context.getBatchSize() == null ? defaultBatchSize : context.getBatchSize();
Integer retryCount = context.getRetryCount() == null ? defaultRetryCount : context.getRetryCount();
String lockPrefix = context.getLockPrefix() == null ? defaultLockPrefix : context.getLockPrefix();
// 2. 查找业务实现(按bizCode匹配)
SyncDataSource<?> dataSource = findDataSourceByBizCode(bizCode);
SyncHandler<?> handler = findHandlerByBizCode(bizCode);
if (dataSource == null || handler == null) {
throw new RuntimeException("未找到业务[" + bizCode + "]的同步实现类");
}
// 3. 分布式锁:避免高并发下多节点执行同一任务
String lockKey = lockPrefix + "_" + bizCode;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!locked) {
log.warn("业务[{}]获取分布式锁失败,任务已在执行", bizCode);
return;
}
// 4. 初始化断点(Redis → MySQL)
Long lastBreakPoint = breakPointManager.getBreakPoint(bizCode);
if (lastBreakPoint == 0) {
lastBreakPoint = syncTaskService.getSyncTaskByCode(bizCode).getLastBreakPoint();
}
log.info("业务[{}]开始同步,断点位置:{}", bizCode, lastBreakPoint);
// 5. 更新任务状态为"同步中"
syncTaskService.updateTaskStatus(bizCode, 1);
// 6. 批次同步主逻辑
int retry = 0;
while (true) {
try {
// 6.1 查询待同步数据(调用业务数据源实现)
List<?> dataList = dataSource.selectWaitSyncData(lastBreakPoint, batchSize);
if (dataList.isEmpty()) {
log.info("业务[{}]同步完成,最终断点:{}", bizCode, lastBreakPoint);
// 同步完成:清理断点+更新任务状态
breakPointManager.clearBreakPoint(bizCode);
syncTaskService.updateTaskBreakPoint(bizCode, lastBreakPoint);
syncTaskService.updateTaskStatus(bizCode, 2);
break;
}
// 6.2 同步数据到目标(调用业务处理器实现)
handler.syncToTarget(dataList);
// 6.3 更新同步状态(幂等保障)
List<Long> dataIds = (List<Long>) dataList.stream()
.map(data -> dataSource.getDataId((Object) data))
.toList();
dataSource.batchUpdateSyncStatus(dataIds, 1);
// 6.4 更新断点(Redis实时保存)
lastBreakPoint = dataSource.getDataId(dataList.get(dataList.size() - 1));
breakPointManager.updateBreakPoint(bizCode, lastBreakPoint);
log.info("业务[{}]批次同步完成,更新断点:{}", bizCode, lastBreakPoint);
// 重置重试次数
retry = 0;
} catch (Exception e) {
retry++;
log.error("业务[{}]同步失败,断点:{},重试次数:{}", bizCode, lastBreakPoint, retry, e);
if (retry >= retryCount) {
// 重试超限:标记任务异常
syncTaskService.updateTaskStatus(bizCode, 3);
throw new RuntimeException("业务[" + bizCode + "]同步失败,重试次数超限", e);
}
// 短暂休眠后重试
TimeUnit.SECONDS.sleep(1);
}
}
} catch (Exception e) {
log.error("业务[{}]同步异常", bizCode, e);
syncTaskService.updateTaskStatus(bizCode, 3);
} finally {
// 释放分布式锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 按bizCode查找数据源实现
*/
private SyncDataSource<?> findDataSourceByBizCode(String bizCode) {
return syncDataSourceMap.values().stream()
.filter(dataSource -> bizCode.equals(dataSource.getBizCode()))
.findFirst()
.orElse(null);
}
/**
* 按bizCode查找处理器实现
*/
private SyncHandler<?> findHandlerByBizCode(String bizCode) {
return syncHandlerMap.values().stream()
.filter(handler -> bizCode.equals(handler.getBizCode()))
.findFirst()
.orElse(null);
}
}
2.2.5 通用工具类
断点管理工具(BreakPointManager)
typescript
package com.example.sync.util;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 通用断点管理工具(Redis存储实时断点)
*/
@Component
@RequiredArgsConstructor
public class BreakPointManager {
private final StringRedisTemplate redisTemplate;
@Value("${sync.break-point-prefix}")
private String breakPointPrefix;
/**
* 获取业务断点
*/
public Long getBreakPoint(String bizCode) {
String key = breakPointPrefix + bizCode;
String value = redisTemplate.opsForValue().get(key);
return StringUtils.hasText(value) ? Long.parseLong(value) : 0L;
}
/**
* 更新业务断点
*/
public void updateBreakPoint(String bizCode, Long breakPoint) {
String key = breakPointPrefix + bizCode;
redisTemplate.opsForValue().set(key, breakPoint.toString());
}
/**
* 清理业务断点
*/
public void clearBreakPoint(String bizCode) {
String key = breakPointPrefix + bizCode;
redisTemplate.delete(key);
}
}
幂等注解(Idempotent)
java
package com.example.sync.annotation;
import java.lang.annotation.*;
/**
* 通用幂等注解(避免重复同步)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/** 幂等Key前缀 */
String prefix() default "sync_idempotent_";
/** 过期时间(秒) */
long expireTime() default 300;
}
幂等切面(IdempotentAspect)
java
package com.example.sync.aspect;
import com.example.sync.annotation.Idempotent;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* 通用幂等切面(基于Redis)
*/
@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {
private final StringRedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Idempotent idempotent = method.getAnnotation(Idempotent.class);
// 生成幂等Key:前缀 + 方法名 + 第一个参数(业务数据ID)
String key = idempotent.prefix() + method.getName() + "_" + joinPoint.getArgs()[0];
String value = UUID.randomUUID().toString();
// SETNX实现幂等(不存在则设置)
Boolean success = redisTemplate.opsForValue().setIfAbsent(
key, value, idempotent.expireTime(), java.util.concurrent.TimeUnit.SECONDS
);
if (Boolean.FALSE.equals(success)) {
throw new RuntimeException("当前数据正在同步,请勿重复操作");
}
try {
return joinPoint.proceed();
} finally {
// 正常执行完成后删除Key
redisTemplate.delete(key);
}
}
}
2.2.6 基础实体与服务
同步任务实体(SyncTask)
kotlin
package com.example.sync.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 同步任务配置(持久化到MySQL)
*/
@Data
@TableName("sync_task")
public class SyncTask {
@TableId(type = IdType.AUTO)
private Long id;
private String taskName; // 任务名称
private String taskCode; // 任务编码(与bizCode一致)
private String source; // 数据源
private String target; // 目标源
private Long lastBreakPoint; // 最终断点
private Integer status; // 状态:0-未开始 1-同步中 2-已完成 3-异常
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
同步任务服务(SyncTaskService)
java
package com.example.sync.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.sync.entity.SyncTask;
import com.example.sync.mapper.SyncTaskMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 同步任务配置服务(通用,所有业务复用)
*/
@Service
@RequiredArgsConstructor
public class SyncTaskService {
private final SyncTaskMapper syncTaskMapper;
/**
* 按业务编码查询任务
*/
public SyncTask getSyncTaskByCode(String bizCode) {
return syncTaskMapper.selectOne(new LambdaQueryWrapper<SyncTask>()
.eq(SyncTask::getTaskCode, bizCode));
}
/**
* 更新任务断点
*/
@Transactional(rollbackFor = Exception.class)
public void updateTaskBreakPoint(String bizCode, Long breakPoint) {
SyncTask task = getSyncTaskByCode(bizCode);
if (task == null) {
throw new RuntimeException("同步任务不存在:" + bizCode);
}
task.setLastBreakPoint(breakPoint);
task.setUpdateTime(LocalDateTime.now());
syncTaskMapper.updateById(task);
}
/**
* 更新任务状态
*/
@Transactional(rollbackFor = Exception.class)
public void updateTaskStatus(String bizCode, Integer status) {
SyncTask task = getSyncTaskByCode(bizCode);
if (task == null) {
throw new RuntimeException("同步任务不存在:" + bizCode);
}
task.setStatus(status);
task.setUpdateTime(LocalDateTime.now());
syncTaskMapper.updateById(task);
}
}
同步任务Mapper(SyncTaskMapper)
java
package com.example.sync.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sync.entity.SyncTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SyncTaskMapper extends BaseMapper<SyncTask> {
}
2.2.7 业务实现示例(订单+商品)
订单实体(OrderDO)
java
package com.example.sync.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单业务实体
*/
@Data
public class OrderDO {
private Long id; // 主键(断点标识)
private String orderNo; // 订单号
private BigDecimal amount; // 金额
private LocalDateTime createTime;
private Integer syncStatus; // 同步状态:0-未同步 1-已同步
}
订单数据源实现(OrderSyncDataSource)
kotlin
package com.example.sync.biz.order;
import com.example.sync.core.SyncDataSource;
import com.example.sync.entity.OrderDO;
import com.example.sync.mapper.OrderMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 订单同步数据源(实现通用接口,仅写订单相关查询逻辑)
*/
@Component
@RequiredArgsConstructor
public class OrderSyncDataSource implements SyncDataSource<OrderDO> {
private final OrderMapper orderMapper;
@Override
public List<OrderDO> selectWaitSyncData(Long lastBreakPoint, Integer batchSize) {
// 订单业务的查询逻辑:按断点ID查询待同步订单
return orderMapper.selectWaitSyncOrder(lastBreakPoint, batchSize);
}
@Override
public void batchUpdateSyncStatus(List<Long> dataIds, Integer syncStatus) {
// 订单业务的状态更新逻辑
orderMapper.batchUpdateSyncStatus(dataIds, syncStatus);
}
@Override
public Long getDataId(OrderDO data) {
// 订单的断点标识是ID
return data.getId();
}
@Override
public String getBizCode() {
// 订单业务编码
return "order_sync";
}
}
订单同步处理器(OrderSyncHandler)
java
package com.example.sync.biz.order;
import com.example.sync.annotation.Idempotent;
import com.example.sync.core.SyncHandler;
import com.example.sync.entity.OrderDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 订单同步处理器(实现通用接口,仅写订单同步逻辑)
*/
@Slf4j
@Component
public class OrderSyncHandler implements SyncHandler<OrderDO> {
@Override
@Idempotent(prefix = "order_sync_", expireTime = 300)
public void syncToTarget(List<OrderDO> dataList) {
// 订单业务的同步逻辑:调用Feign接口同步到仓储服务(示例)
log.info("同步{}条订单数据到仓储服务:{}", dataList.size(),
dataList.stream().map(OrderDO::getOrderNo).toList());
// 实际代码:orderFeignClient.syncOrderData(dataList);
}
@Override
public String getBizCode() {
return "order_sync";
}
}
订单Mapper(OrderMapper)
less
package com.example.sync.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sync.entity.OrderDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrderMapper extends BaseMapper<OrderDO> {
List<OrderDO> selectWaitSyncOrder(@Param("lastId") Long lastId, @Param("batchSize") Integer batchSize);
int batchUpdateSyncStatus(@Param("ids") List<Long> ids, @Param("syncStatus") Integer syncStatus);
}
订单Mapper XML(OrderMapper.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sync.mapper.OrderMapper">
<select id="selectWaitSyncOrder" resultType="com.example.sync.entity.OrderDO">
SELECT id, order_no, amount, create_time, sync_status
FROM order_table
WHERE id > #{lastId} AND sync_status = 0
ORDER BY id ASC LIMIT #{batchSize}
</select>
<update id="batchUpdateSyncStatus">
UPDATE order_table SET sync_status = #{syncStatus}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
商品实体(ProductDO)
java
package com.example.sync.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品业务实体
*/
@Data
public class ProductDO {
private Long id; // 主键(断点标识)
private String productNo; // 商品编码
private String productName; // 商品名称
private BigDecimal price; // 价格
private LocalDateTime createTime;
private Integer syncStatus; // 同步状态:0-未同步 1-已同步
}
商品数据源实现(ProductSyncDataSource)
kotlin
package com.example.sync.biz.product;
import com.example.sync.core.SyncDataSource;
import com.example.sync.entity.ProductDO;
import com.example.sync.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 商品同步数据源(实现通用接口,仅写商品相关查询逻辑)
*/
@Component
@RequiredArgsConstructor
public class ProductSyncDataSource implements SyncDataSource<ProductDO> {
private final ProductMapper productMapper;
@Override
public List<ProductDO> selectWaitSyncData(Long lastBreakPoint, Integer batchSize) {
// 商品业务的查询逻辑
return productMapper.selectWaitSyncProduct(lastBreakPoint, batchSize);
}
@Override
public void batchUpdateSyncStatus(List<Long> dataIds, Integer syncStatus) {
// 商品业务的状态更新逻辑
productMapper.batchUpdateSyncStatus(dataIds, syncStatus);
}
@Override
public Long getDataId(ProductDO data) {
return data.getId();
}
@Override
public String getBizCode() {
// 商品业务编码
return "product_sync";
}
}
商品同步处理器(ProductSyncHandler)
java
package com.example.sync.biz.product;
import com.example.sync.annotation.Idempotent;
import com.example.sync.core.SyncHandler;
import com.example.sync.entity.ProductDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 商品同步处理器(实现通用接口,仅写商品同步逻辑)
*/
@Slf4j
@Component
public class ProductSyncHandler implements SyncHandler<ProductDO> {
@Override
@Idempotent(prefix = "product_sync_", expireTime = 300)
public void syncToTarget(List<ProductDO> dataList) {
// 商品业务的同步逻辑:调用Feign接口同步到商城服务(示例)
log.info("同步{}条商品数据到商城服务:{}", dataList.size(),
dataList.stream().map(ProductDO::getProductNo).toList());
// 实际代码:productFeignClient.syncProductData(dataList);
}
@Override
public String getBizCode() {
return "product_sync";
}
}
商品Mapper(ProductMapper)
less
package com.example.sync.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sync.entity.ProductDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductMapper extends BaseMapper<ProductDO> {
List<ProductDO> selectWaitSyncProduct(@Param("lastId") Long lastId, @Param("batchSize") Integer batchSize);
int batchUpdateSyncStatus(@Param("ids") List<Long> ids, @Param("syncStatus") Integer syncStatus);
}
商品Mapper XML(ProductMapper.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sync.mapper.ProductMapper">
<select id="selectWaitSyncProduct" resultType="com.example.sync.entity.ProductDO">
SELECT id, product_no, product_name, price, create_time, sync_status
FROM product_table
WHERE id > #{lastId} AND sync_status = 0
ORDER BY id ASC LIMIT #{batchSize}
</select>
<update id="batchUpdateSyncStatus">
UPDATE product_table SET sync_status = #{syncStatus}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
2.2.8 控制器(通用接口)
kotlin
package com.example.sync.controller;
import com.example.sync.core.GenericSyncTemplate;
import com.example.sync.core.SyncContext;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 通用同步控制器(所有业务复用同一个接口)
*/
@RestController
@RequestMapping("/sync")
@RequiredArgsConstructor
public class SyncController {
private final GenericSyncTemplate genericSyncTemplate;
/**
* 启动/恢复任意业务的同步任务
* @param bizCode 业务编码(order_sync/product_sync)
* @return 响应信息
*/
@GetMapping("/start/{bizCode}")
public String startSync(@PathVariable String bizCode) {
try {
// 构建同步上下文(仅需传入业务编码,其他用默认配置)
SyncContext context = new SyncContext().setBizCode(bizCode);
genericSyncTemplate.startOrResumeSync(context);
return "业务[" + bizCode + "]同步任务启动/恢复成功";
} catch (Exception e) {
return "业务[" + bizCode + "]同步任务启动/恢复失败:" + e.getMessage();
}
}
}
2.2.9 启动类
kotlin
package com.example.sync;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient // Nacos注册
@EnableFeignClients // OpenFeign
@MapperScan("com.example.sync.mapper") // MyBatis-Plus扫描
public class SyncResumeGenericApplication {
public static void main(String[] args) {
SpringApplication.run(SyncResumeGenericApplication.class, args);
}
}
2.3 测试验证
-
前置准备:
-
创建MySQL数据库
sync_db,执行以下建表语句:sql-- 同步任务表 CREATE TABLE sync_task ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_name VARCHAR(100) NOT NULL, task_code VARCHAR(50) NOT NULL UNIQUE, source VARCHAR(200) NOT NULL, target VARCHAR(200) NOT NULL, last_break_point BIGINT DEFAULT 0, status TINYINT DEFAULT 0 COMMENT '0-未开始 1-同步中 2-已完成 3-异常', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 订单表 CREATE TABLE order_table ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(50) NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, sync_status TINYINT DEFAULT 0 COMMENT '0-未同步 1-已同步' ); -- 商品表 CREATE TABLE product_table ( id BIGINT AUTO_INCREMENT PRIMARY KEY, product_no VARCHAR(50) NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, sync_status TINYINT DEFAULT 0 COMMENT '0-未同步 1-已同步' ); -
插入测试数据:
sql-- 订单同步任务 INSERT INTO sync_task (task_name, task_code, source, target) VALUES ('订单数据同步', 'order_sync', 'order_table', 'warehouse_order'); -- 商品同步任务 INSERT INTO sync_task (task_name, task_code, source, target) VALUES ('商品数据同步', 'product_sync', 'product_table', 'mall_product'); -- 测试订单数据 INSERT INTO order_table (order_no, amount) VALUES ('ORDER001', 100.00), ('ORDER002', 200.00), ('ORDER003', 300.00); -- 测试商品数据 INSERT INTO product_table (product_no, product_name, price) VALUES ('PROD001', '手机', 2999.00), ('PROD002', '电脑', 5999.00); -
启动Nacos、Redis、MySQL,确保服务注册成功;
-
-
测试步骤:
- 调用订单同步接口:
http://127.0.0.1:8080/sync/start/order_sync,验证断点续传逻辑; - 调用商品同步接口:
http://127.0.0.1:8080/sync/start/product_sync,验证商品数据同步; - 手动中断服务后重启,再次调用接口,验证从断点位置继续同步;
- 调用订单同步接口:
3. 总结
关键点回顾
-
解耦核心 :通过
SyncDataSource和SyncHandler抽象业务差异化逻辑,GenericSyncTemplate封装通用断点续传逻辑,实现"通用逻辑复用,业务逻辑定制"; -
扩展便捷:新增业务(如用户数据同步)时,仅需:
- 定义业务实体(UserDO);
- 实现
SyncDataSource<UserDO>和SyncHandler<UserDO>; - 配置同步任务(无需修改通用代码);
-
核心保障:保留分布式锁(防并发)、幂等(防重复)、断点持久化(Redis+MySQL)、批次同步(防高并发超时)等核心能力,适配分布式高并发场景。
生产扩展建议
- 动态配置:通过Nacos配置中心动态调整不同业务的批次大小、重试次数;
- 监控告警:对接Prometheus+Grafana监控同步进度、断点、失败次数,异常时触发告警;
- 异步执行:通过Spring Task/XXL-Job实现定时同步,避免接口阻塞;
- 多数据源适配:扩展
SyncDataSource支持MySQL/ES/MongoDB等不同数据源。