1、背景
daily与线上表结构,索引不一致场景梳理
巡检:巡检业务差异表,表名不一致,已经修改
案例:
alter table finance_sub_order_info modify total_receive decimal(19,3) default 0.000 not null comment '待删除'; alter table finance_sub_order_info modify total_pay decimal(19,3) default 0.000 not null comment '待删除'; alter table finance_sub_order_info modify total_cost decimal(19,3) default 0.000 not null comment '待删除'; alter table finance_fare_info modify img text null comment '附件图片'; alter table finance_bill drop column bill_amount; alter table finance_sub_order_settlement drop column price; alter table finance_sub_order_settlement drop column business_types; alter table finance_sub_order_settlement drop column invalid_state; alter table finance_sub_order_settlement drop column record_user; alter table finance_sub_order_settlement drop column can_settlement; alter table finance_sub_order_settlement drop column create_user;
车队财务:
finance_sub_order_info
- 问题1:三个字段待删除
total_receive
decimal(19,3) NOT NULL DEFAULT ‘0.000’ COMMENT ‘总应收’,total_pay
decimal(19,3) NOT NULL DEFAULT ‘0.000’ COMMENT ‘总应付’,total_cost
decimal(19,3) NOT NULL DEFAULT ‘0.000’ COMMENT ‘总成本’,
- 先将这几个字段设置为待删除
finance_fare_info
- 问题1:
- 问题2:将AttachmentId设置为必填,默认值为0
- 然后排查下代码中需要做兼容的地方
- 问题3:finance_fare_info表
confirm_time datetime null comment '确认时间', confirm_user bigint null comment '确认人',
- 这两字段在线上已经被删除了,但是daily环境还存在,需要排查
- 新功能
- 问题4:发票号码
invoice_code varchar(128) null comment '发票号码数组,以逗号分割',
- 这字段在线上已经被删除了,但是daily环境还存在,需要排查
- 新功能
- 问题5:协作状态
team_state int default 0 not null comment '协作状态 0非协作费用 1协作费用',
- 这字段在线上已经被删除了,但是daily环境还存在,需要排查
- 新功能
finance_bill
- 问题1:账单总额字段,在线上存在,但是在daily环境不存在
bill_amount decimal(19,3) NOT NULL DEFAULT '0.000' COMMENT '账单金额',
- 应该被删除
- 问题2:这几个字段在daily存在
settlement_owned_type tinyint default 0 not null comment '1 自营 2-外协', settlement_entity_id bigint default 0 not null comment '结算实体id', settlement_entity_classify tinyint default 0 not null comment '结算实体类型,1-司机 2-企业id 3-车队id ',
finance_sub_order_settlement
- 问题1:费用合计字段,在线上存在,但是在daily环境不存在
price decimal(19,3) NOT NULL DEFAULT '0.000' COMMENT '费用合计'
- 问题2:以下5字段,在线上存在,但是在daily环境不存在
- business_types varchar(60) null comment ‘业务类型,送重、门到门、提重、送空、提空、带货、运费’,
- invalid_state int(10) default 0 not null comment ‘是否作废或删除,0:正常订单,1:作废订单,2:删除订单’,
- record_user bigint null comment ‘录单员’,
- can_settlement tinyint null comment ‘是否可结算’,
- create_user bigint not null comment ‘创建人’,
- 需要删除这批数据 已经上线的功能
- 问题3:索引不一致
-- daily create index idx_tenantid_settlementtype_invalidstate on falcon_convoy.finance_sub_order_settlement (tenant_id, settlement_type); -- 线上 create index idx_tenantid_settlementtype_invalidstate on falcon_convoy.finance_sub_order_settlement (tenant_id, settlement_type, invalid_state); -- todo 索引名称需要修改
2、存在问题的场景
场景1:索引冲突
两索引tenantId字段重合了,下面这个索引做删除处理
场景2:索引不一致
-- daily create index idx_tenantid_settlementtype_invalidstate on falcon_convoy.finance_sub_order_settlement (tenant_id, settlement_type); -- 线上 create index idx_tenantid_settlementtype_invalidstate on falcon_convoy.finance_sub_order_settlement (tenant_id, settlement_type, invalid_state);
场景3:某些字段在线上存在,但是在daily环境不存在
场景4:某些字段在线上已经被删除了,但是daily环境还存在
3、技术方案
3.1、页面如下
3.2、整体流程图
目标:避免正式环境与测试环境数据库/表、列结构不一致带来问题。
- 检测daily环境和线上环境表结构是否一致,不一致的数据记录起来,并推送钉钉告警
步骤1:数据获取
- 上游:线上环境库+表
- 下游:daily环境库+表
- 频率:一周两次即可
步骤2:数据比对
- 1、线上存在,daily不存在,场景可能是daily环境发生了不兼容的升级改造,消息推送即可;
- 2、线上不存在,daily存在,场景可能是daily在新增了表,可以将表名存放到redis中,7天后,线上还不存在该表,消息推送;
- 3、都存在,但是不一致,场景是索引遗漏、comment该了、字段名改了、字段类型改了,立即消息推送。
步骤3:差错处理
- 不一致的数据记录起来,并推送钉钉告警(对接钉钉机器人)
3.3、数据获取
卡点1:daily环境与线上环境网络不通
- 解法:将ecs部署到control区
卡点2:多数据源配置,application.yml文件中配置
datasource: driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource first: url: ${huxun.datasource.url} username: ${huxun.datasource.username} password: ${huxun.datasource.password} second: url: ${huxun.datasource.daily.url} username: ${huxun.datasource.daily.username} password: ${huxun.datasource.daily.password}
多数据源具体实现:
1、定义一个动态数据源: 继承AbstractRoutingDataSource 抽象类,并重写determineCurrentLookupKey()方法
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType(); return dataBaseType; } }
2、创建一个切换数据源类型的类
public class DataSourceType { public enum DataBaseType { //默认数据库 FIRST, SECOND; } // 使用ThreadLocal保证线程安全 private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>(); // 往当前线程里设置数据源类型 public static void setDataBaseType(DataBaseType dataBaseType) { if (dataBaseType == null) { throw new NullPointerException(); } System.out.println("[将当前数据源改为]:" + dataBaseType); TYPE.set(dataBaseType); } // 获取数据源类型 public static DataBaseType getDataBaseType() { DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.FIRST : TYPE.get(); System.out.println("[获取当前数据源的类型为]:" + dataBaseType); return dataBaseType; } // 清空数据类型(清理时机不好掌控,且目前ThreadLocal只存在一个值,不清理也没影响) public static void clearDataBaseType() { TYPE.remove(); } }
3、定义多个数据源: 将定义好的多个数据源放在动态数据源中。
@Configuration @MapperScan(basePackages = {"com.huxun.inspection.mapper"}, sqlSessionFactoryRef = "SqlSessionFactory") public class DruidConfig { @Bean(name = "firstDataSource") @Primary @ConfigurationProperties(prefix = "spring.datasource.first") public DataSource firstDataSource(){ return DruidDataSourceBuilder .create() .build(); } @Bean(name = "secondDataSource") @ConfigurationProperties(prefix = "spring.datasource.second") public DataSource secondDataSource(){ return DruidDataSourceBuilder .create() .build(); } @Bean(name = "dynamicDataSource") public DynamicDataSource DataSource(@Qualifier("firstDataSource") DataSource test1DataSource, @Qualifier("secondDataSource") DataSource test2DataSource) { Map<Object, Object> targetDataSource = new HashMap<>(); targetDataSource.put(DataSourceType.DataBaseType.FIRST, test1DataSource); targetDataSource.put(DataSourceType.DataBaseType.SECOND, test2DataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSource); dataSource.setDefaultTargetDataSource(test1DataSource); return dataSource; } @Bean(name = "SqlSessionFactory") public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dynamicDataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml")); return bean.getObject(); } }
4、定义AOP: 用于切换不同业务数据库的入口。
@Aspect @Component public class DataSourceAspect { @Before("execution(* com.huxun.inspection.mapper..Daily*.*(..))") public void setDataSource2test01() { System.err.println("读取第二个数据源"); DataSourceType.setDataBaseType(DataSourceType.DataBaseType.SECOND); } @Before("execution(* com.huxun.inspection.mapper..*.*(..)) && !execution(* com.huxun.inspection.mapper..Daily*.*(..))") public void setDataSource2test02() { System.err.println("读取第一个数据源"); DataSourceType.setDataBaseType(DataSourceType.DataBaseType.FIRST); } }
整体目录如图:
需要权限,能读取information_schema.TABLES
数据
定时任务执行时机:每周三和周五(发版后的第一天)
3.4、数据比对
逻辑如下:
- 1、线上存在,daily不存在,场景可能是daily环境发生了不兼容的升级改造,消息推送即可;
- 2、线上不存在,daily存在,场景可能是daily在新增了表,可以将表名存放到redis中,7天后,线上还不存在该表,消息推送;
- 3、都存在,但是不一致,场景是索引遗漏、comment该了、字段名改了、字段类型改了,立即消息推送。
3.5、数据表巡检信息推送
- 业务类型:%s 数据不一致,请及时处理
- 表名:%s
- 负责人:%s
- %s 上下游数据不一致,请及时处理
- 差异类别(0-create、1-update、2-delete):%s
- 批次id:%s
3.6、dbChange
表1:table差异巡检表
CREATE TABLE IF NOT EXISTS `table_diff_inspection`( `id` bigint unsigned auto_increment comment '主键id' primary key, `biz_id` bigint not null comment '业务id', `batch_id` bigint not null comment '批次id', `status` tinyint(1) default 0 not null comment '状态,0-待确认,1-确认', `key_field_json` longtext not null comment '业务关键字段数据', `diff_type` tinyint null comment '差异类别 (0-create、1-update、2-delete)', `db_name` varchar(50) not null COMMENT '库名', `group_name` varchar(50) not null COMMENT '处理人', `create_user` bigint not null comment '创建人', `update_user` bigint null comment '更新人', `create_time` datetime default CURRENT_TIMESTAMP not null comment '创建时间', `update_time` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ) DEFAULT CHARACTER SET = utf8mb4 COMMENT = 'table差异巡检表'; create index idx_batchId_bizIds on falcon_inspection.falcon_table_diff (batch_id, biz_id);
4、问题记录
问题1:dbName没有值
问题2:表结构修改
/** * 创建人 */ private Long createUser; /** * 更新人 */ private Long updateUser; /** * 创建时间 */ private Date createTime; /** * 更新时间 */ private Date updateTime;
问题3:钉钉机器人的流控
- send too fast, exceed 20 times per minute:每分钟最多 20 条
- 会限流10分钟
- 推送消息体过大 单条消息最长 2000 字节
问题4:sql解析失败
param:insert ignore into falcon_convoy
.tp_4740783_ogt_finance_fare_**info**
(id
, tenant_id
, sub_order_id
, sub_order_carrier_id
, sub_order_settlement_id
, fare_item_id
, bill_no
, settlement_type
, settlement_id
, creator_type
, price
, tax_rate
, currency
, img
, attachment_id
, remark
, confirm_state
, confirm_no
, confirm_user
, confirm_remark
, confirm_time
, collate_state
, invoice_state
, invoice_user
, invoice_code
, invoice_time
, verify_state
, verify_user
, verify_time
, team_fare_state
, team_state
, deleted
, create_user
, update_user
, create_time
, update_time
) select id
, tenant_id
, sub_order_id
, sub_order_carrier_id
, sub_order_settlement_id
, fare_item_id
, bill_no
, settlement_type
, settlement_id
, creator_type
, price
, tax_rate
, currency
, img
, attachment_id
, remark
, confirm_state
, confirm_no
, confirm_user
, confirm_remark
, confirm_time
, collate_state
, invoice_state
, invoice_user
, invoice_code
, invoice_time
, verify_state
, verify_user
, verify_time
, team_fare_state
, team_state
, deleted
, create_user
, update_user
, create_time
, update_time
from falcon_convoy
.finance_fare_**info**
force index (primary
) where id
> $0 and (id
< $1 or id
= $2) lock in share mode
Action1:为了实现daily环境线上环境数据表比对,需要解决这两问题
1、daily环境与线上环境网络不通:需要在一个环境中,既访问线上环境db,又访问daily环境db
- 即 将ecs部署到control区
2、现在线上各个库使用各自的账号密码:能不能提供一个只读权限的账号,能访问线上db实例 全部的库
- 这样多数据源只用连两就行:daily实例、线上实例
Action2:SpringBoot使用多数据源导致MyBatis分页插件无效
背景
现象是gateway 网关 报错 FluxOnAssembly$OnAssemblyException
,经过排查,发现是分页查询时返回了1000多条数据,导致数据量超出了网关限制,从而抛错。打断点发现MyBatis分页插件无效,MyBatis分页拦截器断点无法进入。
情景
1、使用Springboot
2、自定义sqlSession(多数据源)
解决方法
1、检查分页插件类上是否加注解 @Component ✅
2、在SqlSessionFactoryConfig类注入拦截器 ✅
3、sqlSessionFactoryBean.setPlugins(new Interceptor[]{pageInterceptor});
注意:设置plugins时必须在sqlSessionFactoryBean.getObject()之前。SqlSessionFactory在生成的时候就会获取plugins,并设置到Configuration中,如果在之后设置则不会注入。