Thursday, June 16, 2011

应用层针对数据切片的实现方法

  为了满足业务增长和服务能力的增长,我们需要必要的sharding操作,通过水平或者垂直的切割将系统分别放置于不同的物理机器上,从而达很好的扩展性,也为业务的发展提供技术保证,那么看似简单的sharding,在应用层次需要做那些工作呢?
   在详细描述之前,我们需要定义以下两件事:
    (1)定义sharding的方式,设定我们需要sharding模式是通过SQL方式访问数据库
    (2)定义SQL访问时sharding的精细度,即需要SQL解析的模式还是简单的数据源绑定模式,不同的实现方式其实现复杂度也完全不一样。
  接下来我们开始详细讲解数据源绑定模式和SQL解析模式的具体实现方法。
  假设我们有用户信息2000万或者更多,随着业务的增长我们需要根据userid将此sharding, 数据源绑定的模式是如何实现的?
    第一步,定义数据库的sharding方式,将数据库通过schema方式切分开来,简单SQL代码如下。
     SQL代码如下:
     create database user00;
     create database user01;
      ...
     create database user09;
   第二步,将10个schema分布到不通的物理机器上,并创建相同的表,表的个数一定为10份。
     machine1:
      user00~user02:
      create table user 
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 

   machine2:

     user03~user05:
      create table user 
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 

machine3:

     user06~user09:
      create table user 
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
第三步,定义数据源与具体逻辑数据库的关系,简单的XML配置代码如下:

 <datasource-config>
 <datasource name="user00" value="0">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.116:3306/user00"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
 <datasource name="user03" value="3">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.117:3306/user03"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
 <datasource name="user06" value="6">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.118:3306/user06"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
</datasource-config>


第四步,根据传入的USERID,进行数据源的选择,代码逻辑如下:
(1)插入数据:
         SQL语句为: insert into user(userid,email) values(1,'aa@sina.com');
         插入逻辑:根据userid的value=1进行取模10的值=1,知道此时插入的数据源为user01,根据数据源打开数据库,插入数据。

(2)查询数据:
           SQL语句为: select * from    user where  userid=12;
           查询逻辑:根据userid的value=1进行取模10的值=2,知道此时查询的数据源为user02,根据数据源打开数据库,获取查询数据。

总结:数据源绑定模式相对比较粗犷,实现方式非常简单,拆分的数量与数据源的配置数量成正比,将数据源与传入的值进行绑定, sharding的精细度只能到schema级别,使用的SQL语句一定是固定的,可改造ibatis的数据源注入情况,达到ibatis的下应用的sharding。

 依然假设我们有用户信息2000万或者更多,随着业务的增长我们需要根据userid将此sharding, SQL解析的模式是如何实现的?
 第一步,定义数据库的shading方式,将数据库通过schema和表的方式切分开来,将6个schema分布到不通的物理机器上,并创建不同命名的表,表的个数是6*10=60。
machine1:
      user00:
      create table user00
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      ......
     create table user09
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
   
user01:
      create table user10
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      .....
      create table user19
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 

   machine2:

     user02:
      create table user20
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      .....
      create table user29
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 

   user03:
      create table user30
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      .....
      create table user39
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 

machine3:

      user04:
      create table user40
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      .....
      create table user49
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
    user05:
      create table user50
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
      .....
      create table user59
      (
        userid   bigint (12),
        email    varchar(256),
        ........
       ) 
第二步,定义数据源与具体逻辑数据库的关系,简单的XML代码如下:

 <datasource-config>
 <datasource name="user00" value="0">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.116:3306/user00"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
 <datasource name="user02" value="2">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.117:3306/user03"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
 <datasource name="user04" value="4">
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://10.224.57.118:3306/user06"/>
  <property name="user" value="test"/>
  <property name="password" value="pass"/>
 </datasource>
 ...
</datasource-config>


第三步,根据传入的USERID,进行数据源和表选择,代码逻辑如下:
(1)插入数据:
         SQL语句为: insert into user(userid,email) values(1,'aa@sina.com');
         插入逻辑:根据userid的value=1进行取模10的值=1,再减一,知道此时插入的数据源为user00,在根据userid末尾两位数据知道表名为user01,此时的SQL语句中表名已经更改,如果使用ibtais书写SQL,那么静态的SQL无法替换成动态的SQL语句,此时有两种做法,一种方法是在ibtais上层实现一套解析层,将SQL解析后并替换再传递给ibatis去执行,另一种方法是改写ibtais的代码,加入SQL解析程序,将SQL解析后并替换再传递给ibatis去执行插入操作,此时的难点在于SQL的语法解析,目前比较著名的淘宝TDDL就是采取第一种的方法,使用javacc去解析SQL语句。本人尝试了下第二种方法即ibatis的改写模式,在此将实现代码分享给大家。
