转载

分库分表算法方案与技术选型(二)技术选型与sharding-jdbc实现

  • 框架比较
  • 主键生成策略
  • sharding-jdbc 代码实现样例,如需源码可在后文中查看 可以按需阅读文章

常见框架

除了原生JDBC,网上常见分库分表框架有: 当当网 sharding-jdbc alibaba.cobar (是阿里巴巴(B2B)部门开发) MyCAT(基于阿里开源的Cobar产品而研发) 蚂蚁金服 ZDAL (开源) 蘑菇街 TSharding

当然除了这些,还有很多各自公司提出的框架,但是根据用户量较高的为以上几种。 其中自从出现基于cobar的MyCAT,也很少人用cobar了。ZDAL虽然也是开源,但是很少文章和使用反馈,不支持MongoDb,交流活跃度也比较低。

所以本次文章来 比较一下活跃度较高的sharding-jdbc和MyCAT 。

扩展阅读:当当网做的不错的,除了sharding-jdbc,还有elastic-job用于定时任务分片

对比概览

主要指标 Sharding-jdbc Mycat
ORM支持 任意 任意
事务 自带弱XA、最大努力送达型柔性事务BASE 自带弱XA
分库 支持 支持
分库 支持 不支持单库分表
开发 开发成本高,代码入侵大 开发成本小,代码入侵小
所属公司 当当网 基于阿里Cobar二次开发,社区维护
数据库支持 任意 Oracle、 SQL Server、 Mysql、DB2、mongodb
活跃度 也有不少的企业在最近几年新项目使用 社区活跃度很高,一些公司已在使用
监控 无 有
读写分离 支持 支持
资料 资料少、github、官网、网上讨论贴 资料多,github、官网、Q群、书籍
运维 维护成本低 维护成本高
限制 部分JDBC方法不支持、SQL语句限制 SQL语句限制
连接池 druid版本 无要求

关键指标对比

1.开发与运维成本

sharding-jdbc

  • sharding-jdbc是一个轻量级框架,不是独立运行中间件,以工程的依赖jar的形式提供功能,无需额外部署,可以理解为增强版JDBC驱动。
  • 对运维、DBA人员无需感知代码与分片策略规则,运维只需要维护执行建立表和数据的迁移。
  • 相对Mycat这是sharding-jdbc的优势,减少了部署成本以及DBA学习成本。
  • 原理是通过规则改写原sql,如select * from A 根据规则变成select * from A_01,运行执行sql时就会向mysql服务器传select * from A_01指令。

MyCat

  1. 而MyCat并不是业务系统代码里面的配置,而是独立运行的中间件,所以配置都会交给DBA执行。
  2. 对于DBA来说,他是一个在mysql Server前,增加一层代理,mycat本身不存数据,数据是在后端的MySQL上存储的,因此数据可靠性以及事务等都是MySQL保证的。
  3. 为了减少迁移数据的风险,在 上一章推荐的增量迁移算法方案 (推荐大家阅读)讲述如何分片达到降低风险。 若用MyCat,DBA需要配置多次的增量分片规则,每配置一次则要重启一次,才能达到一轮的数据迁移。实际上MyCat down掉的时系统都不能对数据库查询,实际依然对所有用户有影响。
  4. 然而sharding-jdbc都在代码实现路由规则,则可以减少DBA操作次数和系统重启次数,进而减少影响用户数。

推荐阅读第一章的第五节才比较好理解上述3~4点 分库分表算法方案与技术选型(一)

  1. proxy整合大数据思路,将 OLTP 和 OLAP 分离处理,可能会对大数据处理的系统比较适合,毕竟数据工作不一定有java后端系统。

该点总结:sharding-jdbc增量分片和增量迁移数据效果更佳,mycat比较适合大数据工作

备注: sharding-jdbc增强了JDBC驱动部分功能,但同时也限制部分原生JDBC接口的使用。具体限制参考: 限制情况: dangdangdotcom.github.io/sharding-jd… 这个文档现在好像访问不了 附:官网文档 官网源码

