打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
亿级流量网站架构核心技术之“数据库分库分表策略”

本文节选自《亿级流量网站架构核心技术——跟开涛学搭建高可用高并发系统》一书

张开涛 著

电子工业出版社出版

小编会从留言中选择获赞最多的前名用户免费送出此书哦!规则见文末。

  


数据库分库分表后就会涉及如何写入和读取数据的问题,应用开发人员主要关心如下几个问题。

    是否需要在应用层做改造来支持分库分表,即是在应用层进行支持,还是通过中间件层呢?

    如果需要应用层做支持,那么分库分表的算法是什么?

    分库分表后,join是否支持,排序分页是否支持,事务是否支持。

应用层还是中间件层

分库分表可以在应用层实现,也可以在中间件层实现,中间件层实现的好处是对应用透明,应用就像查单库单表一样去查询中间件层,如下图所示。

使用数据库中间件层还有一个好处是可以支持多种编程语言,而不受限于特定的语言。使用数据库中间件层可以减少应用的总数据库连接数,从而避免因为应用过多导致数据库连接不够用。缺点是除了维护中间件外,还要考虑中间件的HA/负载均衡等,增加了部署和维护的困难,因此,还是要看当前阶段有没有必要使用中间件和有没有人维护该中间件。

目前开源的数据库中间件有基于MySQL-Proxy开发的奇虎360Atlas、阿里的Cobar、基于Cobar开发的Mycat等。京东内部也有很多分库分表实现,还有如JProxy分布式数据库实现,截止本书出版前暂未开源。Atlas只支持分表或分库(sharding版本)、读写分离等,不支持跨库分表(如分3个库每个库3张表),sharding版本不支持跨库操作(跨库事务/跨库join等)。Cobar支持分库不支持分表(如每个库3个表),不支持跨库join/分页/排序等。Mycat支持分库分表、读写分离、跨库弱事务支持,对跨库join等有限支持(内存聚合)。这些中间件目前主要支持MySQL,但MyCat也提供了对Oracle等数据库的支持。

而应用层可以在JDBC驱动层、DAO框架层,如iBatis/Mybatis/Hibernate/JPA上完成。如当当的sharding-jdbcJDBC驱动层实现,而阿里的cobar-client是基于DAO框架iBatis实现,如下图所示。

应用系统直接在应用代码中耦合了分库分表逻辑,然后通过如iBatis/JDBC直接分库分表实现。

相对来说JDBC层实现的灵活性更好,侵入性更少,因此,本文选择了开源的当当的Sharding-jdbc来进行讲解。Sharding-jdbc直接封装JDBC API,所以迁移成本很低,可以对如iBatisMyBatisHibernateJPADAO框架提供支持,目前只提供了MySQL的支持,未来计划支持如Oracle等数据库。sharding-jdbc支持分库分表、读写分离、跨库join/分页/排序等、弱事务、柔性事务(最大努力送达)。因此,在我们的场景中需要使用的分库分表/弱事务功能它都有。

分库分表策略

分库分表策略是指按照什么算法或规则进行存储,它会影响数据的写入和读取,比如,按照订单ID分库分表,那么就很难按照客户维度进行订单查询。因此,在进行分库分表时需要慎重考虑使用什么策略。常见的策略有取模、分区、路由表等。

1.取模

我们可以按照数值型主键取模来进行分库分表,也可以按照字符串主键哈希取模进行分库分表,常见的如订单表按照订单ID分库分表,用户订单表按照用户ID分库分表,产品表按照产品ID分库分表。取模的优点是数据热点分散,缺点是按照非主键维度进行查询时需要跨库/跨表查询,扩容需要建立新集群并进行数据迁移。如果想减少扩容时带来的麻烦,可以在初期规划时冗余足够数量的分库分表,比如一年规划只需要分2个库4个表,可以冗余设计为4个库8个表,0-1库在机器12-3库在机器2,如果遇到性能问题时可以把13库移到新的机器上。如果遇到容量问题,则可以按照如下步骤进行扩容。

每台物理机上有两个数据库实例,当遇到数据库性能瓶颈时首先可以通过升级硬件解决,如HDD换成SATA SSDSATA SSD换成PCIe SSDNVMe SSD;升级硬件之后,瓶颈可能是磁盘空间或者网卡带宽。如果还是不能解决性能问题,接着通过扩容物理机来解决性能瓶颈。

当通过扩容物理机无法解决性能问题或者当单表容量遇到瓶颈,可以进行成倍扩容,4个库扩容为8个库,如下图所示。

成倍扩容后的数据迁移可以这样实现,先挂数据库主从(order_4-->order_0),当数据库主从同步完成后,停应用写数据库并等待一段时间以保证主从同步完成,接着更新分库分表规则并启动应用进行写库,最后删除各个库的冗余数据即可。

