ShardingSphere 读写分离
基础概念
- 主库,用于写的数据库,ShardingSphere 目前只支持单主库。
- 从库,用户查询的数据库,支持多从库,支持负载均衡分散读库压力。
- 主从同步,把主库的 binlog 通过 IO 线程同步到从库,保证主从库数据一致,会有延迟。
读写分离
不多 BB,先看配置文件。
spring:
shardingsphere:
datasource:
names: master,slave
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3306/master?useUnicode=true&characterEncoding=utf8
username: xxxx
password: xxxx
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3306/master?useUnicode=true&characterEncoding=utf8
username: xxxx
password: xxxx
masterslave:
load-balance-algorithm-type: round_robin # 负载均衡算法,
name: ms
master-data-source-name: master # 主库数据源名字
slave-data-source-names: slave # 从库数据源名字
props:
sql:
show: true # 打印SQL
配两个数据库,master 和 slave,分别表示写库和读库。 再在 masterslave 中自动配一下就完事。
测试
自己写两个 controller,分别读一下和写一下,因为在
props:
sql:
show: true # 打印SQL
配置了打印日志,执行的时候就能看到使用的是读库还是写库。
原理
搞开发就是喜欢这么个玩意儿,追根究底才过瘾。 通常来说,mybatis 中一个 SQL 执行的大概过程是,1获取 datasource,2获取 statement,3执行。
Connection conn = dataSource.getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.executeQuery();
ShardingSphere 在 execute 之前做了处理。
masterSlaveDataRouter 。
@RequiredArgsConstructor
public final class MasterSlaveDataSourceRouter {
private final MasterSlaveRule masterSlaveRule;
/**
* Route.
*
* @param sqlStatement SQL statement
* @return data source name
*/
public String route(final SQLStatement sqlStatement) {
if (isMasterRoute(sqlStatement)) {
MasterVisitedManager.setMasterVisited();
return masterSlaveRule.getMasterDataSourceName();
}
return masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(
masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceNames()));
}
private boolean isMasterRoute(final SQLStatement sqlStatement) {
return containsLockSegment(sqlStatement) || !(sqlStatement instanceof SelectStatement) || MasterVisitedManager.isMasterVisited() || HintManager.isMasterRouteOnly();
}
private boolean containsLockSegment(final SQLStatement sqlStatement) {
return sqlStatement instanceof SelectStatement && ((SelectStatement) sqlStatement).getLock().isPresent();
}
}
获取 connection 源码。
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
if (getCachedConnections().containsKey(dataSourceName)) {
return getCachedConnections().get(dataSourceName);
}
DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName);
Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName);
String realDataSourceName;
if (dataSource instanceof MasterSlaveDataSource) {
NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
realDataSourceName = namedDataSource.getName();
if (getCachedConnections().containsKey(realDataSourceName)) {
return getCachedConnections().get(realDataSourceName);
}
dataSource = namedDataSource.getDataSource();
} else {
realDataSourceName = dataSourceName;
}
Connection result = dataSource.getConnection();
getCachedConnections().put(realDataSourceName, result);
replayMethodsInvocation(result);
return result;
}
如果是 MasterSlaveDataSource 类型,则会进入。
public NamedDataSource getDataSource(final SQLType sqlType) {
if (isMasterRoute(sqlType)) {
DML_FLAG.set(true);
return new NamedDataSource(masterDataSourceName, masterDataSource);
}
String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet()));
DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.get(selectedSourceName);
Preconditions.checkNotNull(selectedSource, "");
return new NamedDataSource(selectedSourceName, selectedSource);
}
private static boolean isMasterRoute(final SQLType sqlType) {
return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
}
有一个比较有意思的地方,如果一个线程中有一次写操作,那么后面所有的 SQL 都会走写库,防止数据不统一。
自行设计
根据源码的思路,怎么自己搞一套骚操作? 会用到的知识点,1 AOP,2注解。 思路就是,在执行SQL语句前判断该方法是读还是写。再来更改对应的 datasource。注解为了显性标识某个方法或类中的 SQL 使用的数据库。
- 先搞两个注解,就叫 Master 和 Slave 吧。被标记的类或方法,则使用指定的数据库。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Documented
public @interface Master {
}
- 搞个拦截器拦截在 Dao 执行 SQL 前面,根据注解、方法综合判断走读库还是写库。
@Pointcut("execution(* com.xx.xx.dao..*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class class = methodSignature.getDeclaringType();
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(Master.class)) {
走主库
}else if (method.isAnnotationPresent(Slave.class)) {
走从库
}
if (class.isAnnotationPresent(Master.class)) {
走主库
}else if (class.isAnnotationPresent(Slave.class)) {
走从库
}
String name = method.getName();
if (name.contains("select") || name.contains("query") || name.contains("find")) {
走主库
}else {
走从库
}
}
大概这么个思路,有想法的同学请在评论区留言讨论。
转载自:https://juejin.cn/post/6986909661362389000