ShardingJDBC

前面章节我们已经介绍了分布式数据库相关的概念和ShardingSphere项目,这篇笔记我们具体学习Sharding-JDBC的使用。

搭建工程

我们这里以SpringBoot工程为例,介绍如何集成Sharding-JDBC。

环境准备

Sharding-JDBC的分布式调度依赖ZooKeeper,我们这里使用Docker启动ZooKeeper服务:

docker run -p 2181:2181 --name zookeeper -d zookeeper

此外,还需要根据实际情况搭建MySQL数据库节点,前面章节已经介绍过如何搭建主从同步方式的MySQL数据库,我们根据实际情况搭建即可。

引入Maven依赖

Sharding-JDBC提供了起步依赖,我们这里使用当前最新的5.1.1版本。

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.1</version>
</dependency>

此外我们还需要引入MySQL驱动和持久层框架,后文会以MyBatis为例进行介绍。

注:由于ShardingSphere-JDBC的不同版本中API经常发生变化,后续代码不保证在除5.1.1以外的版本中能够运行。

读写分离

这里我们学习如何配置和使用Sharding-JDBC实现读写分离。我们准备两个数据库节点,master为主节点,slave为从节点。

实现读写分离

使用Sharding-JDBC需要编写大量的配置,但如果我们已经理解分布式数据库的基本概念,理解起来这些配置并不困难。下面配置中包含了主从库的连接信息,配置好后Sharding-JDBC知道了哪个是主库,哪个是从库,此时就可以针对我们的SQL操作进行读写分离的路由了。

# 配置使用集群模式,信息存储在ZooKeeper
spring.shardingsphere.mode.type=Cluster
spring.shardingsphere.mode.repository.type=ZooKeeper
spring.shardingsphere.mode.repository.props.namespace=demosharding
spring.shardingsphere.mode.repository.props.server-lists=127.0.0.1:2181
spring.shardingsphere.mode.repository.props.retryIntervalMilliseconds=500
spring.shardingsphere.mode.repository.props.timeToLiveSeconds=60
# 显示执行的SQL语句(方便调试,生产没必要打开)
spring.shardingsphere.props.sql-show=true
# 配置主从库数据源名称
spring.shardingsphere.datasource.names=master,slave
# 配置主库
spring.shardingsphere.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master.jdbc-url=jdbc:mysql://127.0.0.1:3306/netstore
spring.shardingsphere.datasource.master.username=root
spring.shardingsphere.datasource.master.password=root
# 配置从库
spring.shardingsphere.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave.jdbc-url=jdbc:mysql://127.0.0.1:3306/netstore
spring.shardingsphere.datasource.slave.username=root
spring.shardingsphere.datasource.slave.password=root
# 读写分离类型选择静态模式
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.type=Static
# 写操作使用主库数据源
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.write-data-source-name=master
# 读操作使用从库数据源
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.read-data-source-names=slave
# 配置从库负载均衡算法,这里设置为轮询(用于多个从库的场景,本例子中无具体效果)
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.load-balancer-name=rr
spring.shardingsphere.rules.readwrite-splitting.load-balancers.rr.type=ROUND_ROBIN

我们继续编写一些代码进行测试。下面Java代码非常简单,我们创建了一个User对象,然后将其存储到数据库中,最后再读取出来。代码中使用了MyBatis和tkMyBatis框架,具体可以参考MyBatis相关章节。

package com.gacfox.demo;

import com.gacfox.demo.dao.UserMapper;
import com.gacfox.demo.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class DemoTests {
    @Resource
    private UserMapper userMapper;

    @Test
    public void testDao() {
        // 插入数据
        User user = new User();
        user.setId(1L);
        user.setUsername("Lucy");
        user.setAge(18);
        user.setGender("female");
        userMapper.insert(user);

        // 读取数据
        userMapper.selectByPrimaryKey(1L);
    }
}

执行上述代码后,会得到类似如下输出:

ShardingSphere-SQL: Logic SQL: INSERT INTO t_user  ( id,username,age,gender ) VALUES( ?,?,?,? )
ShardingSphere-SQL: SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
ShardingSphere-SQL: Actual SQL: master ::: INSERT INTO t_user  ( id,username,age,gender ) VALUES( ?,?,?,? ) ::: [1, Lucy, 18, female]
ShardingSphere-SQL: Logic SQL: SELECT LAST_INSERT_ID()
ShardingSphere-SQL: SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
ShardingSphere-SQL: Actual SQL: slave ::: SELECT LAST_INSERT_ID()
ShardingSphere-SQL: Logic SQL: SELECT id,username,age,gender  FROM t_user  WHERE  id = ?
ShardingSphere-SQL: SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
ShardingSphere-SQL: Actual SQL: slave ::: SELECT id,username,age,gender  FROM t_user  WHERE  id = ? ::: [1]