MyCat配置样例 MyCat配置样例2

2.分库分表能力

  • sharding-jdbc另一个优势是他的分表能力,可以不需要分库的情况下单库分表。
  • MyCAT不能单库分多表,必须分库,这样就会造成让DBA增加机器节点,即使不增加机器节点,也会在同一个机器上增加mysql server实例,若使用sharding-jdbc单库分多表,则DBA只需要执行建立表语句即可。

3.事务

首先说说XA, XA 多阶段提交的方式,虽然对分布式数据的完整性有比较好的保障,但会极大的降影响应用性能。

  • sharding-jdbc和mycat支持弱XA,弱 XA 就是分库之后的数据库各自负责自己事务的提交和回滚,没有统一的调度器集中处理。这样做的好处是天然就支持,对性能也没有影响。但一旦出问题,比如两个库的数据都需要提交,一个提交成功,另一个提交时断网导致失败,则会发生数据不一致的问题,而且这种数据不一致是永久存在的。

  • 柔性事务是对弱 XA 的有效补充。柔性事务类型很多。 Sharding-JDBC 主要实现的是最大努力送达型。即认为事务经过反复尝试一定能够成功。如果每次事务执行失败,则记录至事务库,并通过异步的手段不断的尝试,直至事务成功(可以设置尝试次数,如果尝试太多仍然失败则入库并需要人工干预)。在尝试的途中,数据会有一定时间的不一致,但最终是一致的。通过这种手段可以在性能不受影响的情况下牺牲强一致性,达到数据的最终一致性。最大努力送达型事务的缺点是假定事务一定是成功的,无法回滚,因此不够灵活。

备注:还有一种柔性事务类型是 TCC,即 Try Confirm Cancel。可以通过事务管理器控制事务的提交或回滚,更加接近原生事务,但仍然是最终一致性。其缺点是需要业务代码自行实现 Try Confirm Cancel 的接口,对现有业务带来一定冲击。Sharding-JDBC 未对 TCC 的支持。

4.监控

为什么要监控,因为上述事务的弱XA、最大努力送达型,其实还是有概率失败。

  • MyCat就要监控页面,监控MyCat与Mysql server的心跳,运维人员可以看到
  • 而sharding-jdbc没有监控事务是不是最终执行了,可能需要改写源码,如果有个分片没执行成功就发一下短信、钉钉之类的。MyCat监控配置样例

5.语句限制

  • sharding-jdbc分库分表使用 like 查询是有限制的。目前 Shariding-JDBC 不支持 like 语句中包含分片键,但不包含分片键的 like 语句可以正确执行。 至于 like 性能问题,是与数据库相关的,Shariding-JDBC 仅仅是解析 SQL 以及路由至正确的数据源而已。 是否会查询所有的库和表是根据分片键决定的,如果 SQL 中不包括分片键,就会查询所有库和表,这个和是否有 like 没有关系。
  • MyCat没有限制

主键生成器

因为分库分表的情况下,对于订单号、userId不能使用自增的形式,最好在未分库分表前,做好订单号的规则,不使用uuid,因为会带字母。下面介绍雪花算法和算法的变体。实现还是推荐使用redis保证分布式唯一吧。

1.雪花算法

雪花算法解析 结构 snowflake的结构如下(每部分用-分开):

时间戳 机器id 12bit流水号
0 - 0000000000 0000000000 0000000000 0000000000 0 00000 - 00000 000000000000

上面每个位的值为0/1

其核心思想是: 第一bit为未使用,接下来的41 bit为毫秒级时间(41位的长度可以使用69年), 然后是5bit datacenterId和5bit workerId(10位的长度最多支持部署1024个节点) , 最后12bit 是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 一共加起来刚好64 bit,为一个Long型。(转换成字符串长度为18)。

2.自定义生成规则

