大数据迁移
旧系统与旧库设计不合理,已经不足以处理对日益增多的数据量,需要将系统与数据迁移到新的环境;
涉及系统:
- 旧系统:原本的业务逻辑,提供数据迁移接口;
- 新系统:与旧系统业务逻辑基本相同,有部分优化,增删改查时对数据库进行了分库分表,比老系统多了接收迁移数据的接口;
- 任务调度系统:负责以划分好的纬度来启动全量数据迁移或增量数据迁移,负责灰度开关,维护表如下:
- 区县表:字段:id,区县行政编码,区县状态(如00代表未迁移,01代表迁移中,02代表成功,10代表开启灰度测试,2x代表各种失败情况),写库使用情况(只写旧还是双写还是…),读库使用情况(读新库还是老库);
- 任务表:字段:id,区县行政编码,迁移类型(全量,增量),迁移开始时间,任务状态;
河北省免费避孕药具综合管理服务平台
迁移背景:
- 用户表2000万以上用户,出入库业务表各约1000万数据,用户药具领取表约5000万数据,且都没有分库分表;
数据迁移方案:
- 数据迁移以区(县)为纬度进行迁移,如河北省石家庄市裕华区,或河北省邢台市沙河市;
- web层收到用户请求会先拿请求区划调用任务调度系统来判断是否需要开启双写,双写作用为为增量迁移兜底,具体为收到写请求先判断该区划迁移状态:
- 如未迁移则只写老库;
- 如迁移中或迁移完成但未开启灰度则同步写老库异步写新库(新库写入结果不影响响应结果);
- 如开启灰度则同步写新库老库(都成功=成功,任一失败=失败);
- 灰度测试通过,可修改为同步写新库异步写老库(老库写入结果不影响响应结果),根据过往灰度稳定性也可以改为只写新库;
- 由任务调度系统发起迁移任务,调度系统维护各区县当前状态及任务状态;
- 发起迁移任务后,调度系统调用老系统的数据迁移接口,并提供迁移的区划。同时将该区划在调度系统区县表中的状态改为迁移中,且在任务表中写入一条任务,并记录全量迁移开始时间;
- 老系统收到数据迁移请求,开始扫表,每次500条数据调用新系统的数据写入接口,如插入失败则记录失败的任务表表名及失败的id范围,对失败的任务再人为进行处理;
- 老系统全量迁移后,开始增量迁移,具体流程为:老系统全量迁移完成->调用任务调度系统,通知全量迁移成功->任务调度系统收到后,将任务表任务状态修改为成功,且自动调用老系统增量迁移接口,以全量迁移开始时间为节点,只迁移该时间节点后新插入的数据,并在DB新增一条增量迁移任务;
- 增量迁移完成后,老系统调任务调度系统接口通知成功,任务调度系统将任务改为成功并修改该区划迁移状态;
- 找合适时间开启已经迁移完成区划的灰度测试;
灰度测试方案:
- 当某一区划数据已经迁移完毕,可以在任务调度系统中开启灰度测试,将这部分区划的请求转移到新系统;
- web层收到用户请求会先拿请求区划调用任务调度系统来判断是否开启灰度测试,如返回未开启则web层调老系统服务读老库,如开启则web层调新系统服务读新库;
- 灰度测试时发现新系统出现问题,可随时关闭灰度将请求切回老系统,再对新系统进行调整;
无特殊情况灰度无需关闭,老系统所有数据迁移完成且新系统稳定后,可直接修改web层逻辑只使用新系统;
设计高并发下库存数据安全解决方案
主要问题是高并发场景下的库存扣减如何保证不超扣及数据不一致问题;
使用redis+DB的方式保证扣减不出现安全问题;库存超扣问题
redis端使用Lua脚本对redis进行操作,扣库存之前先判断扣完本次库存是否为负数;
DB端读查到库存先判断扣完本次库存是否为负,如果可以正常扣减,不要在内存中计算后再更新数据库,直接在数据库中update table set stock = stock - 扣减数量;
数据不一致问题
对库存的扣减和添加(非初始化添加)操作,直接在redis中进行,异步扣减数据库库存;
redis端使用Lua脚本对库存进行扣减;
- DB端使用update table set stock = stock - 扣减数量 where stock >= 扣减数量 and stockId = ?解决数据不一致问题(乐观锁模式);
redis与DB的双写一致性:
- 库存发生改变时,如果更新redis,会涉及到反序列化序列化问题,并且有更大几率出现脏数据,如A写库,B写库,B更新缓存,A更新缓存;所以对redis只删除不更新;
- 先删缓存和后删缓存出现脏数据的情况:
- 如果先删缓存:
- A删缓存;
- B查询到缓存失效,查询DB;
- B将查询到的旧值写入缓存;
- A更新DB成功;
- 如果先写库:
- A写库;
- B查询缓存,且缓存刚好失效,查到旧值;
- A写库完毕,删缓存;
- B将脏数据写入缓存;
- 可以看到,先写库时除非查询比更新还慢,且缓存还刚好失效的情况才会出现脏数据,概率极低,所以先写DB再删缓存
异步扣减DB库存
通过Lua脚本扣减redis数据成功后,发送库存扣减消息,发送消息需要得到发送结果,发送失败重试,最终失败要记录在DB,消费者收到扣减库存消息后,可以批量拉取消息,对同一条库存的多次扣减和增加可以在内存中计算最终结果,再更新到DB;初始化内存数据时如何写缓存
同步写缓存
初始化一条库存信息到DB时同步写缓存,都成功响应成功,任意一方失败则初始化库存失败;
这种情况下读取缓存数据时,如果不存在就报错;异步及读取时写缓存
初始化一条库存信息到DB时不关心缓存是否写成功,后续通过定时任务将新初始化的库存信息写入缓存;
如果在初始化库存且定时任务还未将新初始化的库存写入缓存时对redis读取发现读不到,可以加分布式锁从库存中读取数据,并初始化到redis中;
为防止重复从数据库中读取,可使用双重检查锁方式读取;改写sharding-JDBC4.x版本源码,解决使用不便等问题
4.x版本效率更高,启动和响应更快,项目中后期被要求更新到4.x版本,但对强制路由不友好,所以对代码进行改写,改写主要在sql路由这一阶段,获取分库算法前先看本次是否强制路由,如果强制路由则将路由算法直接改为强制路由(由hintmanager这个threadlocal判断),然后正常调执行过程,sharding会自动选择强制路由执行器执行;需要注意两个点,一个是改时需要加锁,保证执行过程中不被其他线程影响,二是执行完后要将路由算法改回,避免该表以后都是强制路由异构索引表
使用sharding-jdbc分库分表,当需要使用非分表键查询但又不希望全库扫描时,可以使用根据条件查询异构索引表找到条件对应的分表键,再定位到具体表的方式来实现;
为my_test表创建异构索引表举例说明传统、新型异构索引表的结构与区别;传统异构索引表
业务表结构
业务表内容
code异构索引表结构
create_time异构索引表结构
优点:
- 如果先删缓存:
表较小,且无需分库分表;
缺点:
缺点:
- 相对来说表较大,需要分库分表;
多级缓存方案
Caffeine使用:
redis数据更新后如何通知本地
redis和本地缓存除具体数据额外存储一个时间戳,每次查询本地缓存时先查询下本地该数据时间戳与redis上时间戳是否一致,不一致认为数据有过变更,从redis读取最新数据即可;
本地和redis除基本数据外额外存时间戳,key为”$version_” + key,value为时间戳;
大数据量批量导入方案
分布式情况下id冲突
如何判断OOM原因及如何调优
生产环境参数一定要配置当出现OOM时生成那一时刻的dump文件;
拿出现OOM生成的dump文件可以使用jconsole或者jvisualvm工具分析dump文件,查看那个对象大或多,再做具体分析;
如果多数对象都是实例化->使用->销毁流程,并不会进入老年代,可以考虑扩容新生代缩容老年代;
如果线上内存确实小,可以考虑扩内存;
如果线上内存大,可以尝试更换垃圾回收器,G1比CMS性能要好(提升有限);
如何确保大数据量导入不会发生OOM
角色(分角色目的是使各个角色独立,防止因某一环节慢拖垮整个服务):
- 接收文件端(生产);
- 读取文件端(消费、生产);
- 写入DB端(消费);
方案:
- 用户上传文件到接收文件端,收到文件后立即上传到文件服务器,并发送消息通知读取文件端,告知有文件上传成功及文件路径;
- 读取文件端收到了消息后,将文件从文件服务器下载,再分条有序写入redis,写入完成后发送该任务写入redis成功的消息,为方便异步写入DB可将成功消息拆分如1000条数据发一条消息,同时在redis存入一个总任务数和已完成数,方便前端回调查询进度;
- 一定要分次读不要直接将整个文件加载进内存,避免OOM;分次读怎么读?分次读不把整个文件加载进内存吗
- 先写入redis是因为分次写入DB响应时间较慢且代价较高;
写入DB端消费到写入redis成功的消息,根据消费到的消息决定具体将哪部分存入mysql,每消费完成一个消息,将redis中已完成任务数更新,方便前端回调查询进度;
大数据量批量导出方案
前端
异步导出数据,点击导出后可拉起一个进度条,进度条可开可关,持续访问后端接口获取导出进度;
后端
角色(分角色目的是使各个角色独立,防止因某一环节慢拖垮整个服务):
创建任务端(生产);
- 执行任务端(消费、生产);
- 数据汇总端(消费);是否要多消费者汇总,能否多消费者操作一个文件?如何在线写文件?
方案:
- 创建任务端收到导出请求,将要导出的行拆分成多个任务发送消息,如拆分成1000行数据发一条消息,并且将总任务数和已完成任务数存放在redis,且在OSS文件服务器创建该导出文件;
- 执行任务端拉取到消息,开始从DB读取数据并存入redis,并且存入成功的消息;
- 数据汇总端拉取到执行任务端将数据存入redis成功的消息后开始从redis读取已存入的消息并写入OSS文件服务器上的文件,写入成功后更新redis中该任务完成的进度;
dubbo负载均衡策略
springboot自动装配