从上面输出中我们可以看出Sharding-JDBC的一些执行逻辑,我们执行的SQL操作会被Sharding-JDBC看作Logic SQL,框架会帮我们按照配置的规则对Logic SQL进行进一步处理,并转换成真正执行的Actual SQL,这才是真正发送到数据库执行的SQL语句。我们的写操作被发送给了master库执行,而读操作被发送给了slave库执行,此时我们已经成功使用Sharding-JDBC实现了读写分离。

使用事务

如果我们使用事务,Sharding-JDBC会将操作都放在主库上,以保证事务的正确执行。下面是一些测试代码,代码中我们使用TransactionTemplate将一个写操作和一个读操作封装到了一个事务中。

transactionTemplate.executeWithoutResult(status -> {
    userMapper.insert(user);
    userMapper.selectByPrimaryKey(1L);
});

此时执行的输出如下。

ShardingSphere-SQL: Logic SQL: INSERT INTO t_user  ( id,username,age,gender ) VALUES( ?,?,?,? )
ShardingSphere-SQL: SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
ShardingSphere-SQL: Actual SQL: master ::: INSERT INTO t_user  ( id,username,age,gender ) VALUES( ?,?,?,? ) ::: [1, Lucy, 18, female]
ShardingSphere-SQL: Logic SQL: SELECT LAST_INSERT_ID()
ShardingSphere-SQL: SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
ShardingSphere-SQL: Actual SQL: master ::: SELECT LAST_INSERT_ID()
ShardingSphere-SQL: Logic SQL: SELECT id,username,age,gender  FROM t_user  WHERE  id = ?
ShardingSphere-SQL: SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
ShardingSphere-SQL: Actual SQL: master ::: SELECT id,username,age,gender  FROM t_user  WHERE  id = ? ::: [1]

可以看到真实的SQL操作都被放在了master库上。

垂直分片

垂直分片很好理解,这里我们直接编写一个例子,我们的例子中有两个数据表,t_user位于库node0中,t_order位于库node1中,两张表在逻辑上具有关联性。我们将这两张表划分到两个库中,这就是最简单的一种数据垂直分片。

# 配置使用集群模式,信息存储在ZooKeeper
spring.shardingsphere.mode.type=Cluster
spring.shardingsphere.mode.repository.type=ZooKeeper
spring.shardingsphere.mode.repository.props.namespace=demosharding
spring.shardingsphere.mode.repository.props.server-lists=127.0.0.1:2181
spring.shardingsphere.mode.repository.props.retryIntervalMilliseconds=500
spring.shardingsphere.mode.repository.props.timeToLiveSeconds=60
# 显示执行的SQL语句(方便调试,生产没必要打开)
spring.shardingsphere.props.sql-show=true
# 配置数据源名称
spring.shardingsphere.datasource.names=node1,node2
# 配置分库1
spring.shardingsphere.datasource.node0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.node0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.node0.jdbc-url=jdbc:mysql://127.0.0.1:3308/netstore
spring.shardingsphere.datasource.node0.username=root
spring.shardingsphere.datasource.node0.password=root
# 配置分库2
spring.shardingsphere.datasource.node1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.node1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.node1.jdbc-url=jdbc:mysql://127.0.0.1:3309/netstore
spring.shardingsphere.datasource.node1.username=root
spring.shardingsphere.datasource.node1.password=root
# 配置垂直分片规则
spring.shardingsphere.rules.sharding.tables.t_user.actual-data-nodes=node0.t_user
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=node1.t_order

此时如果我们在Java代码中对t_usert_order进行插入操作,我们会发现SQL被分别路由到了两个库中。

不过像上面这种垂直分片时,跨库连接查询是无法实现的,例如SQL语句select o.order_no from t_order o inner join t_user u on o.user_id = u.id where u.username=#{username}执行时,会报错在分库中找不到某个表,此时我们只能手动在应用层进行连接了。Sharding-JDBC对连接查询的要求比较苛刻,有关连接查询将在后文介绍。