大多数的号都用上述方法即可,只是其中一些场景会特殊规则,如放款/还款的支付流水号。 为了用于便于人为阅读,如财务核算时需要阅读流水号,导出数据进金蝶软件的场景,用于适应金蝶软件导入规则。 下述这种就太长了,只能用String存储,因为Long最大值为2^63-1=9223372036854775807。这个是个20位数字。

业务类型2位数 年月日时分秒毫秒 机器id 计数位4位数 父级id的hash值
01 20190901 01 01 01 111 00000 1234 4831

第一节 两位是用于表示业务类型,足够一个系统有99个业务类型,如01表示用户还款,02表示用户借款。如果更多业务类型,可能该考虑拆系统,如果真的不够可以写3位。当然这个不是必要,第一节只是用来容易人为识别。 第二节 是时间,像支付宝支付的流水号就是有带时间的,这样用户或者客服可以直观看出这个单是什么时候生成,排查问题也比较方便 第三节 是机器id,由代码获取ip,然后自定义算法,生成一个5位数,记得不要写真实ip,不然就会被所有人发现了。 第四节 是计数位,表示同一个ip下在同一个毫秒下,可以有9999次计数

共28位,已经超出long的最大值,所以存String类型。

有些公司会有第五节,第五节 是父级id的hash值,意思是假如这个是还款支付流水号,最后四位可以是userId的hash值。

这样做是有原因的,最后4位可以方便的根据支付流水号定位到物理表坐标。因为如果这个是支付流水号,假如这个支付流水号只有前面四节,根据上一章的第四、五方案一致性hash,根据会算出 分库 和 分表 的所在位置。但是这样就不方便开发、运维人为上mysql server找到数据。所以会填上userId的hash值(如 id mode 64)作为第五节的前两位表示 分库 位置,userId / 64 mod 64作为 分表 坐标。

例如 用户id % 64 取余 最多可以分64张表,而目前可能用不到这么多,每相邻4个数字分配到一张表,共16张表,既 userID % 64 / 4 * 4 ,而这个地方存储 userID % 64 即可,不必存最终分表的结果(这个算法请阅读上一章)。

但是我认为第五节不是很合理,这种方式不方便后续做扩容,mod 64 可能不足以支撑业务时,可能要分128片(mod 128)的时候,可能分表的规则变更了,但是订单号已无法进行变更,这些订单号也不能去update,已经给财务那边做核算了。

Sharding-jdbc分开分表开发样例

代码样例具体描述,下述关键的开发点。 具体源码请到我的gitee地址 sharding-jdbc-example 。

sharding-jdbc分片的开发主要几个关键点:

  1. 在xml中配置基础数据源对象:两个真实数据库的DataSource,如同平常一样无特殊处理。 新增的是分片数据源、规则和真实数据库的映射关系
<bean id="shardingDataSource"
		class="com.dangdang.ddframe.rdb.sharding.api.ShardingDataSource"
		primary="true">
		<constructor-arg ref="shardingRule" />
	</bean>
	<!-- 配置好dataSourceRulue,即对数据源进行管理 -->
	<bean id="dataSourceRule"
		class="com.dangdang.ddframe.rdb.sharding.api.rule.DataSourceRule">
		<constructor-arg>
			<map>
				<entry key="db1" value-ref="db1" />
				<entry key="db2" value-ref="db2" />
			</map>
		</constructor-arg>
	</bean>
复制代码
  1. 然后就是配置分库分表的策略,其中UserDbShardingAlgorithm,UserTbShardingAlgorithm需要在java代码里面实现
