当数据大的时候,都会考虑分库分表的实现。分库分表可以在不同的层做。一般来说有以下几种:
- 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);
}