引用官网的原文,https://shardingsphere.apache.org/index_zh.html,
前身是由当当的Sharding-jdbc发展而来,主要作者张亮也是elsatic-job的作者。
通过前期的一些调研,也对其他的分布分表中间件有所了解,最终选择了shardingsphere。我们这次用的是Sharding-JDBC对原有的业务做分表,暂时没有做分库,官网上的文档比较齐全,github上也有示例,所以入门还是比较简单的,但是在实际的集成过程中还是踩到一些坑。
Sharding-jdbc支持的分表策略有5种,ComplexShardingStrategy、HintShardingStrategy、InlineShardingStrategy、StandardShardingStrategy、NoneShardingStrategy,每个策略对应各自的分片算法,但是一个表只能配置一个策略,并不支持多个策略混合的模式,如果某个表既想走HintShardingStrategy又想走StandardShardingStrategy,只能将服务继续分拆,颗粒度细化到走各自的策略,如果能支持类似于类似于责任链的这种多策略,那在配置的时候有更好的灵活性。
Sharding-jdbc不支持复杂的聚合函数,和子查询,在使用的过程中一定要注意。对于HintShardingStrategy这种策略来说,理论上只要解析表名来指定分片策略即可,不需要解析除表名的剩余sql,但是实际上sharding-jdbc会解析整条sql,可怕的是如果sql解析后和原来的loginSql不一致,业务方并不知晓,只能加强测试。还有如果集成了sharding-jdbc,会对所有的sql进行解析,不管有没有配置对应的分表策略,如果服务中有复杂的sql,就不要去集成,只能将服务继续拆分细化。
对于Mysql会用到这样的INSET INTO ON DUPLICATE KEY UPDATE这样的sql,在ShardingSphere 4.0.0-RC1前并不支持,新版本4.0.0-RC2只支持了一部分,丢失了部分参数。
例如:
Preparing: insert into origin_pre_order (OrderID,HotelID,LinkMobile,LinkPhone,LinkName,
CusCategoryID,MemMobile,SrcID,AssType,KeepTime,
ArrDate,ArrTime,DepDate,CreateDate,ModifyDate,OrderStatus,
CardNo,BookerID,MemName,BookerLevel,RoomInfo,RoomCnt,
Days,RcpType,CompanyName,SyncTime) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE HotelID = IF(ModifyDate <= ?, ? , HotelID),...
Sharding-jdbc在解析的时候会丢掉IF后面的参数。
分表之后会用到分布式主键,Sharding-jdbc内置了UUID和Snowflake,官网说后续会提供Leaf方面的集成。Snowflake算法需要workedId字段,如果在各自的服务中配置,需要间隔开配置不同的值,如果对于如果配置是集中化管理的,这个workedId的配置会固定,生成的主键可能会冲突。好在提供了ShardingKeyGenerator接口,可以实现这个接口同时按照SPI接口规范,Sharding-jdbc会在初始化的时候反射SPI接口实例化实现类。
我们就是按照这个规范去这样实现的,参照Leaf用zk注册顺序的方式生成workedId,集成配置zk的地址就行了。
具体参照下图
在用分布式主键插入数据的时候,如果数据里有某一列空值,id会补到这一列去,目前没有去跟踪源码了,项目中我们换了一下sql,用非空的形式
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="callId != null">call_id,if>
<if test="deviceIp != null">device_ip,if>
<if test="deviceMac != null">device_mac,if>
<if test="recordingAddress != null">recording_address,if>
<if test="duration != null">duration,if>
<if test="trunkNumber != null">trunk_number,if>
<if test="createTime != null">create_time,if>
trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="callId != null">#{callId},if>
<if test="deviceIp != null">#{deviceIp},if>
<if test="deviceMac != null">#{deviceMac},if>
<if test="recordingAddress != null">#{recordingAddress},if>
<if test="duration != null">#{duration},if>
<if test="trunkNumber != null">#{trunkNumber},if>
<if test="createTime != null">#{createTime},if>
trim>
这个坑刚开始踩的时候都有点懵,我们的项目中是用的PageHepler来做分页的,PageHelper里面有个机制是,当解析的sql比较复杂的时候,会加上别名,而Sharding-jdbc执行这个带有别名的sql会报错,
Error querying database. Cause: java.lang.IllegalStateException:
Must have sharding column with subquery.
The error may involve ai.injoy.outbound.mapper.TaskRecOrderMapper.
selectFirstTaskRecOrder-Inline
The error occurred while setting parameters
SQL: select count(0) from (SELECT t.* FROM ob_task_rec_order t,
ob_task_config c WHERE t.hotel_code = c.hotel_code AND
c.check_out_confirm_enable = 1 AND
date_sub(t.last_check_out_time,INTERVAL
c.check_out_confirm_time MINUTE) < SYSDATE()
AND t.last_check_out_time > SYSDATE()
AND t.task_status = 0 AND t.order_status = 5
AND t.task_result = 0 AND t.last_check_out_time like ?
AND t.out_cnt = 0 AND t.rcp_type = 'RcpType01'
order by t.create_time,t.task_id) tmp_count
解决办法是在另加一个XXX_COUNT的sql,不要让PageHelper给原始sql加上别名。
项目当中一些比较复杂的sql可能会解析错误,没法自动路由到指定的分库或者分表,Shardingsphere提供了HintManager.setDatabaseShardingValue()方法,当指定了这个方法,
public SQLRouteResult route(final List<Object> parameters) {
if (null == sqlStatement) {
sqlStatement = shardingRouter.parse(logicSQL, true);
}
return masterSlaveRouter.route(shardingRouter.route(logicSQL, parameters, sqlStatement));//shardingRouter有2个实现
}
shardingRouter有2个实现,ParsingSQLRouter和DatabaseHintSQLRouter,当指定了HintManager.setDatabaseShardingValue()方法,会构造DatabaseHintSQLRouter实例,
public RoutingResult route() {
Collection<Comparable<?>> shardingValues = HintManager.getDatabaseShardingValues();
Preconditions.checkState(!shardingValues.isEmpty());
Collection<String> routingDataSources;
routingDataSources = databaseShardingStrategy.doSharding(dataSourceNames, Collections.<RouteValue>singletonList(new ListRouteValue<>("", "", shardingValues)));
Preconditions.checkState(!routingDataSources.isEmpty(), "no database route info");
RoutingResult result = new RoutingResult();
for (String each : routingDataSources) {
result.getTableUnits().getTableUnits().add(new TableUnit(each));
}
return result;
}
本质上通过HintManager固化分库的的做法来跳过sql解析和分表的,如果一个事务里包含了多个数据库操作,就需要在每个操作前后指定强制路由的逻辑,代码很不优雅。
参考文档:https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/usage/sharding/hint/
虽然只用到了ShardingSphere里的Sharding-jdbc功能,虽然遇到了一些坑,但还是逐一解决了,目前已经满足我们的业务需要了。在使用过程中仔细阅读官方的文档和Demo是很有必要的。
整体来说,作为一些不是很复杂的sql用sharding-jdbc是很方便集成到业务系统的,而且客户端集成的好处是真正的分布式的,不同的微服务可以选择不同的策略。
值得注意的是,分表之后的运维工作尤其重要,目前我们用的是配置中心,上线新门店,需要手动去修改分表的参数。
后续关于治理和性能的监控,还需要进一步学习实践。