<!-- 分库策略 -->
	<bean id="userDatabaseShardingStrategy"
		class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.DatabaseShardingStrategy">
		<constructor-arg index="0" value="user_id" />
		<constructor-arg index="1">
			<bean
				class="com.dizang.sharding.infrastrusture.rule.UserDbShardingAlgorithm" />
		</constructor-arg>
	</bean>

	<!-- 分表策略 -->
	<bean id="userTableShardingStrategy"
		class="com.dangdang.ddframe.rdb.sharding.api.strategy.table.TableShardingStrategy">
		<constructor-arg index="0" value="user_id" />
		<constructor-arg index="1">
			<bean
				class="com.dizang.sharding.infrastrusture.rule.UserTbShardingAlgorithm" />
		</constructor-arg>
	</bean>
复制代码
  1. java代码编写分库策略 需要继承SingleKeyDatabaseShardingAlgorithm分开规则类,重写equal等于、大于、小于时的路由规则
public class UserDbShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<Long>{  

    /** 
     * sql 中关键字 匹配符为 =的时候,表的路由函数 
     */  
    public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {  
        for (String each : availableTargetNames) {  
            if (each.endsWith(shardingValue.getValue() % 64 / 32 * 32 + "")) {  
                return each;  
            }  
        }  
        throw new IllegalArgumentException();  
    }  

    /** 
     * sql 中关键字 匹配符为 in 的时候,表的路由函数 
     */  
    public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {  
        Collection<String> result = new LinkedHashSet<String>(availableTargetNames.size());  
        for (Long value : shardingValue.getValues()) {  
            for (String tableName : availableTargetNames) {  
                if (tableName.endsWith(value % 64 / 32 * 32 + "")) {  
                    result.add(tableName);  
                }  
            }  
        }  
        return result;  
    }  

    /** 
     * sql 中关键字 匹配符为 between的时候,表的路由函数 
     */  
    public Collection<String> doBetweenSharding(Collection<String> availableTargetNames,  
            ShardingValue<Long> shardingValue) {  
        Collection<String> result = new LinkedHashSet<String>(availableTargetNames.size());  
        Range<Long> range = (Range<Long>) shardingValue.getValueRange();  
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {  
            for (String each : availableTargetNames) {  
                if (each.endsWith(i % 64 / 32 * 32 + "")) {  
                    result.add(each);  
                }  
            }  
        }  
        return result;  
    }  

}  
复制代码
  1. java代码编写分表策略 需要继承SingleKeyTableShardingAlgorithm分开规则类,重写equal等于、大于、小于时的路由规则
public class UserTbShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long>{  

    /** 
     * sql 中 = 操作时,table的映射
     *  
     */  
    public String doEqualSharding(Collection<String> tableNames, ShardingValue<Long> shardingValue) {  
        for (String each : tableNames) {  
            if (each.endsWith(String.valueOf(shardingValue.getValue() / 64 % 64 / 32 * 32 ))) {  
                return each;  
            }  
        }  
        throw new IllegalArgumentException();  
    }  

    /** 
     * sql 中 in 操作时,table的映射 
     */  
    public Collection<String> doInSharding(Collection<String> tableNames, ShardingValue<Long> shardingValue) {  
        Collection<String> result = new LinkedHashSet<String>(tableNames.size());  
        for (Long value : shardingValue.getValues()) {  
            for (String tableName : tableNames) {  
                if (tableName.endsWith(String.valueOf(value / 64 % 64 / 32 * 32 ))) {  
                    result.add(tableName);  
                }  
            }  
        }  
        return result;  
    }  

    /** 
     * sql 中 between 操作时,table的映射 
     */  
    public Collection<String> doBetweenSharding(Collection<String> tableNames,  
            ShardingValue<Long> shardingValue) {  
        Collection<String> result = new LinkedHashSet<String>(tableNames.size());  
        Range<Long> range = (Range<Long>) shardingValue.getValueRange();  
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {  
            for (String each : tableNames) {  
                if (each.endsWith(String.valueOf(i / 64 % 64 / 32 * 32))) {  
                    result.add(each);  
                }  
            }  
        }  
        return result;  
    }  

}  
复制代码
原文  https://juejin.im/post/5d739ba7e51d4561c541a729
正文到此结束
Loading...