ShardingSphere 读写分离

2,585 阅读2分钟

基础概念

  1. 主库,用于写的数据库,ShardingSphere 目前只支持单主库。
  2. 从库,用户查询的数据库,支持多从库,支持负载均衡分散读库压力。
  3. 主从同步,把主库的 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

配置了打印日志,执行的时候就能看到使用的是读库还是写库。

image.png

image.png

原理

搞开发就是喜欢这么个玩意儿,追根究底才过瘾。
通常来说,mybatis 中一个 SQL 执行的大概过程是,1获取 datasource,2获取 statement,3执行。

Connection conn = dataSource.getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.executeQuery();

ShardingSphere 在 execute 之前做了处理。

image.png

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 使用的数据库。

  1. 先搞两个注解,就叫 Master 和 Slave 吧。被标记的类或方法,则使用指定的数据库。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Documented
public @interface Master {
}
  1. 搞个拦截器拦截在 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 {
        走从库
    }

}

大概这么个思路,有想法的同学请在评论区留言讨论。