选择开源核心组件的一个非常重要的考虑通常是社区活跃性,一旦项目团队无法进行自己后续维护和扩展的情况下更是如此。
关于分库分表和读写分离、主从
一般来说,需要分库分表的系统是流量比较大的,而且比较容易出现峰值的比如说打折/活动的时候;其次,当单机扛不住业务流量的时候,分库分表一定不是第一选择,在分库分表之前,应该先保证垂直拆分完成了,子系统内都是高内聚的,其次基于Master-Slave的读写分离或者模糊查询很多的,可能NoSQL比如elastic就引流去很大一部分了。当读写分离也做完了,主库只剩下关键业务逻辑之后,流量还是很高,这个时候才开始考虑分库分表。因为相对于读写分离、垂直拆分,分库分表对开发和运维的要求多得多,如果确定业务一两年内不会剧增的,盲目引入只会导致成本高昂(尤其是各种SQL限制)。
其次,分库分表会增加N倍的数据库服务器,一般来说是4的倍数,如果某个应用说要做分库分表,又只有两台机器,那完全就是凑热闹。
读写分离和分库分表应该来说是前后的两件事比较合理,不少人将这两个事情混到一起去讲准确的说不合理的。分库分表通常更多的是用于纯粹的OLTP的事情,让系统可以水平扩展。而读写分离更多的是为了把一部分可以容忍短时延迟/不保证100%(但应该在99%以上)准确的查询路由到查询库,准确的说是对业务根据优先级做个归类,这些查询从资源消耗的角度来说相对逻辑上的PK查询要高几倍到数百倍,比如说查询某个人过去3个月的交易情况,但他从业务角度并不算是DSS概念,比如说查询已经T-N的订单/针对这些订单进行导出,并针对这个单子可能会进行一些操作,甚至人工修改状态。通过把这些功能从核心的订单/交易/资金OLTP中拆分出去,可以保证核心业务系统不会因为一个异常操作比如SQL不合理导致系统出现业务负载增加外的额外抖动因素。
从系统设计角度来说,读写分离虽然从逻辑表结构角度来说是相同的,都具有相同的字段定义,但是物理实现上是一定是不相同(要是完全相同,说明没有领会读写分离的初衷)的,尤其是在索引上,写库可能除了PK、唯一索引(任何表都应该有唯一索引,分布式锁只能作为缓冲器)外,最多还有一两个简单的字段索引以最大化性能,任何SQL符合条件的数据一般不会超过几十条(极端客户除外),但是读库根据业务不同,可能会有很多的索引以满足各种查询以及简单的统计汇总报表的需求。
那写库是不是一定就比读库重要呢?是,又不是。是是绝对的,不是是相对的。因为读库不是DSS库,是交易库中相对来说不是特别重要的业务功能。所以,写库一旦挂了,就会导致业务下不去,读库挂了,可能会导致做业务的人决策错误。比如没有没有查到做过某交易,又重新交易一次。
考虑分库分表一个很重要的决策就是是否允许跨多库操作以及有没有必要。TODO。。。。待补充。。。。。。
其次,分库和分表是两件事,是到底选择分库还是分表,如果量没有那么大的话而且是虚拟机的话,估计分库就够了。
sharding-jdbc的版本及其架构差异
目前最新版本的sharding-jdbc是v3.0.0.M1,应该来说还不稳定,包名又改成了io.shardingsphere,jar包名是sharding-jdbc。
1.5.4.1是目前最新版,也可能是终版,1.x的,坐标是com.dangdang,jar包名是sharding-jdbc。
2.0.3是目前最新版,包名和坐标统一改成了io.shardingjdbc,jar包名是sharding-jdbc-core。
说明规划不是特别好,还是有些乱。
因为2.0之后基本上纯粹分库分表的核心特性的增强就不多了,主要往治理和代理方向去了,所以如果没有特别的需求比如需要类似mycat的独立服务代理模式,使用1.x(注:1.x版本官网文档好像下线了)就可以了,不过如果是大规模的部署,同时已经使用了微服务架构中的注册中心或者基于spring boot,可以考虑使用2.0,因为2.0增加了基于注册中心的配置管理以及spring boot starter。所以2.0的架构是这样的:
3.0之后,增加了类似mycat角色的sharding-proxy无状态服务器(代理层可以有,但是不应该作为应用直接访问的入口,如下图所示),以及类似于Service Mesh的Database Mesh。不过核心没有变化,对SQL语法增加了部分新的支持。所以3.0的架构如下:
就分库分表核心来说,我们就只要关心sharding-jdbc就可以了,不需要关心sharding-sphere的其他部分。
sharding-jdbc/Sharding-Proxy/Sharding-Sidecar三者的对比如下:
事务
对任何重要的系统来说,事物一致性都是关键的。对分布式系统来说更是如此,最重要的就是事务一致性,从这个层面来说,分库分表本身并没有引入另外一个层次的复杂性。因为它在jdbc驱动层包了一层,所以我们有必要理解它对事务的支持性以及相关的限制。事务
sharding jdbc 2.x不支持强一致性的分布式事务,一般现在的系统设计也不追求强一致性,而是最终一致性。所以sharding jdbc 2.x支持2中事务:弱XA(同mycat)和最大努力投递事务(官方简称BED)(算是BASE的一种),具体选择哪一种需要根据业务和开发进行权衡,如果架构规范和设计做得好,是可以做到不跨库分布式事务的。
弱XA事务是默认的模式(即只要dml期间没有抛出异常,commit期间有机器断网或者宕机,是无法保证一致性的),没有特别的要求。
BED则在一定上增加了短时容忍,将执行的语句另作中心化存储,然后轮询commit期间失败的事务重试,所以BED的架构如下:
但是没有免费的午餐,BED对开发和维护有着一定的额外要求,而且这些要求都涉及面很广,绝对算得上伤筋动骨。开发层面包括:
- INSERT语句的主键不能自增
- UPDATE必须可重复执行,比如不支持UPDATE xxx SET x=x+1,对于更新余额类,这就相当于要求必须乐观锁了
运维层面包括:
- 需要存储事务日志的数据库
- 用于异步作业使用的zookeeper
- 解压sharding-jdbc-transaction-async-job-$VERSION.tar,通过start.sh脚本启动异步作业
我们选择了从设计层面避免强一致性的分布式事务。
分片灵活性
对于分库分表来说,很重要的一个特性是分片的灵活性,比如单个字段、多个字段的=、IN、>=、<=。为什么多个字段很重要的,这里涉及到一个特殊的考虑
sharding-jdbc目前提供4种分片算法。
由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。
对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,多分片键逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。
- Hint分片算法(Hint分片指的是对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录ID分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。)
对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
因为算法的灵活性,标准的方式是通过实现具体的java接口是实现具体的分片算法比如SingleKeyDatabaseShardingAlgorithm,有不少的情况下,分片是比较简单的,比如说纯粹是客户编号,此时提供了行内表达式分片策略,使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,不过这只支持单分片键。比如,t_user_${u_id % 8}
表示t_user表按照u_id按8取模分成8个表,表名称为t_user_0
到t_user_7
。
分片键+分片算法=真正可用的分片策略。
算法和分片键的选择是分库分表的关键,其直接决定了各个分库的负载是否均衡,以及扩展是否容易。在设计上的考虑一节笔者会详细阐述,订单和委托业务、用户在使用分库分表时设计上的考虑以及原因。
语法限制
对于分库分表来说,还需要知道有哪些SQL的限制,尤其是涉及到需要二次处理的,比如排序,去重,聚合等。
这里笔者就列下那些常用但没有被支持的。比如:
不过好在这些在java/js中处理都比较方便。
如果有很复杂的SQL,那最大的可能就是设计上有问题,应该采用读写分离解决。
sharding-jdbc对SQL的限制完整可以参考http://shardingsphere.io/document/current/cn/features/sharding/usage-standard/sql/
设计上的考虑
哪些表要分库分表
首先从设计上要区分清楚哪些是广播表/哪些是分库表/哪些是只在一个库的表,因为是公用数据源的,所以不管是不是分库的表,都需要配置,不配置分片规则Sharding-JDB即无法精确的断定应该路由至哪个数据源。但是一般分库分表组件包括Sharding-JDBC都会提供简化配置的方法。对于不分片的表:
方法1:sharding-jdbc可以在配置default-data-source-name,这样未配置分片规则的表将通过默认数据源定位。
方法2:将不参与分库分表的数据源独立于Sharding-JDBC之外,在应用中使用多个数据源分别处理分片和不分片的情况。
分库还是分表
分片键的选择
其中最重要的是分片键不能是自增字段,否则insert就不知道去哪里了。
分布式主键
Sharding-JDBC使用说明
对于只有一个分片键的使用=和IN进行分片的SQL,建议使用行表达式代替Java类的配置。假设我们不使用弱性事务(如果使用柔性事务,则还需要引入sharding-jdbc-transaction以及sharding-jdbc-transaction-async-job),这样就只要引入sharding-jdbc-core这个jar包就可以了,(因为sharding-jdbc的配置支持java、yaml、spring boot以及spring命名空间(类似dubbo),所以建议使用spring 命名空间方式)如下:
<dependency>
<groupId>io.shardingjdbcgroupId>
<artifactId>sharding-jdbc-coreartifactId>
<version>2.0.3version>
dependency>
<dependency>
<groupId>io.shardingjdbcgroupId>
<artifactId>sharding-jdbc-core-spring-namespaceartifactId>
<version>2.0.3version>
dependency>
因为sharding jdbc可以支持dbcp、druid,所以就不用改动其他的依赖项了。
接下去配置数据源、数据源分片策略和表分片策略。
注意点:
1、使用了sharding-jdbc之后,select from TableName会被转换为select from tablename,也就是转小写,这是在代码中处理的,如果不希望转换(sql标准是大小写不敏感的,主要是mysql linux下lower-case-table-names=1这个特立会区分大小写,而且mysql 8.0不允许数据库初始化和启动的值不一致,5.7之前是可以的,https:);,则需要自己编译sharding-jdbc,并更改getTableTokens中的转小写的代码(这应该算是个bug),2.0.3版本代码位置为:
// io.shardingjdbc.core.rewrite.SQLRewriteEngine.getTableTokens(TableUnit)中对sql语句中的表名做了toLowerCase()导致的,如下:
private Map getTableTokens(final TableUnit tableUnit) {
String logicTableName = tableUnit.getLogicTableName().toLowerCase();
Map tableTokens = new HashMap<>();
tableTokens.put(logicTableName, tableUnit.getActualTableName());
Optional bindingTableRule = shardingRule.findBindingTableRule(logicTableName);
if (bindingTableRule.isPresent()) {
tableTokens.putAll(getBindingTableTokens(tableUnit, bindingTableRule.get()));
}
return tableTokens;
}
2、一定要设置context:property-placeholder的ignore-unresolvable属性为true,即<context:property-placeholder location="classpath:property/*.properties" ignore-unresolvable="true" />,否则会报无法解析占位符。
参考:
https://www.oschina.net/question/2918182_2280300
https://www.oschina.net/news/88860/sharding-jdbc-1-5-4-released
http://www.infoq.com/cn/news/2017/12/Sharding-JDBC-2
http://cmsblogs.com/?p=2542
http://shardingjdbc.io/document/legacy/2.x/en/00-overview/
https://www.oschina.net/question/2356021_2264290
https://blog.csdn.net/vkingnew/article/details/80613043