一、 背景

  基于成本和数据安全的考虑,需要将XX的存储数据源从Oracle切成MySQL。券系统作为XX中心最底层系统,不仅数据极其重要而且数据量庞大,如何设计一套完整迁移方案保证准确无误的实现数据(历史数据和增量数据)迁移且让上层业务无感知成为挑战。基于以上分析可以看出有以下两个问题需要解决:

  • 历史数据如何同步?
  • 如何平滑切换让业务无感知?

二、迁移方案

1. 历史数据

  历史数据同步需借助BDP(Big Data Process,BDP)提供的ETL工具实现,如果目标库为单库则直接利用ETL完成。如果目标库为分片库,则需要先通过ETL将数据推送到kafka,应用服务消费kafka后再插入或更新到分片库。                      

    数据同步完成后需验证数据正确性,先把新老库中的数据同步到hive库,然后创建对比hql脚本进行对比,再通过对比结果判断是否完全达成一致。如果是历史数据不一致则再同步历史数据,如果是增量数据不一致则检查双写是否成功。

2.增量数据

   ETL数据同步只能解决历史数据迁移,增量数据需要通过双写实现,经过分析有以下两种双写方案可供选择:
  
  双写方案一通过在原来写入Oracle的mapper上添加自定义注解,然后通过aop拦截自定义注解,在切面中加载写入MySQL的新mapper,通过反射调用实现写入mysql。
  优点:调用方无代码污染;
  缺点:mybatis-plus自动装配时只能配置单数据源,需要改造代码定义双数据源,改造成本大风险高;
  

  双写方案二在写入Oracle的mapper处通过开关控制调用写入MySQL的mapper,从而实现双写两个数据源。
  优点:简洁;
  缺点:在原有业务调用处需添加新调用逻辑;
  
  经过以上分析选择双写方案二,梳理出整体的迁移方案流程如下:

三、收益

  通过双写加ETL同步迁移Oracle表格21张,共计数十亿条数据,为XX券系统下线Oracle做好铺垫。

四、采坑总结

1、采坑1、事务导致切换数据源失败

1.1、现象
代码逻辑如下:

@Transactional(rollbackFor = {Exception.class})