水平分片

水平分片是指将一个表的不同行拆分到多个库或表中,它配置起来要更加复杂,我们这里直接看一个例子,例子中用户表和订单表被水平拆分到了两个库中,node0数据库中存储t_usert_order0t_order1,node1数据库中同样存储t_usert_order0t_order1,用户表进行了分库,订单表进行了分库分表,同一份数据会分成多个分片存储在两个库中。

# 配置使用集群模式,信息存储在ZooKeeper
spring.shardingsphere.mode.type=Cluster
spring.shardingsphere.mode.repository.type=ZooKeeper
spring.shardingsphere.mode.repository.props.namespace=demosharding
spring.shardingsphere.mode.repository.props.server-lists=127.0.0.1:2181
spring.shardingsphere.mode.repository.props.retryIntervalMilliseconds=500
spring.shardingsphere.mode.repository.props.timeToLiveSeconds=60
# 显示执行的SQL语句(方便调试,生产没必要打开)
spring.shardingsphere.props.sql-show=true
# 配置数据源名称
spring.shardingsphere.datasource.names=node0,node1
# 配置分库1
spring.shardingsphere.datasource.node0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.node0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.node0.jdbc-url=jdbc:mysql://127.0.0.1:3308/netstore
spring.shardingsphere.datasource.node0.username=root
spring.shardingsphere.datasource.node0.password=root
# 配置分库2
spring.shardingsphere.datasource.node1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.node1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.node1.jdbc-url=jdbc:mysql://127.0.0.1:3309/netstore
spring.shardingsphere.datasource.node1.username=root
spring.shardingsphere.datasource.node1.password=root
# 配置t_user分库规则
spring.shardingsphere.rules.sharding.tables.t_user.actual-data-nodes=node$->{0..1}.t_user
spring.shardingsphere.rules.sharding.tables.t_user.database-strategy.standard.sharding-column=id
spring.shardingsphere.rules.sharding.tables.t_user.database-strategy.standard.sharding-algorithm-name=moduserds
# 配置t_order分库分表规则
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=node$->{0..1}.t_order$->{0..1}
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=modorderds
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=modordertb
# 配置分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.moduserds.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.moduserds.props.algorithm-expression=node$->{id % 2}
spring.shardingsphere.rules.sharding.sharding-algorithms.modorderds.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.modorderds.props.algorithm-expression=node$->{user_id % 2}
spring.shardingsphere.rules.sharding.sharding-algorithms.modordertb.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.modordertb.props.algorithm-expression=t_order$->{id % 2}

上面配置确实比较复杂,看得人头晕目眩,我们可以按不同的表来分开看。t_user表相关的规则只配置了database-strategy分库规则,规则中使用id % 2作为分片规则,将数据路由到两个库中;t_order表的规则既配置了database-strategy分库规则也配置了table-strategy分表规则,将数据路由到了2个库共4张表中。注意配置中类似$->{}的写法,这是一种ShardingSphere使用的表达式语言,例如node$->{0..1}.t_order$->{0..1}实际上可以看作node0.t_order0,node0.t_order1,node1.t_order0,node1.t_order1的简写。

这里另外要注意的一点是我们使用了主键作为数据表的拆分键,此时在数据库中使用MySQL的自增主键就不合适了,它可能造成不同数据在不同分库表中的主键相同,这会导致后续的查询错误。分库分表下建议在应用层使用SnowFlake算法生成主键值。

绑定表

Sharding-JDBC中,使用连接查询的条件是分片键相同且分片键在JOIN条件中。对于分片规则一致的表,我们可以将其创建为绑定表,这样在关联查询时可以避免跨库关联和笛卡尔积。

spring.shardingsphere.rules.sharding.binding-tables[0]=t_order,t_product

广播表

广播表是一种在所有数据源中都存在的表,各个数据源中的表结构和数据均一致。广播表用于数量较小且经常进行关联查询的表,比如系统的字典表。广播表的特点:

  1. 插入和更新操作会实时在所有节点上执行,保持各个数据源上的数据一致性
  2. 查询操作只从一个节点获取
  3. 可以跟任何一个表进行连接查询

配置广播表例子如下。

spring.shardingsphere.rules.sharding.tables.t_dict.actual-data-nodes=node$->{0..1}.t_dict
spring.shardingsphere.rules.sharding.broadcast-tables[0]=t_dict
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap