sharding-jdbc是shardingsphere中的一个产品,实现客户端的分库分表和读写分离,而不需要引入类似mycat这些中间件。个人觉得,sharding-jdbc最重要的就是sql解析:
对shardingsphere有兴趣的,可以去阅读一下官方文档《shardingsphere官方文档》
com.alibaba
druid
1.0.19
mysql
mysql-connector-java
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
4.0.0-RC1
org.apache.shardingsphere
sharding-jdbc-spring-namespace
4.0.0-RC1
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语句类型,路由到不同的库。
先看看org.apache.shardingsphere:sharding-jdbc-core:4.0.0RC1的目录结构
比较关心读写分离的细节,只需要看org.apache.shardingsphere.shardingjdbc.jdbc.core目录下的代码。可以看到,core目录下分别有connection,datasource、resultset和statement4个目录。到这里,可以很容易想到,sharding-jdbc是实现java.sql下的接口,用于代理真实的数据库对象。展开目录可以看到如下内容:
因为关注的是读写分离,所以只需要看MasterSlaveDataSource、MasterSlaveConnection、MasterSlaveStatement和MasterSlavePreparedStatement这4个类。
@Getter
public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
private final DatabaseMetaData cachedDatabaseMetaData;
private final MasterSlaveRule masterSlaveRule;
private final ShardingProperties shardingProperties;
public MasterSlaveDataSource(final Map 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 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 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 dataSourceMap;
public MasterSlaveConnection(final MasterSlaveDataSource masterSlaveDataSource, final Map 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 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().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 route(final String sql) {
Collection result = route(new SQLJudgeEngine(sql).judge().getType());
if (showSQL) {
SQLLogger.logSQL(sql, result);
}
return result;
}
private Collection 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实现读写分离的原理,怎么解决同一个事务中有读有写的情况,已经出来 。
通过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中修改或插入的数据