前言
随着业务的增长,数据库会成为我们业务系统的瓶颈,这时我们就会想到分库分表这种手段,以此降低单数据库的负担。
当我们的业务系统需要进一步拆分成多库多表时,需要对系统做一些侵入性的改造,例如:耦合业务代码,分别为新表和老表做读写操作,此外,新老双数据源的事务性问题还需更复杂的解决方案,由此,为了解决业务演进过程中的这个痛点,我们设计研发了DataSS (data shard Synchronization),中文名:穿山甲,寓意帮助业务打通库表拆分时的数据,使业务能够快速的,低成本的,无风险的平滑过渡到拆分后的新库表。
为了减少用户的学习成本,特别简化了用户的接入流程,仅需三步完成数据拆分改造。
<dependency>
<groupId>com.xinye</groupId>
<artifactId>data-shard-sync</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
/**
* 配置数据源
*/
@Bean(name = "PpdaiLoanCouponDataSource")
@Qualifier("PpdaiLoanCouponDataSource")
public DataSource getPpdaiLoanCouponDataSource() throws SQLException {
DataShardSyncDataSource dataShardSyncDataSource = new DataShardSyncDataSource();
dataShardSyncDataSource.setOldDataSource(createOldDruidDataSource());
dataShardSyncDataSource.setNewDataSource(createNewShardingDataSource());
return dataShardSyncDataSource;
}
/**
* 注解使用:
* enableMappingKey 控制是否需要使用mapping功能。
* 如打开,配置后面的MappingKeys(originKey 代表原表的查询字段,targetKey 代表需要映射的该表的shardingkey)
*/
@DataShardSync(enableMappingKey = true, mappingKeys = {@MappingKeys(originKey ="i d",targetKey = "user_id")})
public interface CouponMapper extends Mapper<Coupon> {
}
支持新老数据源数据同步:将配置的old的数据源的表数据同步写一份至new数据源。
支持mapping路由功能,对于不支持的sql,根据用户指定的映射关系,支持通过mapping表,动态的填入shardingkey.
在开启事务的模式下,根据开关动态切换事务的执行主题,保证主体事务的完整性。
支持Apollo动态热切。
可根据开关配置动态切换读取新表老表数据。
可在application.properties配置文件中配置属性,完全支持spring的相关配置方式:
/**
* 是否开启功能
*/
private Boolean enable = true;
/**
* 老表写开关
*/
private Boolean oldWrite = true;
/**
* 新表写开关
*/
private Boolean newWrite = false;
/**
* 读开关,读哪个数据源,那个就是默认数据源,也是数据源事务,true = 读新库,false = 读老库
*/
private Boolean readOldOrNew = false;
我们使用了mybatis的拦截器功能,拦截系统所有sql请求。根据系统所执行的sql,我们帮助系统自动的切换数据源进行读写
@Override
public Object intercept(Invocation invocation) throws Throwable {
//关闭功能,即插件关闭,实现可插拔式
if (dataShardSyncProperties.getEnable() != null && !dataShardSyncProperties.getEnable()){
return invocation.proceed();
}
//如果未加注解的mapper执行,直接返回
if (dataShardSyncAnnotation == null){
return invocation.proceed();
}
//前置处理
beforeProcess(invocation);
//切换数据源且执行
Object result = setDataSourceKeyAndProcess(invocation,mappedStatement);
//后置处理
afterProcess(invocation);
return result;
}
为了支持mapping路由功能,帮助业务解决一些不支持的sql,可根据用户指定的映射关系查询mapping表,动态的为用户的sql填入shardingkey。我们对shardingConditions进行重实现。封装mapping逻辑:
帮助大家回忆一下,,我们前面提到在mapper类上配置了该注解:
/**
* 注解使用:
* enableMappingKey 控制是否需要使用mapping功能。
* 如打开,则配置后面的MappingKeys(originKey 代表原表的查询字段,targetKey 代表需要映射的该表的shardingkey)
*/
@DataShardSync(enableMappingKey = true, mappingKeys = {@MappingKeys(originKey ="i d",targetKey = "user_id")})
public interface CouponMapper extends Mapper<Coupon> {
}
读请求时,我们根据sql的条件,查询mapping是否存在映射关系,帮助找到分片键信息
写请求时,我们根据sql内的分片键和配置的映射关系,自动的在mapping表插入一条映射关系记录
/**
* 对shardingConditions进行重实现。封装mapping逻辑。
* @param parameters
* @param sqlStatementContext
* @param schemaMetaData
* @param shardingRule
* @return
*/
@Override
public ShardingConditions getShardingParams(List<Object> parameters, SQLStatementContext sqlStatementContext, SchemaMetaData schemaMetaData, ShardingRule shardingRule) {
ShardingConditions shardingConditions = super.getShardingConditions(parameters,sqlStatementContext,schemaMetaData,shardingRule);
try {
DataShardSyncAnnotation dataShardSyncAnnotation = DataShardSyncContext.dataShardSyncAnnotationThreadLocal.get();
if (dataShardSyncAnnotation != null){
//如果开启了配置映射功能
if (dataShardSyncAnnotation.getEnableMappingKey()){
for (Map.Entry<String,String> entry : dataShardSyncAnnotation.getShardKeyMapping().entrySet()) {
if (sqlStatementContext instanceof InsertStatementContext){
//如果是插入语句
buildShardingConditionForInsert(entry.getKey(),entry.getValue(),parameters,sqlStatementContext,schemaMetaData,shardingRule);
} else {
//如果是查询,删除,更新语句
if (CollectionUtils.isEmpty(shardingConditions.getConditions())){
try {
shardingConditions = buildShardingConditionForWhere(entry.getKey(),entry.getValue(),parameters,sqlStatementContext,schemaMetaData,shardingRule);
if (!CollectionUtils.isEmpty(shardingConditions.getConditions())){
break;
}
} catch (Exception e){
logger.error("buildShardingCondition by shardingMapping error!" + e.getMessage());
e.printStackTrace();
}
}
}
}
}
}
} finally {
DataShardSyncContext.dataShardSyncAnnotationThreadLocal.remove();
}
return shardingConditions;
}
我们进行新老数据源双写的时候,保证一个原则:
我们通过DataX的全量数据比对机制来做新老库的数据检查,知道观察一段时间数据保持一致,方可切读到新库,停双写。
DataX是我们的数据同步以及数据比对工具,还具有数据差错处理,可自动修补数据功能,同时发现差错可邮件告警通知。有兴趣了解DataX的可以联系我,在此就不赘述此工具。
这样我们就保证了切读前,老库数据一致,新库读写失败不影响业务。在数据比对一段时间之后,保证新老库的双写并无问题,方可切读。
业务自实现数据拆分 | 引入DataSS插件 | |
---|---|---|
系统改造研发效率 | 10天 | 2小时 |
业务侵入 | 耦合在业务代码实现 | 可插拔组件内置双写,路由等操作 |
非shardingkey查询 | 不支持(会造成全库全表扫描) | 支持映射关系路由,提高查询效率 |
使用扩展性 | 无 | 1.数据源配置 2.ShardingRoute路由 3.ShardingMapping可定制 |
使用灵活性 | 无 | 支持spring体系的配置,apollo等,可动态切换关闭 |