转载

分库分表算法方案与技术选型(二)技术选型与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...