在之前两篇文章《springBoot+mybatis数据库读写分离》和《对“springBoot+mybatis数据库读写分离”中两种方式的对比》两篇文章中,介绍了两种读写分离的实现方式和各自的优缺点,这篇文章讲一下用“sharding-jdbc”来实现读写分离
sharding-jdbc简介
sharding-jdbc是shardingsphere中的一个产品,实现客户端的分库分表和读写分离,而不需要引入类似mycat这些中间件。个人觉得,sharding-jdbc最重要的就是sql解析:
- 对于读写分离,通过解析sql语句,可以知道语句是属于DML还是DQL,DML就从主库获取连接执行sql;DQL则从从库获取连接执行sql。
- 对于分库分表,通过解析sql语句,得到sql语句中包含的分片键,然后通过我们配置的分片规则,可以运算得出语句涉及到的表和库。
对shardingsphere有兴趣的,可以去阅读一下官方文档《shardingsphere官方文档》
springBoot项目用sharding-jdbc实现读写分离的代码
- 添加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的目录结构
比较关心读写分离的细节,只需要看org.apache.shardingsphere.shardingjdbc.jdbc.core目录下的代码。可以看到,core目录下分别有connection,datasource、resultset和statement4个目录。到这里,可以很容易想到,sharding-jdbc是实现java.sql下的接口,用于代理真实的数据库对象。展开目录可以看到如下内容:
因为关注的是读写分离,所以只需要看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实现读写分离的原理,怎么解决同一个事务中有读有写的情况,已经出来 。
通过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中修改或插入的数据