将一个数据库表的数据同步至另一数据库表中是开发过程中必不可少的业务需求,大部人在遇到此类问题时就会选择复制表结构,表对表插入数据,当然这不乏是个很方便快捷的方式,但是如果后期需要更多的这样处理就很麻烦,所以在以上的情况下实现任务调度不为一个非常好的方法。下面本菜菜鸟就以PG数据库下两个不同的库之间数据同步为例,展开共享自己的小收获。
目录
此功能是在XxlJob中实现
时间紧,任务重!直接就开始了,谨记不同数据源之间同步最重要最主要的是配置
一、数据源的配置
XxlJob中,一般主要两部分,一是服务启动模块,二是具体实现模块,服务驱动实现,在实现模块中配置数据源。
这里有一点很重要,非常值得注意,一般配置数据库链接为url,但是但是但是多数据源用jdbc-url,否则配置的可能就会不生效。
datasource:
dynamic:
primary: pg1 #设置默认的数据源或者数据源组,默认值即为pg1
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
pg1:
jdbc-url: jdbc:postgresql://xx.xx.xx.xx:xxxx/pg1?currentSchema=pg1&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
pg2:
jdbc-url: jdbc:postgresql://xx.xx.xx.xx:xxxx/pg2?currentSchema=pg2&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始化大小,最小,最大
initial-size: 5
max-active: 30
min-idle: 5
# 获取数据库连接等待的超时时间
max-wait: 3000
max-open-prepared-statements: -1
# 配置多久进行一次检测,检测需要关闭的空闲连接 单位毫秒
time-between-eviction-runs-millis: 60000
# 配置连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
# 配置连接在池中的最大生存时间
max-evictable-idle-time-millis: 400000
# 系统启动时通过该sql语句验证数据库是否可用,如果不配置validationQuery,则下面三项无效
validation-query: SELECT 1
# 启用空闲连接检测,以便回收
test-while-idle: true
# 从连接池获取连接时,是否检测连接可用性,开启性能会有些许影响
test-on-borrow: false
# 释放连接到连接池时,是否检测连接可用性,开启性能会有些许影响
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
然后进行配置文件处理,这目的主要用于扫描相应的mapper
pg1Config
@Configuration
// 配置mybatis的接口类放的地方
@MapperScan(basePackages = "com.app.task.relations.mapper.pg1", sqlSessionFactoryRef = "pg1SqlSessionFactory")
public class Pg1Config {
// 将这个对象放入Spring容器中
@Bean(name = "pg1DataSource")
// 表示这个数据源是默认数据源
@Primary
// 读取application.properties中的配置参数映射成为一个对象
// prefix表示参数的前缀
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.pg1")
public DataSource getPg1DateSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "pg1SqlSessionFactory")
// 表示这个数据源是默认数据源
@Primary
// @Qualifier表示查找Spring容器中名字为pg1DataSource的对象
public SqlSessionFactory pg1SqlSessionFactory(@Qualifier("pg1DataSource") DataSource datasource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:mapping/pg1/*Mapper.xml");
bean.setMapperLocations(resources);
return bean.getObject();
}
@Bean("pg1SqlSessionTemplate")
// 表示这个数据源是默认数据源
@Primary
public SqlSessionTemplate pg1SqlSessionTemplate(
@Qualifier("pg1SqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
pg2Config
@Configuration
@MapperScan(basePackages = "com.app.task.relations.mapper.pg2", sqlSessionFactoryRef = "pg2SqlSessionFactory")
public class Pg2Config {
@Bean(name = "pg2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.pg2")
public DataSource getPg2DateSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "pg2SqlSessionFactory")
public SqlSessionFactory pg2SqlSessionFactory(@Qualifier("pg2DataSource") DataSource datasource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/pg2/*.xml"));
return bean.getObject();
}
@Bean("pg2SqlSessionTemplate")
public SqlSessionTemplate pg2SqlSessionTemplate(
@Qualifier("pg2SqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
}
以上的配置完成之后就可以实现自己的需求任务,在相应的服务内创建自己任务,启动服务,利用debug日志检查是否成功。
@Slf4j
@Component
public class SyncJob {
@Autowired
Pg1Service pg1Service;
/**
* pg同步
* @param param
* @return
* @throws Exception
*/
@XxlJob("pgSyncJob")
public ReturnT<String> pgSyncJob(String param) throws Exception {
pg1Service.saveSyncData();
return ReturnT.SUCCESS;
}
}
/**
* 服务启动
*/
@SpringBootApplication(scanBasePackages = {"com.app.job.executor","com.app.task.relations"})
public class RelationsExecutorApplication {
@PostConstruct
void setDefaultTimezone() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); }
public static void main(String[] args) {
SpringApplication.run(RelationsExecutorApplication.class, args);
}
}
二、 实现调度
菜菜鸟实现同步逻辑:删——查——插
1、Service
Service
1)、pg1Service
数据库pg1里面的pg处理接口
/**
* 保存DATA
*/
public interface Pg1Service {
void saveSyncData();
void deletePg1OldData();
void savePg1NewData(List<PgVO> pgVOS);
}
2)、pg2Service
数据库pg2里面的pg处理接口
public interface Pg2Service {
Cursor<PgVO> getPg2Data();
}
ServiceImpl
1)、pg1ServiceImpl
个人想法简单粗暴,删原来的,插最新的。
@Slf4j
@Service
public class Pg1ServiceImpl implements Pg1Service {
@Autowired
private Pg1Mapper pg1Mapper;
@Autowired
private Pg1Service pg1Service;
@Autowired
private Pg2Service pg2Service;
@Override
public void deletePg1OldData() {
pg1Mapper.deletePg1OldData();
}
@Override
public void savePg1NewData(List<PgVO> pgVOS) {
pg1Mapper.savepg1NewData(pgVOS);
}
/**
* 删除原表数据
* 插入 异步处理
* 保存新表
* 失败回滚,重新调度
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveSyncData() {
log.info("Start scheduling:");
// 删除原有数据
pg1Service.deletePg1OldData();
// 保存数据
int size=1000;
Cursor<PgVO> odsData = pg2Service.getOdsPg2Data();
// TODO 改进:根据城市同步 更新区县及城市编码
// 插入
List<PgVO> pgVOS = new ArrayList<>();
odsData.forEach(x -> {
pgVOS.add(x);
if(pgVOS.size()>=size){
log.info("SAVE DATA:{}", pgVOS.size());
pg1Service.savePg1NewData(pgVOS);
// 清理集合
pgVOS.clear();
}
});
// 最后数据量不足1000
if(CollectionUtil.isNotEmpty(pgVOS)){
log.info("Less than 1000 pieces of data:{}",pgVOS.size());
pg1Service.savePg1NewData(pgVOS);
}
// 核对 更新区县及城市编码
}
}
2)、pg2ServiceImpl
@Slf4j
@Service
public class Pg2ServiceImpl implements Pg2Service {
@Autowired
private Pg2Mapper mapper;
@Override
@Transactional
public Cursor<PgVO> getPg2Data() {
return mapper.getPg2Data();
}
}
2、Mapper
Mapper
1)、Pg1Mapper
@Mapper
public interface Pg1Mapper {
void savePg1NewData(@Param("list") List<PgVO> list);
void deletePg1OldData();
}
2)、Pg2Mapper
@Mapper
public interface Pg2Mapper {
Cursor<PgVO> getPg2Data();
}
Xml
1)、Pg1Xml
<delete id="deletePg1Data">
TRUNCATE table pg.pg1;
</delete>
<insert id="savePg1NewData"
parameterType="com.topprism.task.relations.vo.PgVO">
insert into pg.pg1(
province,
city,
district
) values
<foreach collection="list" item="item" separator=",">
(
#{item.province},
#{item.city},
#{item.district}
)
</foreach>
</insert>
1)、Pg2Xml
<select id="getPg2Data" parameterType="com.topprism.task.relations.vo.PgVO" resultSetType="FORWARD_ONLY" fetchSize="1000">
SELECT
province,
city,
district
FROM
pg.pg2
</select>
三、总结
此任务调度过程最重要的数据源的配置,可能一个小小的空格就会发现注入不进去,导致扫不到包。在调度过程中不能改变原有业务需要的单一数据源,所以配置之处就需考虑到,启动服务时不用扫描mapper,只需各个业务需求需要什么数据源,就自行扫描自己的mapper。
多数据源配置根据业务需求配置,上面的菜菜鸟只需要两个数据源,如果所需较多数据源,可以实现多数据源动态切换,菜菜鸟正在努力