天天看点

sharding-jdbc读写分离源码分析

在之前两篇文章《springBoot+mybatis数据库读写分离》和《对“springBoot+mybatis数据库读写分离”中两种方式的对比》两篇文章中,介绍了两种读写分离的实现方式和各自的优缺点,这篇文章讲一下用“sharding-jdbc”来实现读写分离

sharding-jdbc简介

sharding-jdbc是shardingsphere中的一个产品,实现客户端的分库分表和读写分离,而不需要引入类似mycat这些中间件。个人觉得,sharding-jdbc最重要的就是sql解析:

  1. 对于读写分离,通过解析sql语句,可以知道语句是属于DML还是DQL,DML就从主库获取连接执行sql;DQL则从从库获取连接执行sql。
  2. 对于分库分表,通过解析sql语句,得到sql语句中包含的分片键,然后通过我们配置的分片规则,可以运算得出语句涉及到的表和库。

对shardingsphere有兴趣的,可以去阅读一下官方文档《shardingsphere官方文档》

springBoot项目用sharding-jdbc实现读写分离的代码

  1. 添加maven依赖
<!--数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.19</version>
        </dependency>

        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--sharding-jdbc-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-namespace</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>
           

2.添加配置

spring:
#  datasource:
  shardingsphere:
    datasource:
      names: master,slave  #所有数据库名称,多个数据库用英文逗号隔开,这里一个叫master,一个叫slave
      master: #master数据库配置
        type: com.alibaba.druid.pool.DruidDataSource  
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/xdchen_test?useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456
        maxPoolsize: 20
        validationQuery: SELECT 1
        validationQueryTimeout: 1000
      slave: #slave数据库配置
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/xdchen_test?useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456
        maxPoolsize: 20
        validationQuery: SELECT 1
        validationQueryTimeout: 1000
    masterslave: #读写分离配置
      load-balance-algorithm-type: round_robin  #多个从库的时候有用,负载均衡算法
      name: ms 
      master-data-source-name: master  #主库的名字
      slave-data-source-names: slave  #从库的名字,多个之间用英文逗号分隔
    props:
      sql:
        show: true  #是否打印日志。。。。分库分表才有用,读写分离啥也没打
           

这样就完成了!!

项目启动过后,会自动注册一个org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.MasterSlaveDataSource的bean,mybatis用这个该dataSouce对象来生成SqlSessionFactory,就能自动根据sql语句类型,路由到不同的库。

sharding-jdbc怎么做到的?怎么规避分布式事务的问题?

先看看org.apache.shardingsphere:sharding-jdbc-core:4.0.0RC1的目录结构

sharding-jdbc读写分离源码分析

比较关心读写分离的细节,只需要看org.apache.shardingsphere.shardingjdbc.jdbc.core目录下的代码。可以看到,core目录下分别有connection,datasource、resultset和statement4个目录。到这里,可以很容易想到,sharding-jdbc是实现java.sql下的接口,用于代理真实的数据库对象。展开目录可以看到如下内容:

sharding-jdbc读写分离源码分析

因为关注的是读写分离,所以只需要看MasterSlaveDataSource、MasterSlaveConnection、MasterSlaveStatement和MasterSlavePreparedStatement这4个类。

MasterSlaveDataSource源码

@Getter
public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
    
    private final DatabaseMetaData cachedDatabaseMetaData;
    
    private final MasterSlaveRule masterSlaveRule;
    
    private final ShardingProperties shardingProperties;
    
    public MasterSlaveDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRuleConfiguration masterSlaveRuleConfig, final Properties props) throws SQLException {
        super(dataSourceMap);
        cachedDatabaseMetaData = createCachedDatabaseMetaData(dataSourceMap);
        this.masterSlaveRule = new MasterSlaveRule(masterSlaveRuleConfig);
        shardingProperties = new ShardingProperties(null == props ? new Properties() : props);
    }
    
    public MasterSlaveDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRule masterSlaveRule, final Properties props) throws SQLException {
        super(dataSourceMap);
        cachedDatabaseMetaData = createCachedDatabaseMetaData(dataSourceMap);
        this.masterSlaveRule = masterSlaveRule;
        shardingProperties = new ShardingProperties(null == props ? new Properties() : props);
    }
    
    private DatabaseMetaData createCachedDatabaseMetaData(final Map<String, DataSource> dataSourceMap) throws SQLException {
        try (Connection connection = dataSourceMap.values().iterator().next().getConnection()) {
            return new CachedDatabaseMetaData(connection.getMetaData(), dataSourceMap, null);
        }
    }
    
    @Override
    public final MasterSlaveConnection getConnection() {
        return new MasterSlaveConnection(this, getDataSourceMap(), getShardingTransactionManagerEngine(), TransactionTypeHolder.get());
    }
}

           

代码很简单,就是构造函数和getConnection方法。

从父类和构造函数可以看出,MasterSlaveDataSource就是真是数据源的一个适配器,持有一个存放数据源的Map(可以通过名字找到数据源)和主从规则配置(上面yaml文件中的spring.shardingsphere.masterslave)。

接下来看看MasterSlaveConnection的代码,代码稍微多一些,我们只需要关注一部分

@Getter
public final class MasterSlaveConnection extends AbstractConnectionAdapter {
 private final MasterSlaveDataSource masterSlaveDataSource;
    
    private final Map<String, DataSource> dataSourceMap;
    