public Result<Object> saveActivityEventOut(ActivitySavaOutDto activitySavaOutDto){

//省略非关键代码

ScsActivityEvent activityEvent = activityToChange(activitySavaOutDto); scsActivityEventMapper.insert(activityEvent); if(SystemProperty.writeCouponEvent2mysql()){ try { scsBaseActivityEventMapper.insertSelective(activityEvent); }catch (Exception e){ log.error("write2mysql error,param:{}",JSONObject.toJSONString(activityEvent),e); } } //省略非关键代码

目的:此处通过给scsActivityEventMapper和scsBaseActivityEventMapper标注不同的数据源,通过DynamicDataSrouce切换数据源双写Oracle和MySQL。

1.2、定位过程

  • 检查xml中sql语句,未发现任何错误;
  • 怀疑是sql中插入条件判断有误,尝试改成直接插入,同样报错;
  • 尝试精简sql,只保留id

  

此处逻辑是先执行scsActivityEventMapper.insert(activityEvent)插入oracle,然后返回自增id填充到activityEvent,再将此id插入mysql,怀疑mysql是不是已经存在此id,因此debug代码查看:

  可以看到MySQL不存在此数据但依然报重复key。

  • 再次检查报错信息
cause: java.sql.SQLException: ORA-00001: 违反唯一约束条件 (SCS.SYS_C0013672)

  这几个关键字引起注意,目前仅插入id但MySQL并不存在275483这个Id,难道是数据源切换失败写入了Oracle?

  • 验证

      基本实锤切换数据源失败导致此问题,将本应写入mysql的数据再次写入Oracle。回到代码发现此处与其他地方不同的是该方法上标注事务注解@Transactional(rollbackFor = {Exception.class}),因此猜想肯定是事务造成数据源的切换失败,下面深入源码一探究竟。

1.3、源码分析

1.3.1、动态数据源原理

  可以看到在执行mapper接口调用时解析当前注解得到数据源标识,然后再通过数据源获取连接时查询线程变量栈顶元素得到当前数据源标识,再从dataSrouceMap中获得数据源。如果获取不到,会将设置成primary的数据源作为默认数据源。

  线程变量LOOKUP_KEY_HOLDER结构为什么用Dequeue?
  为了支持嵌套切换,如ABC三个service都是不同的数据源,其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换形成链。传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。

1.3.2、aop和事务集成

  事务和aop的集成可以拆解成以下两步:

  步骤1:先通过TransactionAutoConfiguration配置导入BeanFactoryTransactionAttributeSourceAdvisor通知器:;

  步骤2:在容器生成ActivityEventManager bean时通过beanPostProcessor后置处理器生成代理Bean。先找出所有Advisor,然后遍历所有的Advisor判断是否与当前bean匹配,如果匹配则加入通知器数组中,以此作为拦截器生成代理对象。

  可见在调用此方法时会被TransactionInterceptor中invoke方法拦截。

1.3.3、事务和mybatis集成

TransactionSynchronizationManager线程变量:

ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>(“Transactional resources”);

ThreadLocal actualTransactionActive =new NamedThreadLocal<>(“Actual transaction active”);

ThreadLocal<Set> synchronizations =new NamedThreadLocal<>(“Transaction synchronizations”);

  上述逻辑可以拆解成以下四个步骤:
  步骤1: 在执行带事务注解方法前先执行增强逻辑,通过DruidDataSource#getConnection()获取连接并绑定在resources中,同时将actualTransactionActive设置成true;

  步骤2:sql执行时调用代理方法MybatisMapperProxy#invoke,被SqlSessionInterceptor拦截获取SqlSession,根据SqlSessionFactory优先从resources 查询,如果存在则返回;如果不存在则跳转步骤3;

  步骤3:sessionFactory开启sqlSession,根据Exector、SpringManagedTransactionFactory生成DefaultSqlSession。如果actualTransactionActive为true,以sessionFactory作为Key,sqlSession作为value注册到resources上,同时将事务信息注册到synchronizations ;

  步骤4:获取到sqlSession后即可执行sql语句,如果出现在执行业务方法时出现失败(包含sql)则在TransactionAspectSupport执行回滚;

  通过上述逻辑保证同一个线程同一个事务里面是同一个连接。

1.4、失败原因总结及解决办法

  执行带事务注解saveActivityEventOut方法时会被TransactionInterceptor拦截,此时未调用mapper接口,则获取默认oracle数据源并绑定在当前线程的resources线程变量上并激活当前事务(actualTransactionActive设置true)。
  执行scsActivityEventMapper.insert(activityEvent)时,生成sqlSession时会根据resources中的数据源获取连接生成,然后以sqlSessionFactory作为Key,sqlSession作为value保存到resource上;
  继续执行scsBaseActivityEventMapper.insertSelective(activityEvent),在获取sqlsession时优先从resource中查询,此时已经存在则直接返回,sqlSession中数据源仍然是oracle的数据源,切换失败。

@Transactional(rollbackFor = {Exception.class})
public Result<Object> saveActivityEventOut(ActivitySavaOutDto activitySavaOutDto){//省略非关键代码ScsActivityEvent activityEvent = activityToChange(activitySavaOutDto);scsActivityEventMapper.insert(activityEvent);if(SystemProperty.writeCouponEvent2mysql()){CouponEventBusFactory.build().registerAsynWriteEvent(AsynWriteEventListener.class);CouponEventBusFactory.build().saveCouponAsyncEvent(new CouponAsynEvent(activityEvent));}//省略非关键代码}

  事务是通过将dataSource和sqlSession绑定在线程上,另起线程执行写入mysql,原有线程上下文变量即失效,即会重新生成sqlSession和获取数据源。

2、采坑2、自增主键Id

  为了实现主键id自增,Oracle在插入之前需要通过以下代码实现,但MySQL可以直接将字段设置成autoIncrement即可。

    <selectKey keyProperty="id" resultType="String" order="BEFORE">select SEQ_ACTIVITY_COUPON.nextval from dual</selectKey>

3、采坑3、日期函数不一致

  Oracle使用to_char(created_time, ‘yyyy-MM-dd’),sysdate() ;
  Mysql对应的使用date_format(created_time,’%Y-%m-%d’),now(),curdate(),sysdate()。

3、采坑3、排序方式不一致(特别注意)

  Oracle对字段升序时,null值会放在最后;倒序时,null值会放在最前面;非null值区分大小写排序;
  MySql对字段升序时,null值会放在最前面;倒序时,null值会放在最后面;非null值不区分大小写排序。

4、采坑4、分页不一致

  Oracle分页是依赖ROWNUM的,所以代码中一般都会嵌套2~3层来实现分页;rownum > startNum and rownum <= endNum,rownum默认是从1开始的,其中endNum = startNum + pageSize,startNum和endNum都会根据偏移量改变;
  MySql分页是通用limit实现:limit startNum, endNum,startNum从0开始的,startNum会根据分页起始偏移量改变,endNum = pageSize不会改变。

5、采坑5、日期比较

  Oracle不能直接使用Date类型字段与字符串进行比较;
  MySql可以直接使用Date类型字段与字符串进行比较。

6、采坑6、分组时查询字段必须是分组字段或统计函数字段

  Oracle在使用group by时,select的字段必须是group by中的字段或统计函数字段;
  MySql在使用group by时,select的字段允许为非gooup by中的字段。

7、采坑7、更新语句

  Oracle更新后均返回1;
  MySql更新成功后,如果更新的字段没有发生变化,返回0,否则返回1。

四、 总结

  本文首先分析券基础数据迁移存在的困难和挑战,然后针对问题提出历史数据迁移方案和双写方案,同时对实际迁移中踩过的坑进行总结形成经验。

亿级数据迁移 从Oracle到Mysql相关推荐

  1. 菜鸟积分系统稳定性建设 - 分库分表百亿级数据迁移

    点击上方"服务端思维",选择"设为星标" 回复"669"获取独家整理的精选资料集 回复"加群"加入全国服务端高端社群「后 ...

  2. 亿级数据迁移实战方案,真实案例

    背景: 公司某业务模块的用户通讯记录和通话记录的表,单表记录过亿了,mysql很坚挺好不好!!!! 其中某表是205773235 约2.0亿 另外某表是575213155 约5.7亿 用户数量 约10 ...

  3. MySQL 亿级数据导入导出/数据迁移笔记

    最近MySQL的笔记有点多了,主要是公司Oracle比较稳定维护较少,上周被安排做了一个MySQL亿级数据的迁移,趁此记录下学习笔记: 数据迁移,工作原理和技术支持数据导出.BI报表之类的相似,差异较 ...

  4. MySQL 亿级数据的迁移、清洗、与审计分析

    职业生涯中,曾经有两年多时间从事IT内部审计的工作,需要介入公司大部分的业务系统,主要的数据库为 MySQL,特别是三年期审计,需要统计三年的数据素材(亿级以上).对MySQL的性能有一些自已的理解, ...

  5. MySQL亿级数据数据库优化方案测试-银行交易流水记录的查询

    对MySQL的性能和亿级数据的处理方法思考,以及分库分表到底该如何做,在什么场景比较合适? 比如银行交易流水记录的查询 限盐少许,上实际实验过程,以下是在实验的过程中做一些操作,以及踩过的一些坑,我觉 ...

  6. MySQL 亿级数据需求的优化思路(二),100亿数据,1万字段属性的秒级检索

    最近在研究亿级数据的时候,无意中看到了一个关于写58同城的文章 https://blog.csdn.net/admin1973/article/details/55251499?from=timeli ...

  7. mysql select count 5万条数据很慢_mysql亿级数据数据库优化方案测试银行交易流水记录的查询...

    点击上方△蓝字关注我们 带你征服编程和泡妞两座大山 对MySQL的性能和亿级数据的处理方法思考,以及分库分表到底该如何做,在什么场景比较合适? 比如银行交易流水记录的查询 限盐少许,上实际实验过程,以 ...

  8. MySQL 亿级数据需求的优化思路(一),交易流水记录的查询

    对MySQL的性能和亿级数据的处理方法思考,以及分库分表到底该如何做,在什么场景比较合适? 比如银行交易流水记录的查询 限盐少许,上实际实验过程,以下是在实验的过程中做一些操作,以及踩过的一些坑,我觉 ...

  9. oracle tns 代理配置_OGG实现Oracle到MySQL数据平滑迁移

    技术分享 | 使用OGG实现Oracle到MySQL数据平滑迁移 - 爱可生开源社区​opensource.actionsky.com 一.OGG概述 OGG全称为Oracle GoldenGate, ...

最新文章

  1. 硕博就业:引进博士40万;硕士20万;本科15万
  2. Hadoop搭建完全分布式
  3. Haar-like矩形特征计算详解
  4. SendKeys中特殊字符的键代码
  5. 机器学习(三)聚类深度讲解
  6. outdated: 3.Adding Color
  7. android 6.0 dm-verity技术,Android 7.0安全性大幅提升,要求严格强制执行验证启动
  8. const char * array[]和char * const array[]
  9. Windows Server入门系列之三 硬盘分区
  10. ie型lfsr_线性反馈移位寄存器原理与实现 - 全文
  11. MES系统在注塑行业中的应用
  12. win7无法连接打印机拒绝访问_win7系统共享打印机拒绝访问的完美解决方法
  13. 张宇用的【数学公式壁纸】大公开!
  14. Session注销方式
  15. Mac使用Homebrew极速安装启动RabbitMQ一把梭
  16. 浅谈getaddrinfo
  17. _motz_ forum.php_开启模块化大门 moto z体验
  18. python飞机大战任务报告_Python飞机大战实战项目案例
  19. KIS旗舰版加载插件提示不符合格式
  20. 15.windbg-dds、dps、dqs、PE文件解析

热门文章

  1. Tensorflow C++ API 生成复数算子
  2. C语言毕业啦!!!(内附超详细知识框架图)
  3. 程序人生,一首诠释我们程序员生活的歌!
  4. Linux命令查看运行服务,在Linux系统中查看所有正在运行的服务
  5. 项目进度管理:控制进度--监控过程组
  6. Android触摸事件分发
  7. XP计算机管理员不见了,xp系统管理员账户消失了怎么处理
  8. 掘金 Android 文章精选合集
  9. A Survey on Deep Learning for Named Entity Recognition论文笔记
  10. 【原生HTML+SpringBoot】电子病历编辑器源码