a)在MappedStatement.java中增加下面代码,使用opensource工具JSqlParser:
/**
   * add by philip zhong to replace the tables name.2010-12-15
   * @param processedSql
   * @return
   * @throws JSQLParserException
   * @throws Exception
   */
  private String replaceSql(String processedSql,String replaceTableSuffixKey) throws JSQLParserException
  {

      CCJSqlParserManager pm = new CCJSqlParserManager();
      net.sf.jsqlparser.statement.Statement statement = pm.parse(new StringReader(processedSql));
      TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
      List tableList = tablesNamesFinder.getTableList(statement);
      String sql=processedSql;
      for (int i=0;i<tableList.size();i++)
      {
       String tempSQL=sql;
       String tableName=tableList.get(i).toString().trim();
       String repTabName=tableList.get(i).toString().trim()+replaceTableSuffixKey.trim();
       sql=tempSQL.replaceAll(tableName,repTabName);
       //System.out.println("replaceSql="+sql);
      }
   return sql;
  }
 /**
  * add by philip zhong to check has lookupkey in parameter object
  * @param localSql
  * @param map
  * @return
  */
   private String getTableSuffixKey(Object map)
   {
   
    String value = StaticBeanProbe.getObject(map, "lookupkey").toString();
    return value;
   }


 b)再修改IBATIS代码,在MappedStatement.createStatement方法中添加下面代码:
 //从传入的parameter中找到表的后缀关键字,例如表:user0101就是后缀关键字。
 String lookupkey=getTableSuffixKey(parameterObject);
  //在执行前把sql语句对应的表替换掉,然后再执行
 processedSql=replaceSql(processedSql,lookupkey);


 c)需要在DAO代码中执行SQL的时间加上以下例子的代码,lookupkey是一个标签
public long getUseridCount(long userid,String lookupkey) throws DaoException {
    HashMap<String, Comparable> paraMap = new HashMap<String, Comparable>();
    paraMap.put("userid", new Long(userid));
    //for get table name
     paraMap.put("lookupkey", lookupkey);
    return ((Long) executeQueryForObject("getUseridCount",paraMap)).longValue();
}
     
(2)查询数据:
           SQL语句为: select * from    user where  userid=12;
           查询逻辑:根据userid的value=12进行取模10的值=2,再减一,知道此查询的数据源为user01,在根据userid末尾两位数据知道表名为user12,在经过SQL解析程序替换具体执行的SQL,然后获取SQL执行的结果。


总结:SQL解析模式sharding的精细度较高,在相同的资源下可支持的数据量大,对数据库的资源利用率较高,但实现方式比较复杂,需要实现SQL的语法解析程序,拆分的数量与数据源的配置数量以及表的拆分规则成正比, sharding的精细度到表级别


至于sharding带来的维护以及异常case,后面blog的会尝试详细阐述。




2 comments:

  1. 你好,我发现这个文章有三处地方有点问题,估计是不是你写错啦?
    第一处:
    “第一步,定义数据库的shading方式,将数据库通过schema和表的方式切分开来,将4个schema分布到不通的物理机器上,并创建不同命名的表,表的个数是4*10=40。”
    依据你后面写的部分,这里的schema应该为6吧,所以表的个数应该为6*10=60。

    第二处:
    在“SQL语句为: insert into user(userid,email) values(1,'aa@sina.com');
    插入逻辑:根据userid的value=1进行取模10的值=1,知道此时插入的数据源为user01,在根据userid末尾两位数据知道表名为user01,此时的SQL语句中表名已经更改,如果使用ibtais书写SQL,那么静态的SQL无法替换成动态的SQL语句,”中,
    我个人觉得,根据userid末尾两位数据知道表名应该为user11吧?因为数据源user01中的表是从user10~user19并没有user01这个表,

    第三处:
    SQL语句为: select * from user where userid=12;
    查询逻辑:根据userid的value=12进行取模10的值=2,知道此查询的数据源为user02,在根据userid末尾两位数据知道表名为user12,在经过SQL解析程序替换具体执行的SQL,然后获取SQL执行的结果。
    这里也和第二处类似,在根据userid末尾两位数据知道表名应该为user22,理由也是一样,在数据源为user02中,表名的范围也是从user20~user29的,并不存在user12这个表的。

    ReplyDelete
  2. @rainly,非常感谢仔细的阅读,你指出的地方,的确有错误,数据源的关系的计算上需要减一。

    ReplyDelete