关于分库分表的实现

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

  • 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)
@Documented
public @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);
    }
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值