关于分库分表的实现

来源:互联网 发布:数据抓包工具 编辑:程序博客网 时间:2024/06/05 23:50

当数据大的时候,都会考虑分库分表的实现。分库分表可以在不同的层做。一般来说有以下几种:

  • jdbc层:实现复杂,属于轻量级,对应用基本没有侵入性;缺点是不能复用数据库连接,在应用部署多的时候资源耗费大,不适于大规模部署。类似当当网的sharding-jdbc.
  • ORM层:比如蘑菇街TSharding框架封装mybatis,实现简单。缺点是必须依赖ORM层,侵入性比较大。
  • DBProxy层:如cobar和mycat,可以做到连接复用,性能不错,完全没侵入性。缺点是实现复杂,框架比较重,维护工作量大。
  • DAO层:实现简单,缺点是分表比较麻烦。美团的框架似乎就是这样做到。

无论怎么做分库分表,其基本思路都是一样的。需要有分库路由,分库规则,分库关键字等。

下面简单用Spring在DAO层做一个分库的实现。假如有2个数据源,通过在RouteKey选择不同的数据源。

设计路由关键字

public enum RouteKey {    CURRENT,    HISTORY;    private static Map<String, RouteKey> map = new HashMap<>(values().length);    static {        for (RouteKey routeKey : values()) {            map.put(routeKey.name(), routeKey);        }    }    public static RouteKey convertRoutekey(String key) {        return map.get(key);    }}

设计路由注解

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface DataSourceConfig {    String key();}

路由规则实现,利用了Spring的AOP思想,对DAO方法做个拦截,进来做到对应用的无侵入性。根据传入的参数,绑定不同的路由key到当前线程,为了后续获取connection时候需要。

@Component@Aspect//@Order(0)public class DataSourceAspect {    private static final LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer =            new LocalVariableTableParameterNameDiscoverer();    private static final ConcurrentHashMap<Method, String[]> paramNameMap = new ConcurrentHashMap<>();    @Around(value = "@annotation(dataSourceConfig)", argNames = "joinPoint, dataSourceConfig")//    @Around(value = "@annotation(dataSourceConfig) && args(dataSourceConfig)")    public Object dataSourceAspect(ProceedingJoinPoint joinPoint, DataSourceConfig dataSourceConfig) throws Throwable {        String keyName = dataSourceConfig.key();        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = signature.getMethod();        String[] paramNames = paramNameMap.get(method);        if (paramNames == null) {            synchronized (this) {                String[] names = paramNameMap.get(method);                if (names == null){                    names = parameterNameDiscoverer.getParameterNames(method);                }                paramNames = names;            }        }        Object[] args = joinPoint.getArgs();        RouteKey routeKey;        int i = 0;        for (String name : paramNames) {            if (name.equals(keyName)) {                routeKey = (RouteKey) args[i];                DataSourceKeyHolder.setRouteKey(routeKey);                break;            }            i++;        }        try {            return joinPoint.proceed();        } finally {            DataSourceKeyHolder.clear();        }    }}

扩展AbstractRoutingDataSource类,重载determineCurrentLookupKey方法,路由到不同的库

public class CustomDataSource extends AbstractRoutingDataSource {    @Override    protected Object determineCurrentLookupKey() {        return DataSourceKeyHolder.getRouteKey().name();    }}

DataSourceKeyHolder是个ThreadLocal,将路由key绑定到当前线程。Spring很多涉及事务操作的都会用到ThreadLocal。

public class DataSourceKeyHolder {    private static final ThreadLocal<RouteKey> holder = new ThreadLocal<RouteKey>(){        protected RouteKey initialValue() {            return RouteKey.CURRENT;        }    };    public static RouteKey getRouteKey(){        return holder.get();    }    public static void setRouteKey(RouteKey key){        holder.set(key);    }    public static void clear(){        holder.remove();    }}

数据源配置。Spring事务会在方法前获取数据连接connection,但是这时还没有到DAO层进行路由选择,因此需要延迟加载数据源,需要用到LazyConnectionDataSourceProxy。

 <bean id="dataSource1" class="org.springframework.jdbc.datasource.DriverManagerDataSource">        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>        <property name="url" value="jdbc:mysql://localhost:3306/example"/>        <property name="username" value="root"/>        <property name="password" value="123456"/>    </bean>    <bean id="dataSource2" class="org.springframework.jdbc.datasource.DriverManagerDataSource">        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>        <property name="url" value="jdbc:mysql://localhost:3306/example2"/>        <property name="username" value="root"/>        <property name="password" value="123456"/>    </bean>        <bean id="dataSource" class="com.ydoing.spring.core.transaction.CustomDataSource">      <property name="targetDataSources">          <map>              <entry key="CURRENT" value-ref="dataSource1"/>              <entry key="HISTORY" value-ref="dataSource2"/>          </map>      </property>    </bean>    <bean id="lazyDataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">        <property name="targetDataSource" ref="dataSource"/>    </bean>    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property name="dataSource" ref="lazyDataSource"/>    </bean>    <tx:annotation-driven transaction-manager="txManager" />    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">        <property name="dataSource" ref="lazyDataSource"/>    </bean>

怎么使用?

    @Transactional    @DataSourceConfig(key = "key")    public void update(RouteKey key) {        String sql2 = "insert into customer(name, age, address,  code) values ('Jack', 12, 'BJ', '002')";        int effectNum = jdbcTemplate.update(sql2);        System.out.println(effectNum);    }
1 0