作者:京东批发 张宾
一、OMS 供应链零碎零碎简介
抖快电商业务与京东电商供应链能力之间的连接器,用于承载抖、快京东官网店铺业务。
二、面临业务挑战
我的项目初期为了疾速适配业务开发,数据都存储在 MySQL 中,应用京东数据库中间件团队提供 JED 弹性库依照店铺维度的做的数据库分片。随着业务疾速倒退,存储数据越来越多,咱们在 MySQL 面临这如下痛点:
1. 数据库单分片热点
随着达人直播场次和拉新流动的减少,呈现局部店铺订单量爆涨,因为以后数据库分片策略是依照店铺维度进行分片,存在数据歪斜,零碎吞吐量预估到 2000 QPS 即达到性能瓶颈。按以后的订单量增长速度,半年内局部店铺的订单量可能超千万级,单表数据量过大。
2. 大表构造批改艰难
业务模式变动快,为了疾速响应业务需要,表构造常常调整。在对一些数据在百万级别以上的大表做 DDL 的时候,批改的工夫较长,对存储空间、IO、业务有肯定的影响。
3. 经营端订单列表查问常常超时
随着订单量增长,局部店铺的订单量超过千万之后,经营端订单列表查问会超时,经营端经营人员常常应用查问近 7 天、近 30 天的订单列表数据超时景象增多,经营端查问体验变差,同时订单列表性能导出也耗时重大。
4. 抖快历史订单查问问题
抖音、快手订单明细数据超过 6 个后,历史订单不再反对查问,须要将抖音、快手订单明细数据落地存储。
5. 零碎吞吐量优化
要晋升零碎吞吐量,须要调整数据库分片策略,要调整分片策略,首先要先解决经营端列表业务人员查问问题,所以必须首先且迫切的抉择一种存储两头解决来解决列表查问问题。
三、为什么抉择 TiDB
面对以上痛点,咱们开始思考对订单数据存储的架构进行降级革新,咱们依据业务方的诉求和将来数据量的增长,将一些常见数据存储技术计划做来一些比照:
TiDB 具备程度弹性扩大,高度兼容 MySQL,在线 DDL,一致性的分布式事务等个性,合乎以后零碎数据量大,业务变更频繁,数据保留周期长等场景。联合团队成员常识储备和在不影响业务需要迭代状况下,以较少人工成本实现数据异构和数据库分片键的切换,通过调研发现公司数据库团队提供已 TiDB 中间件能力和反对,咱们通过对 TiDB 的内部测试后,确认能够满足现有业务需要。咱们最终抉择了 TiDB 做为这类需要的数据存储,并通过数据同步中件件 DRC 平台实现 MySQL 异构到 TiDB。
四、技术实施方案
1. 零碎架构
2. 零碎中多数据源反对
除了引入一些分库分表组件,Spring 本身提供了 AbstractRoutingDataSource 的形式,让少数数据源的治理成为可能。同时分库分表组件应用上限度很多,应用之前须要理解去学习应用办法和忍耐中间件对 SQL 的刻薄要求,比照中间件以及以后我的项目应用的 Spring 技术栈,反而应用 Spring 本身提供了 AbstractRoutingDataSource 的形式可能让代码的改变量尽量的缩小。
Spring 提供的多数据源能进行动静切换的外围就是 spring 底层提供了 AbstractRoutingDataSource 类进行数据源路由。AbstractRoutingDataSource 实现了 DataSource 接口,所以咱们能够将其间接注入到 DataSource 的属性上。
咱们次要继承这个类,实现外面的办法 determineCurrentLookupKey(),而此办法只须要返回一个数据库的名称即可。
public class MultiDataSource extends AbstractRoutingDataSource {
@Getter
private final DataSourceHolder dataSourceHolder;
public MultiDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
// 设置默认数据源,在未指定数据源状况下,则应用默认的数据源拜访
super.setDefaultTargetDataSource(defaultTargetDataSource);
// 多数据源配置
super.setTargetDataSources(targetDataSources);
this.dataSourceHolder = new DataSourceHolder();}
@Override
protected Object determineCurrentLookupKey() {
// 获取数据源上下文对象持有的数据源
String dataSource = this.dataSourceHolder.getDataSource();
// 如果为空,则应用默认数据源 resolvedDefaultDataSource
if (StringUtils.isBlank(dataSource)) {return null;}
return dataSource;
}
数据源上下文切换存储,应用 ThreadLocal 绑定这个透传的属性,像 Spring 的嵌套事务等实现的原理,也是基于 ThreadLocal 去运行的。所以,DataSourceHolder. 实质上是一个操作 ThreadLocal 的类。
public class DataSourceHolder {
/**
* 保留数据源类型线程平安容器
*/
private final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源类型
*
* @param dataSource 数据源
*/
public void putDataSource(String dataSource) {CONTEXT_HOLDER.set(dataSource);
}
/**
* 获取数据源类型
*
* @return
*/
public String getDataSource() {return CONTEXT_HOLDER.get();
}
/**
* 清空数据源类型
*/
public void clear() {CONTEXT_HOLDER.remove();
定义数据源配置自定义注解:
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DataSourceAnnotation {
/**
* 数据源名称
*
* @return
*/
String value();}
多数据源抉择 AOP 切面
@Slf4j
@Aspect
public class MultiDataSourceAspect {
/**
* 多数据源
*/
@Setter
private MultiDataSource multiDataSource;
/**
* 定义切入点
*/
@Pointcut("execution(* com.jd.mkt.oms.mapper.order.*.*(..))")
public void aspect() {}
/**
* 办法执行前 - 抉择数据具体的数据源并放入到数据源上下文中
*/
@Before("aspect()")
public void beforeExecute(JoinPoint joinPoint) {if (!(joinPoint.getSignature() instanceof MethodSignature)) {return;}
MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature());
Method method = methodSignature.getMethod();
// 抉择具体的数据源
selectDataSource(method, joinPoint);
}
/**
* 办法执行后 - 清空数据源上下文
*/
public void afterExecute(JoinPoint joinPoint) {getDataSourceHolder().clear();}
/**
* 抉择具体的数据源
*/
private void selectDataSource(Method method, JoinPoint joinPoint, String aspectType) {
//1. 获取被 aop 拦挡办法的数据源自定义注解,若有,则应用办法上标注的数据源
DataSourceAnnotation dataSourceAnno = method.getAnnotation(DataSourceAnnotation.class);
String dataSourceStr = "";
if (dataSourceAnno != null) {dataSourceStr = dataSourceAnno.value();
getDataSourceHolder().putDataSource(dataSourceStr);
return;
}
//2. 获取被 aop 拦挡办法所在类上的数据源自定义注解,若有,则应用类上标注的数据源
Class<?> declaringClass = method.getDeclaringClass();
dataSourceAnno = declaringClass.getAnnotation(DataSourceAnnotation.class);
if (dataSourceAnno != null) {dataSourceStr = dataSourceAnno.value();
log.debug("{}--final method.getDeclaringClass()={}", aspectType, dataSourceStr);
getDataSourceHolder().putDataSource(dataSourceStr);
return;
}
//3. 获取被 aop 拦挡办法被代理的指标类上的数据源自定义注解,若有,则应用指标类上标注的数据源
Class<?> targetClass = AopUtils.getTargetClass(joinPoint.getTarget());
dataSourceAnno = targetClass.getAnnotation(DataSourceAnnotation.class);
if (dataSourceAnno != null) {dataSourceStr = dataSourceAnno.value();
log.debug("{}--final AopUtils.getTargetClass={}", aspectType, dataSourceStr);
getDataSourceHolder().putDataSource(dataSourceStr);
return;
}
//4. 获取被 aop 拦挡办法被代理的泛型上的数据源自定义注解,若有,则应用泛型类上标注的数据源,反对 tk.mybatis 等泛型接口上申明的数据源配置
Type[] genericInterfaces = targetClass.getGenericInterfaces();
if (genericInterfaces.length > 0) {if (genericInterfaces[0] instanceof Class) {Class genericInterface = (Class) genericInterfaces[0];
log.debug("genericInterface:{}", genericInterface.getName());
Annotation annotation = genericInterface.getAnnotation(DataSourceAnnotation.class);
if (annotation instanceof DataSourceAnnotation) {dataSourceAnno = (DataSourceAnnotation) annotation;
dataSourceStr = dataSourceAnno.value();
log.debug("final genericInterface={}", dataSourceStr);
getDataSourceHolder().putDataSource(dataSourceStr);
}
}
}
log.debug("final selectDataSource {}", dataSourceStr);
}
private DataSourceHolder getDataSourceHolder() {return multiDataSource.getDataSourceHolder();
}
3. 具体数据拜访办法数据源配置
3.1 我的项目中多数据源配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mySqlDataSource" parent="abstractDataSource">
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</bean>
<bean id="tiDbDataSource" parent="abstractDataSource">
<property name="url" value="${tidb.url}"/>
<property name="username" value="${tidb.username}"/>
<property name="password" value="${tidb.password}"/>
</bean>
<bean id="orderMultiDataSource" class="MultiDataSource" lazy-init="false">
<constructor-arg index="0" ref="mySqlDataSource"/>
<constructor-arg index="1">
<map>
<entry key="MySQL" value-ref="mySqlDataSource"/>
<entry key="TiDB" value-ref="tiDbDataSource"/>
</map>
</constructor-arg>
</bean>
<bean id="orderMultiDataSourceAspect" class="MultiDataSourceAspect">
<property name="multiDataSource" ref="orderMultiDataSource"/>
</bean>
<bean id="orderTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="orderMultiDataSource"/>
</bean>
<!-- 基于注解进行事物治理 -->
<tx:annotation-driven transaction-manager="orderTransactionManager"/>
<bean id="orderSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="orderMultiDataSource"/>
<property name="typeAliasesSuperType" value="com.jd.mkt.oms.infrastructure.po.base.PO"/>
<property name="mapperLocations" value="classpath:sqlmap/order/*.xml"/>
</bean>
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="markerInterface" value="com.jd.mkt.oms.infrastructure.mapper.base.ISqlMapper"/>
<property name="sqlSessionFactoryBeanName" value="orderSessionFactory"/>
<property name="basePackage" value="com.jd.mkt.oms.infrastructure.mapper.order"/>
</bean>
</b
3.2 数据拜访层 Dao 类或办法上减少数据源配置注解
dao 层接口办法减少数据源抉择注解
/**
* 依据店铺、平台、订单号查问订单列表
*
* @param extShopId 店铺 id
* @param platform 平台
* @param orderIds 订单号列表
* @return
*/
@DataSourceAnnotation("TiDB")
List<CtpOrderSkuPO> selectOrderList(@Param("extShopId") String extShopId, @Param("platform") int platform, @Param("orderIds") List<String> orderIds);
dao 层接口减少数据源抉择注解
@DataSourceAnnotation("TiDB")
@Repository
public interface OmsOrderLogMapper {
/**
* 查问订单操作日志列表数据
*
* @param platform
* @param orderId
* @return
*/
List<OmsOrderLogPO> selectOmsOrderLogs(@Param("platform") int platform, @Param("orderId") String orderId);
}
4.TiDB 数据库索引 KV 映射原理
4.1 SCHEMA 的 KV 映射原理
•聚簇表 KV 的映射规定
假如 Column_1 为 Cluster Index
Key: tablePrefix{TableID}_recordPrefixSep{Col1}
Value: [col2,col3,col4]
•非聚簇表 KV 的映射规定
Key: tablePrefix{TableID}\_recordPrefixSep{\_TiDb_RowID}
Value: [col1,col2,col3,col4]
KV 存储中 Value 存储实在的行数据
4.2 惟一索引 & 非聚簇表的主键
Key: tablePrefix{TableID}\_indexPrefixSep{IndexID}\_indexedColumnsValue
Value: RowID
4.3 二级索引
Key: tablePrefix{TableID}\_indexPrefixSep{IndexID}\_indexedColumnsValue_{RowID}
Value: null
5.MySQL 和 TiDB 索引创立和调整
基于 TiDB 索引和 MySQL 索引映射原理,依据业务解决个性,业务流程解决中须要依据订单号查问业务数据,经营端列表查问和数据导出依据店铺、订单号、工夫等多条件组合实现业务数据查问,咱们别离在 MySQL 中创立订单号索引,在 TiDB 创立基于店铺 + 工夫 d 额二级索引和基于订单号的惟一索引。
6. 数据库表路由分片键切换
因为咱们我的项目采纳 DDD 畛域驱动设计思维搭建的我的项目代码构造,所以咱们只须要在基层设施层实现分片键的路由键的适配切换即可,并借助 DRC 平台实现 MySQL 数据库数据迁徙,切换后防止了数据热点歪斜和晋升零碎解决性能。
五、上线后成果
1. 零碎解决性能,依据压测数据,数据库单分片解决 QPS 约 400 左右。
2. 防止数据歪斜,按订单号分库后,可保障单表数据量在 500 万以下,数据量在正当区间。
3. 经营端列表查问和数据导出经营体验,千万级订单数据量查问性能晋升了 5 倍。
六、将来布局
1. 对帐数据由原来应用 JED 间接替换成 TiDB
2. 抖快历史订单详情数据间接写入 TiDB