当数据大的时候,都会考虑分库分表的实现。分库分表可以在不同的层做。一般来说有以下几种:
无论怎么做分库分表,其基本思路都是一样的。需要有分库路由,分库规则,分库关键字等。
下面简单用Spring在DAO层做一个分库的实现。假如有2个数据源,通过在RouteKey选择不同的数据源。
设计路由关键字
public enum RouteKey {
CURRENT,
HISTORY;
private static Map 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 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 holder = new ThreadLocal(){
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。
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"/>
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"/>
id="dataSource" class="com.ydoing.spring.core.transaction.CustomDataSource">
<property name="targetDataSources">
property>
id="lazyDataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
<property name="targetDataSource" ref="dataSource"/>
id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="lazyDataSource"/>
transaction-manager="txManager" />
id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="lazyDataSource"/>
怎么使用?
@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);
}