分库数量不是越多越好,可以参考“第12章连接池线程池详解”相关章节,分库太多会导致消耗更多的数据库连接,并且应用会创建更多的线程。这种情况下数据代理中间件会是更好的选择。

2.分区

可按照时间分区、范围分区进行分库分表,时间分区规则如一个月一个表、一年一个库。范围分区规则如0~2000万一个表,2000~4000万一个表。如果分区规则很复杂,则可以有一个路由表来存储分库分表规则。分区的缺点是存在热点,但是易于水平扩展,能避免数据迁移。

另外,也可以取模+分区组合使用。比如,京东一元夺宝先按抢宝项Hash分库,然后按抢宝期区间段分表,更多细节可扫二维码参考《京东一元抢宝系统的数据库架构优化》。

使用sharding-jdbc分库分表

在数据库设计起初一般都是单库单表设计,随着数据量的增长将带来存储容量和写/读性能瓶颈问题。如果是容量问题,则可以通过分库到多台机器解决。而引起写/读问题的主要原因是记录太多(几千万到一亿)、列数太多、索引太多、查询太复杂等引发单表出现性能问题。出现性能问题要从很多维度去分析,如果经过分析得以解决,则我们可以继续单库分表,如果单库分表确实有问题,则要进行分库分表解决。比如,电商系统的商品数据库,就会存在这种问题。为了演示方便,我们将商品数据库分为2个库,每个库2个表,使用MySQL数据库。

1.数据库DDL

创建2个库,每个库2个表,即N01M01,然后执行如下脚本。

CREATEDATABASEIF NOT EXISTS product_N;

CREATE TABLEproduct_M(

  id bigint primary key,

  title varchar(255),

  last_modified datetime

)ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.数据源配置

使用DBCP 2配置2DataSourceabstractDataSource把公共部分抽取为父Bean,可参考“第12章连接池线程池详解”部分进行配置。

<bean id="dataSource_0"parent="abstractDataSource">

<property name="url"

value="jdbc:mysql://192.168.1.2:3306/product _0"/>

<property name="username"value="root"/>

<property name="password"value="root"/>

</bean>

 

<bean id="dataSource_1"parent="abstractDataSource">

<property name="url" value="jdbc:mysql://192.168.1.3:3306/product_1"/>

<property name="username"value="root"/>

<property name="password"value="root"/>

</bean>

sharding-jdbc分库分表配置

本文使用sharding-jdbc 1.3.2依赖。

<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-transaction</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-config-spring</artifactId>
<version>1.3.2</version>
</dependency>

如下配置可实现分2个库,每个库分2个表。

<!-- 分库规则 -->

<rdb:strategy id="dataSourceStrategy"

          sharding-columns="id"

          algorithm-expression=

"dataSource_${Math.floorMod(id.longValue(),2L)}"/>

<!-- 分表规则 -->

<rdb:strategyid="productTableStrategy"

          sharding-columns="id"

          algorithm-expression=

"product_${Math.floorMod(Math.floorDiv(id.longValue(),2L),2L)}"/>

 

<!-- 分库分表数据源 -->

<rdb:data-source id="shardingDataSource">

<!-- 使用的真实数据源 -->

<rdb:sharding-ruledata-sources="dataSource_0,dataSource_1">

<rdb:table-rules>

<!-- 分表规则:分库策略、分表策略、逻辑表名、实际表名-->

<rdb:table-rule

                   database-strategy="dataSourceStrategy"

                   table-strategy="productTableStrategy"

                   logic-table="product"

                   actual-tables="product_${0..1}"/>

</rdb:table-rules>

</rdb:sharding-rule>

</rdb:data-source>

分库/分表策略:使用sharding-columns指定分库分表键,algorithm-expression指定分库分表策略,我们按照ID分了两个库,每个库两张表。算法为:库ID = id % 库数量,表ID = id / 库数量 % 单库表数量。另一种算法为:库ID = id % 表总数量 / 单库表数量,表ID = id % 表总数量。

分库分表数据源:配置分库数据源,其会按照分库策略(table-rule/ database-strategy)选择使用哪一个数据源。然后使用table-strategy配置分表策略来选择使用哪一张表。logic-table是逻辑表名,写SQL时使用这个标识,然后会根据分表策略和actual-tables决定真实表名。

sharding-jdbc的分库分表算法是独立的,即分库可以使用一套规则,分表可以使用一套规则,如订单库按照商家ID分库,然后每个库按照订单ID分表。如果你的分库分表策略太复杂,则可以使用algorithm-class指定SingleKeyDatabaseShardingAlgorithm/MultipleKeysDatabaseShardingAlgorithmSingleKeyTableShardingAlgorithm/MultipleKeysTableShardingAlgorithm分库分表实现算法,其支持单个键/多个组合键作为分片键。分库分表算法支持如=BETWEENIN等多维度实现。

1.事务管理器配置

配置弱事务管理器在大多数场景够用了。

