CobarClient源码分析

CobarClient是阿里巴巴公司开发一个的开源的、基于iBatis和Spring的分布式数据库访问层。为了支持iBatis,Spring框架提供了一个SqlMapClientTemplate,通过模板模式简化了在Spring框架中对于iBatis中的使用。而CobarClient则继承了SqlMapClientTemplate,提供了CobarSqlMapClientTemplate给应用使用。CobarSqlMapClientTemplate和SqlMapClientTemplate继承关系如下:

屏幕快照 2014-02-16 下午7.07.02

CobarSqlMapClientTemplate持有的众多属性中,比较重要的有cobarDataSourceService(提供多数据源的管理服务)、router(Sql路由接口的实现)、concurrentRequestProcessor(Sql并发执行的命令类),下面就从路由规则到最终的Sql执行来分析一下CobarClient的原理。

  1. 路由规则的生成与执行。

根据CobarClient的文档,CobarClient一共提供了四种路由规则:

  • NamespaceShardingRule:针对iBatis中Sql Map定义的,属于某一个NameSpace的Sql语句执行某一个路由规则,根据路由的结果选择一个或者多个数据分区。
  • Namespace(Only)Rule:针对iBatis中Sql Map定义的,属于某一个NameSpace的Sql语句无差别的将Sql语句路由到一个或者多个数据分区。
  • SqlActionShardingRule:针对iBatis中Sql Map定义的某一个Sql Id对应的Sql语句,执行某一个路由规则,根据路由的结果选择一个或者多个数据分区。
  • SqlAction(Only)Rule:针对iBatis中Sql Map定义的某一个Sql Id对应的Sql语句,无差别的将这个语句路由到一个或者多个数据分区。

在CobarClient中这四个路由规则,分别对应四个具体的实现类,而这四个实现类,都实现了IRoutingRule这个接口,继承关系如下所示:

屏幕快照 2014-02-16 下午7.30.22

在4种路由规则分别具体对应的实现类中,都实现了isDefinedAt(IBatisRoutingFact routingFact)这个方法,这个方法实现的功能判断现在要执行的Sql语句是不是可以匹配当前的路由规则实例。这个方法的参数,routingFact的类型是IBatisRoutingFact,代表的是一个路由上下文信息:

public class IBatisRoutingFact {
  private String action; // SQL identity	
  private Object argument; // the argument of SQL action
}
举个例子,看看IBatisNamespaceRule类中isDefinedAt方法的实现如下:
public boolean isDefinedAt(IBatisRoutingFact routingFact) {
  Validate.notNull(routingFact);
  String namespace = StringUtils.substringBeforeLast(routingFact.getAction(), ".");
  return StringUtils.equals(namespace, getTypePattern());
}

实际完成的工作很简单,就是看看当前的Sql Id的NameSpace是否和规则定义的NameSpace一致,如果一致,表示使用该路由规则。清楚了路由规则的实现之后,那么这么多路由规则又是怎么汇总到CobarSqlMapClientTemplate中的呢?根据CobarClient文档中的说明:

默认情况下, CobarClientInternalRouter将接收4组不同类型的路由规则, 但路由规则的类型对于用户来说实际上是不必要的, 所以, 为了避免用户过多的纠缠于CobarClientInternalRouter的实现细节, 我们给出了针对CobarClientInternalRouter配置的一个Spring的FactoryBean实现, 以帮助简化CobarClientInternalRouter的配置, 该FactoryBean实现类为com.alibaba.cobar.client.router.config.CobarInteralRouterXmlFactoryBean。CobarInteralRouterXmlFactoryBean将根据指定的xml形式的配置文件中的内容, 自动构建不同类型的路由规则, 然后注入到它将最终返回的CobarClientInternalRouter实例之上。 而读取, 解析配置信息, 并构建不同类型路由规则等 “琐事” 将完全对用户透明。

一个典型的路由规则文件可能如下