    public MasterSlaveConnection(final MasterSlaveDataSource masterSlaveDataSource, final Map<String, DataSource> dataSourceMap,
                                 final ShardingTransactionManagerEngine shardingTransactionManagerEngine, final TransactionType transactionType) {
        super(shardingTransactionManagerEngine, transactionType);
        this.masterSlaveDataSource = masterSlaveDataSource;
        this.dataSourceMap = dataSourceMap;
    }
   	@Override
    public PreparedStatement prepareStatement(final String sql) throws SQLException {
        return new MasterSlavePreparedStatement(this, sql);
    }
    、、、、、、、、
    省略一堆java.sql.Connection的方法实现,主要就是new出MasterSlaveStatement和MasterSlavePreparedStatement对象
    、、、、、、、、
    @Override
    protected boolean isOnlyLocalTransactionValid() {
        return true;
    }
}
           

关注构造函数,是要知道MasterSlaveConnection的对象会持有产生它的MasterSlaveDataSource对象,transcationType永远是"local",读写分离只会是本地事务。

MasterSlaveStatement和MasterSlavePreparedStatement类似,我只看其中一个,选MasterSlavePreparedStatement——用mybatis基本都是用preparedStatement来执行sql

MasterSlavePreparedStatement源码

@Getter
public final class MasterSlavePreparedStatement extends AbstractMasterSlavePreparedStatementAdapter {
  private final MasterSlaveConnection connection;
    
    @Getter(AccessLevel.NONE)
    private final MasterSlaveRouter masterSlaveRouter;

	、、、、、、
	省略一堆构造函数,代码类似
    、、、、、、
    
    private final Collection<PreparedStatement> routedStatements = new LinkedList<>();
        public MasterSlavePreparedStatement(
            final MasterSlaveConnection connection, final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException {
        this.connection = connection;
        masterSlaveRouter = new MasterSlaveRouter(connection.getMasterSlaveDataSource().getMasterSlaveRule(),
                connection.getMasterSlaveDataSource().getShardingProperties().<Boolean>getValue(ShardingPropertiesConstant.SQL_SHOW));
        for (String each : masterSlaveRouter.route(sql)) {
            PreparedStatement preparedStatement = connection.getConnection(each).prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
            routedStatements.add(preparedStatement);
        }
    }

 @Override
    public ResultSet executeQuery() throws SQLException {
        Preconditions.checkArgument(1 == routedStatements.size(), "Cannot support executeQuery for DDL");
        return routedStatements.iterator().next().executeQuery();
    }
    
    @Override
    public int executeUpdate() throws SQLException {
        int result = 0;
        for (PreparedStatement each : routedStatements) {
            result += each.executeUpdate();
        }
        return result;
    }
	 、、、、、、
	 、、、、、、
}
           

MasterSlaveRouter 源码

@RequiredArgsConstructor
public final class MasterSlaveRouter {
    
    private final MasterSlaveRule masterSlaveRule;
    
    private final boolean showSQL;
    
    /**
     * Route Master slave.
     *
     * @param sql SQL
     * @return data source names
     */
    // TODO for multiple masters may return more than one data source
    public Collection<String> route(final String sql) {
        Collection<String> result = route(new SQLJudgeEngine(sql).judge().getType());
        if (showSQL) {
            SQLLogger.logSQL(sql, result);
        }
        return result;
    }
    
    private Collection<String> route(final SQLType sqlType) {
        if (isMasterRoute(sqlType)) {
            MasterVisitedManager.setMasterVisited();
            return Collections.singletonList(masterSlaveRule.getMasterDataSourceName());
        }
        return Collections.singletonList(masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(
                masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceNames())));
    }
    
    private boolean isMasterRoute(final SQLType sqlType) {
        return SQLType.DQL != sqlType || MasterVisitedManager.isMasterVisited() || HintManager.isMasterRouteOnly();
    }
}
           

看构代函数的代码,可以看出,new一个MasterSlavePreparedStatement对象时,会new一个MasterSlaveRouter路由器,对要执行的sql语句进行路由,路由的结果虽然是一个Collection对象,但是从MasterSlaveRouter.route(final SQLType sqlType)的源码可以看出来,用户只有一个元素,而且值为某个真实数据源的名称。MasterSlavePreparedStatement根据数据源的名称,去获取一个真正的数据库连接,代码在org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractConnectionAdapter.getConnection方法,有兴趣可以去看看,其中涉及到缓存获取的connction,这个很重要,只有缓存原先获取的connection,才不会有分布式事务的问题

到了这里,关于sharding-jdbc实现读写分离的原理,怎么解决同一个事务中有读有写的情况,已经出来 。

sharding-jdbc读写分离源码分析

通过SQLJudgeEngine判断sql语句类型,DQL、DML,DAL。。。。

只有DQL才有可能走从库,前提是“之前没有访问过主库”+“没有强制走主库的hint”。

走了主库,会用MasterVisitedManager.setMasterVisited()设置主库标识,底层是一个threadLocal变量,这让同一个线程的语句,只要访问了一次主库,接来下执行的语句都会走主库,即使语句类型是DQL。

@Transcational
public Object test() {
	if (count()<1) {
		insert();
	}
	return select();
}
           

1、在一个事务里,先执行DQL语句,会先走从库查询

2、再执行DML语句,会都走主库

3、再执行DQL语句,继续走主库,能够查询到步骤2中修改或插入的数据

sharding-jdbc读写分离的demo代码也加到原先的项目中:读写分离练习项目