<!-- 事务管理器,此处使用弱事务 -->

<bean id="transactionManager"

 class="org.springframework.jdbc.datasource.

DataSourceTransactionManager">

<property name="dataSource"ref="shardingDataSource"/>

</bean>

此处我们使用了弱事务机制,如下图所示,事务不是原子的,可能提交分库1事务后,提交分库2事务失败,造成跨库事务不一致。可以考虑sharding-jdbc提供的柔性事务实现。

柔性事务目前支持最大努力送达,未来计划支持TCCTry-Confirm-Cancel)。最大努力送达是当事务失败后通过最大努力反复尝试送达操作实现,是在假定数据库操作一定可以成功的前提下进行的,保证数据最终的一致性。其适用场景是幂等性操作,如根据主键删除数据、带主键地插入数据、更新记录最后状态(如商品上下架操作)。

Sharding-JDBC的最大努力送达型柔性事务分为同步送达和异步送达两种,同步送达不需要ZooKeeperelastic-job,内置在柔性事务模块中。但是在有些场景下,事务需要经过一段时间才能准备完毕,则可通过异步送达,异步送达比较复杂,是对柔性事务的最终补偿,不能和应用程序部署在一起,需要额外地通过elastic-job实现。异步送达是对同步送达的有效补充,但即使不配置异步送达,同步送达机制也可以正常使用。最大努力送达型事务也可能出现错误,即无论如何补偿都不能正确提交。为了避免反复尝试带来的系统开销,同步送达和异步送达均可配置最大重试次数,超过最大重试次数的事务将进入失败列表,需要定期进行人工干预。具体使用请参考sharding-jdbc官方文档。

在一般场景中,只要保证单库事务能工作即可,跨库通过一些机制保证最终一致性即可,在高并发高可用的场景下不应该采用强一致模型。

2.代码逻辑

在实际使用时,通过JDBC模板和编程式完成事务开发,通过AOP机制配置事务。

//获取分库分表数据源

DataSource shardingDataSource =

(DataSource) ctx.getBean("shardingDataSource");

//创建JdbcTemplate

JdbcTemplate jdbcTemplate = new JdbcTemplate(shardingDataSource);

//获取事务管理器

AbstractPlatformTransactionManagertransactionManager =

(AbstractPlatformTransactionManager)ctx.getBean("transactionManager");

//创建事务模板

TransactionTemplate transactionTemplate =

new TransactionTemplate(transactionManager);

//执行SQLproduct是逻辑表名、id是分库分表键)

transactionTemplate.execute(new TransactionCallbackWithoutResult(){

   @Override

protected void doInTransactionWithoutResult(TransactionStatustransactionStatus) {

       jdbcTemplate.update("insert intoproduct(id,title,last_modified)values (?, ?, ?)", 1L, "title",new Date());

    }

});

整体使用和非分库分表没什么区别,在实际执行时,逻辑表名product会被替换为如product_1这种实际表名,即实际SQL会是如下样子:

INSERTINTO product_1 (id, title, last_modified) VALUES (?, ?, ?)

使用sharding-jdbc读写分离

随着数据库读访问量的增长,主库不能承受更多的读访问,此时,可以通过给主库挂从库,然后把读访问分流到从库来减少主库的压力。sharding-jdbc通过简单的配置就可以支持读写分离。

读写分离数据源配置

通过如下配置就可以实现读写分离,即配置一个主库和两个从库。

<rdb:master-slave-data-sourceid="dataSource_0"

master-data-source-ref="dataSource_master_0"

slave-data-sources-ref="dataSource_slave_0,dataSource_slave_1"/>

使用如上配置读请求会通过路由到达从库,但是,假设刚刚写入数据,此时立即读的话可能读不到,因为MySQL默认使用异步复制,复制是有一定延迟的。因此要想在写完后立即读数据,可以通过Hint机制强制读取主库。

HintManager.getInstance().setMasterRouteOnly();

jdbcTemplate.queryForList("selectid, title from product where id=?", 1L);

Sharding-JDBC的读写分离为了最大限度避免由于同步延迟而产生强制读取主库的场景,在更新方面做了优化,在一个请求线程中,只要存在对数据库的更新操作,则在此操作之后的任何对数据库的访问都会自动通过路由达到主库。因此,在写后读的场景中不需要使用HintManager,只有在读场景下,需要强制读主库时,才使用HintManager强制通过路由到达主库。

通过数据库主从分离读写,但不是从库越多越好,当从库同步遇到瓶颈,或者通过从库无法满足查询需求时,应该选择数据异构。  

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
ShardingJdbc分库分表实战案例解析(下)
笔者带你剖析轻量级Sharding中间件——Kratos1.x
Sharding-JDBC—分库分表实例【面试+工作】
深度认识Sharding
分库分表方案之Shark
订单中心架构设计与实践
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服