<rules>
  <rule>
    <namespace>com.alibaba.cobar.client.entity.Follower</namespace>
    <shards>partition1</shards>
  </rule>
  <rule>
    <sqlmap>com.alibaba.cobar.client.entity.Follower.create</sqlmap>
    <shards>p1, p2</shards>
  </rule>
  <rule>
    <sqlmap>com.alibaba.cobar.client.entity.Follower.create</sqlmap>
    <shardingExpression>id&gt;10000 and id&lt; 20000</shardingExpression>
    <shards>p1, p2</shards>
  </rule>
  <rule>
    <namespace>com.alibaba.cobar.client.entity.Follower</namespace>
    <shardingExpression>id&gt;10000 and id&lt; 20000</shardingExpression>
    <shards>p1, p2</shards>
  </rule>
</rules>
解析了完所有的路由规则配置文件之后,CobarClientInternalRouter类的实例将持有一个List,该List的定义如下:
private List<Set<IRoutingRule<IBatisRoutingFact, List<String>>>> ruleSequences

实际上,这个List只有4个元素,每一个元素是一个集合,分别对应之前提到的四种路由规则。而每个集合中的元素都是一个具体的规则实现类。当具体执行一个Sql语句之前,CobarSqlMapClientTemplate通过它持有的CobarClientInternalRouter实例的doRoute方法来实现具体的路由规则选择,可见CobarClientInternalRouter实际上充当了一个门面(Facade)的作用,汇总了所有的路由信息,而且CobarSqlMapClientTemplate是一个更高层的门面。
   在doRoute方法中,有两层循环,第一层循环,遍历ruleSequence这个List,第二层循环遍历List中每个Set

for (Set<IRoutingRule<IBatisRoutingFact, List<String>>> ruleSet : getRuleSequences()) {
  ruleToUse = searchMatchedRuleAgainst(ruleSet, routingFact);
    if (ruleToUse != null) {
      break;
    }
}
在searchMatchedRuleAgainst方法中,执行第二次遍历
for (IRoutingRule<IBatisRoutingFact, List<String>> rule : rules) {
  if (rule.isDefinedAt(routingFact)) {
      return rule;
    }
}

在这里最终调用之前提到的isDefinedAt方法,两次遍历了ruleSequence之后,将获得一个Map,
SortedMap<String, DataSource> resultMap 表示的这次Sql语句执行需要用对应的数据源。

2.Sql的执行
   当CobarSqlMapClientTemplate通过CobarClientInternalRouter获取到了本次Sql执行对应的数据源之后,将进入到Sql执行的阶段。在Spring框架中,提供了一个接口SqlMapClientCallback接口,CobarSqlMapClientTemplate在每一次执行Sql语句之前,都会生成一个SqlMapClientCallback的内部类,该内部类实现了SqlMapClientCallback接口中定义的回调函数,doInSqlMapClient。例如,在某一次查询语句之前,对应的内部类可能是:

callback = new SqlMapClientCallback() {
  public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
    return executor.queryForList(statementName, parameterObject,skipResults, maxResults); 
    }
};

具体到Sql语句的执行,CobarClient命令模式结合回调函数的方式来实现,标准的命令模式结构图如下:
2010070907240523

在CobarSqlMapClientTemplate中,担任ConcreteCommand角色的是IConcurrentRequestProcessor,它的执行方法为:

List<Object> process(List<ConcurrentRequest> requests)

不同的是,为了支持Sql并行执行,这里传入的不是单个的request,而是由多个ConcurrentRequest类型的request组成的List(这里的ConcurrentRequest可以看成命令模式的中的Receiver),不同的是在ConcurrentRequest类中是通过多线程加调用SqlMapClientCallback中的回调函数的方式来实现命令模式中Receiver的action方法的。在IConcurrentRequestProcessor调用process方法之后,会通过多线程的方式来执行SqlMapClientCallback中的回调函数,

request.getExecutor().submit(new Callable<Object>() {
  public Object call() throws Exception {
    try {
      return executeWith(connection, action);
    } finally {
      latch.countDown();
    }
  }
} )

executeWith中connection具体的数据源的连接,action就是之前提到的匿名内部类,在executeWith方法中,会调用这个内部的回调函数:

protected Object executeWith(Connection connection, SqlMapClientCallback action) {
        SqlMapSession session = getSqlMapClient().openSession();
        try {
            try {
                session.setUserConnection(connection);
            } catch (SQLException e) {
                throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", e);
            }
            try {
                return action.doInSqlMapClient(session);
            } catch (SQLException ex) {
                throw new SQLErrorCodeSQLExceptionTranslator().translate("SqlMapClient operation",
                        null, ex);
            }
        } finally {
            session.close();
        }
    }

