作者:京东物流 张广治
1 背景
传统的将数据集中存储至繁多数据节点的解决方案,在性能和可用性方面曾经难于满足海量数据的场景,零碎最大的瓶颈在于单个节点读写性能,许多的资源受到单机的限度,例如连接数、网络IO、磁盘IO等,从而导致它的并发能力不高,对于高并发的要求不满足。
每到月初国内财务零碎压力微小,因为月初有大量补全工作,重算、计算工作、账单生成工作、推送集成等都要赶在月初1号实现,显然咱们须要一个反对高性能、高并发的计划来解决咱们的问题。
2 咱们的指标
- 反对每月接单量一亿以上。
- 一亿的单量补全,计算,生成账单在24小时内实现(反对后面说的月初大数据量计算的场景)
3 数据调配规定
事实世界中,每一个资源都有其提供服务能力的下限,当某一个资源达到最大下限后就无奈及时处理溢出的需要,这样就须要应用多个资源同时提供服务来满足大量的工作。当应用了多个资源来提供服务时,最为要害的是如何让每一个资源比拟平均的承当压力,而不至于其中的某些资源压力过大,所以调配规定就变得十分重要。
制订调配规定:要依据查问和存储的场景,个别依照类型、工夫、城市、区域等作为分片键。
财务零碎的租户以业务线为单位,毛病为拆分的粒度太大,不能实现打散数据的目标,所以不适宜做为分片键,事件定义作为分片键,毛病是十分不平均,目前2C进口清关,一个事件,每月有一千多万数据,鲲鹏的事件,每月单量很少,如果依照事件定义拆分,会导致数据极度歪斜。
目前最适宜作为分片键的就是工夫,因为零碎中计算,账单,汇总,都是基于工夫的,所以工夫非常适合做分片键,适宜应用月、周、作为Range的周期。目前应用的就是工夫分区,但只依照工夫分区显然曾经不能满足咱们的需要了。
通过筛选,实践上最适宜的分区键就剩下工夫和收付款对象了。
最终咱们决定应用收付款对象分库,工夫作为表分区。
数据拆分前构造(图一):
数据程度拆分后构造(图二):
调配规定
(payer.toUpperCase()+"_"+payee.toUpperCase()).hashCode().abs()%128
收款对象大写加分隔符加付款对象大写,取HASH值的绝对值模分库数量
重要:payer和payee字母对立大写,因为大小写不对立,会导致HASH值不统一,最终导致路由到不同的库。
4 读写拆散一主多从
4.1ShardingSphere对读写拆散的解释
对于同一时刻有大量并发读操作和较少写操作类型的数据来说,将数据库拆分为主库和从库,主库负责解决事务性的增删改操作,从库负责解决查问操作,可能无效的防止由数据更新导致的行锁,使得整个零碎的查问性能失去极大的改善。
通过一主多从的配置形式,能够将查问申请平均的扩散到多个数据正本,可能进一步的晋升零碎的解决能力。 应用多主多从的形式,岂但可能晋升零碎的吞吐量,还可能晋升零碎的可用性,能够达到在任何一个数据库宕机,甚至磁盘物理损坏的状况下依然不影响零碎的失常运行。
把数据量大的大表进行数据分片,其余大量并发读操作且写入小的数据进行读写拆散,如(图三):
左侧为主从构造,右侧为数据分片
4.2 读写拆散+数据分片实战
当咱们理论应用sharding进行读写拆散+数据分片时遇到了一个很大的问题,官网文档中的实现形式只适宜分库和从库在一起时的场景如(图四)
而咱们的场景为(图三)所示,从库和分库时彻底离开的,参考官网的实现办法如下:
https://shardingsphere.apache...
官网给出的读写拆散+数据分片计划不能配置
spring.shardingsphere.sharding.default-data-source-name默认数据源,如果配置了,所有读操作将全副指向主库,无奈达到读写拆散的目标。
当咱们困扰在读从库的查问会被轮询到分库中,咱们理论的场景从库和分库是拆散的,分库中基本就不存在从库中的表。此问题困扰了我近两天的工夫,我浏览源码发现
spring.shardingsphere.sharding.default-data-source-name能够被赋值一个DataNodeGroup,不仅仅反对配置datasourceName,sharding源码如下图:
由此
spring.shardingsphere.sharding.default-data-source-name配置为读写拆散的groupname1,问题解决
从库和分库不在一起的场景下,读写拆散+数据调配的配置如下:
#数据源名称spring.shardingsphere.datasource.names= defaultmaster,ds0,ds1,ds2,ds3,ds4,ds5,ds6,ds7,ds8,ds9,ds10,ds11,ds12,ds13,ds14,ds15,ds16,ds17,ds18,ds19,ds20,ds21,ds22,ds23,ds24,ds25,ds26,ds27,ds28,ds29,ds30,ds31,slave0,slave1#未配置分片规定的表将通过默认数据源定位,留神值必须配置为读写拆散的分组名称groupname1spring.shardingsphere.sharding.default-data-source-name=groupname1#主库spring.shardingsphere.datasource.defaultmaster.jdbc-url=jdbc:mysql:spring.shardingsphere.datasource.defaultmaster.type= com.zaxxer.hikari.HikariDataSourcespring.shardingsphere.datasource.defaultmaster.driver-class-name= com.mysql.jdbc.Driver#分库ds0spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql:spring.shardingsphere.datasource.ds0.type= com.zaxxer.hikari.HikariDataSourcespring.shardingsphere.datasource.ds0.driver-class-name= com.mysql.jdbc.Driver#从库slave0spring.shardingsphere.datasource.slave0.jdbc-url=jdbc:mysql:spring.shardingsphere.datasource.slave0.type= com.zaxxer.hikari.HikariDataSourcespring.shardingsphere.datasource.slave0.driver-class-name= com.mysql.jdbc.Driver#从库slave1spring.shardingsphere.datasource.slave1.jdbc-url=jdbc:mysql:spring.shardingsphere.datasource.slave1.type= com.zaxxer.hikari.HikariDataSourcespring.shardingsphere.datasource.slave1.driver-class-name= com.mysql.jdbc.Driver#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,反对inline表达式。缺省示意应用已知数据源与逻辑表名称生成数据节点,用于播送表(即每个库中都须要一个同样的表用于关联查问,多为字典表)或只分库不分表且所有库的表构造完全一致的状况spring.shardingsphere.sharding.tables.incident_ar.actual-data-nodes=ds$->{0..127}.incident_ar#行表达式分片策略 分库策略,缺省示意应用默认分库策略spring.shardingsphere.sharding.tables.incident_ar.database-strategy.inline.sharding-column= dept_no#分片算法行表达式,需合乎groovy语法spring.shardingsphere.sharding.tables.incident_ar.database-strategy.inline.algorithm-expression=ds$->{dept_no.toUpperCase().hashCode().abs() % 128}#读写拆散配置spring.shardingsphere.sharding.master-slave-rules.groupname1.master-data-source-name=defaultmasterspring.shardingsphere.sharding.master-slave-rules.groupname1.slave-data-source-names[0]=slave0spring.shardingsphere.sharding.master-slave-rules.groupname1.slave-data-source-names[1]=slave1spring.shardingsphere.sharding.master-slave-rules.groupname1.load-balance-algorithm-type=round_robin
能够看到读操作能够被平均的路由到slave0、slave1中,分片的读会被调配到ds0,ds1中如下图:
4.3 实现本人的读写拆散负载平衡算法
Sharding提供了SPI模式的接口
org.apache.shardingsphere.spi.masterslave.MasterSlaveLoadBalanceAlgorithm实现读写拆散多个从的具体负载平衡规定,代码如下:
import lombok.Getter;import lombok.RequiredArgsConstructor;import lombok.Setter;import org.apache.shardingsphere.spi.masterslave.MasterSlaveLoadBalanceAlgorithm;import org.springframework.stereotype.Component;import java.util.List;import java.util.Properties;@Component@Getter@Setter@RequiredArgsConstructorpublic final class LoadAlgorithm implements MasterSlaveLoadBalanceAlgorithm { private Properties properties = new Properties(); @Override public String getType() {return "loadBalance";} @Override public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) { //本人的负载平衡规定 return slaveDataSourceNames.get(0);
RoundRobinMasterSlaveLoadBalanceAlgorithm 实现为所有从轮询负载
RandomMasterSlaveLoadBalanceAlgorithm 实现为所有从随机负载平衡
4.4 对于某些场景下必须读主库的解决方案
某些场景比方分布式场景下写入马上读取的场景,能够应用hint形式进行强制读取主库,Sharding源码应用ThreadLocal实现强制路由标记。
上面封装了一个注解能够间接应用,代码如下:
@Documented@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface SeekMaster {}import lombok.extern.slf4j.Slf4j;import org.apache.shardingsphere.api.hint.HintManager;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;/** * ShardingSphere >读写拆散自定义注解>用于实现读写拆散时>须要强制读主库的场景(注解实现类) * * @author zhangguangzhi1 **/@Slf4j@Aspect@Componentpublic class SeekMasterAnnotation { @Around("@annotation(seekMaster)") public Object doInterceptor(ProceedingJoinPoint joinPoint, SeekMaster seekMaster) throws Throwable { Object object = null; Throwable t = null; try { HintManager.getInstance().setMasterRouteOnly(); log.info("强制查问主库"); object = joinPoint.proceed(); } catch (Throwable throwable) { t = throwable; } finally { HintManager.clear(); if (t != null) { throw t; } } return object;
应用时办法上打SeekMaster注解即可,办法下的所有读操作将主动路由到主库中,办法外的所有查问还是读取从库,如下图:
4.5 对于官网对读写拆散形容不够明确的补充阐明
版本4.1.1
经实际补充阐明为:
同一线程且同一数据库连贯且一个事务中,如有写入操作,当前的读操作均从主库读取,只限存在写入的表,没有写入的表,事务中的查问会持续路由至从库中,用于保证数据一致性。
5 对于分库的JOIN操作
办法1
应用default-data-source-name配置默认库,即没有配置数据分片策略的表都会应用默认库。默认库中表禁止与拆分表进行JOIN操作,此处须要做一些革新,目前零碎有一些JOIN操作。(举荐应用此办法)
办法2
应用全局表,播送表,让128个库中冗余根底库中的表,并实时扭转。
办法3
分库表中冗余须要JOIN表中的字段,能够解决JOIN问题,此计划单个表字段会减少。
6 分布式事务
6.1 XA事务管理器参数配置
XA是由X/Open组织提出的分布式事务的标准。 XA标准次要定义了(全局)事务管理器(TM)和(局 部)资源管理器(RM)之间的接口。支流的关系型 数据库产品都是实现了XA接口的。
分段提交
XA须要两阶段提交: prepare 和 commit.
第一阶段为 筹备(prepare)阶段。即所有的参与者筹备执行事务并锁住须要的资源。参与者ready时,向transaction manager报告已准备就绪。
第二阶段为提交阶段(commit)。当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令。
ShardingSphere默认的XA事务管理器为Atomikos,在我的项目的logs目录中会生成xa_tx.log, 这是XA解体复原时所需的日志,请勿删除。
6.2 BASE柔性事务管理器(SEATA-AT配置)
Seata是一款开源的分布式事务解决方案,提供简略易用的分布式事务服务。随着业务的疾速倒退,利用单体架构暴露出代码可维护性差,容错率低,测试难度大,麻利交付能力差等诸多问题,微服务应运而生。微服务的诞生一方面解决了上述问题,然而另一方面却引入新的问题,其中次要问题之一就是如何保障微服务间的业务数据一致性。Seata 注册配置服务中心均应用 Nacos。Seata 0.2.1+ 开始反对 Nacos 注册配置服务中心。
- 依照seata-work-shop中的步骤,下载并启动seata server。
- 在每一个分片数据库实例中执创立undo_log表(以MySQL为例)
CREATE TABLE IF NOT EXISTS `undo_log`( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME NOT NULL COMMENT 'create datetime', `log_modified` DATETIME NOT NULL COMMENT 'modify datetime', PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
3.在classpath中减少seata.conf
client { application.id = example ## 利用惟一id transaction.service.group = my_test_tx_group ## 所属事务组}
6.3 Sharding-Jdbc默认提供弱XA事务
官网阐明:
齐全反对非跨库事务,例如:仅分表,或分库然而路由的后果在单库中。
齐全反对因逻辑异样导致的跨库事务。例如:同一事务中,跨两个库更新。更新结束后,抛出空指针,则两个库的内容都能回滚。
不反对因网络、硬件异样导致的跨库事务。例如:同一事务中,跨两个库更新,更新结束后、未提交之前,第一个库死机,则只有第二个库数据提交。
6.4 分布式事务场景
1.保留场景
举荐应用第三种弱XA事务,尽量设计时防止跨库事务,目前设计为事件和事件数据为同库(分库时,将一个线索号的事件和事件数据HASH进入同一个分库),尽量避免跨库事务。
事件和计费后果自身设计为异步,非同一事务,所以事件和对应的后果不波及跨库事务。
保留多个计费后果,每次保留都属于一个事件,一个事件的计费后果都属于一个收付款对象,人造同库。
弱XA事务的性能最佳。
2.更新场景
对一些依据ID IN的更新场景,依据收付款对象分组执行,能够防止在所有分库执行更新。
3.删除场景
无,目前都是逻辑删除,理论为更新。
7 总结
1.举荐应用Sharding-Sphere进行分库,分表能够思考应用MYSQL分区表,对于研发来讲齐全是通明的,能够躲避JOIN\分布式事务等问题。(分区表须要为分区键+ID建设了一个联结索引)MYSQL分区失去了大量的实际印证,没有BUG,包含我在新计费初期,始终保持推动应用的分表计划,不会引起一些难以发现的问题,在同库同磁盘下性能与分表相当。
2.对于同一时刻有大量并发读操作和较少写操作类型的数据来说,适宜应用读写拆散,减少多个读库,缓解主库压力,要留神的是必须读主库的场景应用SeekMaster注解来实现。
3.数据分库抉择适合的分片键十分重要,要依据业务需要抉择好分库键,尽力防止数据歪斜,数据不平均是目前数据拆分的一个独特问题,不可能实现数据的齐全平均;当查问条件没有分库键时会遍历所有分库,查问尽量带上分库键。
4.在咱们应用中间件时,不要只看官网解释,要多做测试,用理论来验证,有的时候官网解释话术可能存在歧义或表白不够全面的中央,剖析源码和理论测试能够清晰的取得想要的后果。