当执行完SqlMapClientCallback中的doInSqlMapClient方法之后,CobarSqlMapClientTemplate还会对多个Sql语句执行获取的结果进行简单的合并之后在返回给应用程序,至此,整个Sql的分布式数据库执行才算结束。

3. 一点体会
CobarClient扩展了Spring提供的SqlMapClientTemplate,在Sql执行之前根据数据路由规则获取了真正要执行的数据分区,在Sql执行之后在进行结果集的简单合并,最终实现了分布式数据库的数据路由功能,但是CorBarClient不是没有缺点

  1. 数据合并功能的羸弱:CobarClient并没有引入Sql语句的解析,对于数据的合并是在应用层做的,而且现在只能支持简单的合并,当应用程序有比较复杂的结果集处理要求的时候,例如order by,group by等,需要自己去实现merge类,而且由于没有Sql语句的解析,即使实现了,也很难做到通用。
  2. 路由算法的效率:根据CobarClient的文档:
             我们可以根据情况提供不同的ICobarRouter实现类, 比如Cobar Client默认提供的com.alibaba.cobar.client.router.CobarClientInternalRouter和 om.alibaba.cobar.client.router.DefaultCobarClientInternalRouter, 或者如果路由规则数量很多, 为了保证性能, 也可以实现基于Rete等算法的实现类等. 没有特殊需求的情况下,我们默认采用CobarClientInternalRouter作为CobarSqlMapClientTemplate使用的 默认Router实现. 但用户也可以根据情况选用DefaultCobarClientInternalRouter, 二者的使用是类似的。DefaultCobarClientInternalRouter在CobarClientInternalRouter的基础 上, 对路由规则的匹配进行了分组优化, 通过配置时期的复杂度换取运行时期的简单高效。 如果规则很多的话,可以考虑使用DefaultCobarClientInternalRouter。
            之前代码分析的CobarClientInternalRouter的doRoute实现,在两层的循环中,如果有n个具体的路由规则,那么最坏的情况,要执行n次路由判断,算法的效率是O(n)。文档说DefaultCobarClientInternalRouter的doRoute实现效率更高(但是DefaultCobarClientInternalRouter的doRoute方法没有用到cache,而CobarClientInternalRouter用到了),但实际上只是对于ruleSequences根据Namespace做了分组,那么当有n个路由规则,m个namespace的时候,算法的效率是O(n/m)。由于在每个sql语句执行之前,都需要执行doRoute方法来路由,所以如果用HashMap来保存Sql Id和路由规则的对应关系,算法的效率将提高到O(1),这样算法的时间消耗不会随着路由规则的增多而增长。
  3. 路由规则的设计和解析:路由规则的解析,CobarClient使用的XStream这个开源的包,这样的话,当路由规则文件要新增结点或者某个结点要新增属性的时候,XStream并不能做到很灵活。此外,CobarInteralRouterXmlFactoryBean中根据路由规则解析文件的时候,基本上都是用if else的语句来做判断的,这是一种代码的坏味道。此外,根据之前路由规则的继承图可以看到,在具体的路由规则实现类之上,还有三个抽象的路由规则类,我推倒是CobarClient的设计者刚开始的时候,是为了以后的路由规则能够扩展,但是他最后发现四种规则就能涵盖一切情况,但是没有对代码进行必要的重构。
  4. 代码的风格:这是一个小问题,但是确实很影响代码的理解。例如在iBatis语境中默认的Sql Id到了IBatisRoutingFact就变成
    private String action了。Rule文件中的namespace到了具体的规则类之中,就变成了private String typePatten。这种情况充斥于整个代码之中,于是最后你能发现,ICobarRouter的默认实现类是CobarClientInternalRouter而不是DefaultCobarClientInternalRouter。

瑕不掩瑜,如果需要一个轻量级的,支持分布式数据库的数据访问层框架,并且应用不需要对于查询的结果集做过于复杂的排序聚会等操作,CobarClient是一个不错的选择。

原文地址:https://www.cnblogs.com/javanerd/p/3551831.html