今天遇到一个业务上的需求,因为线上数据库磁盘空间已经接近3个T,想到的一个解决方案是对线上分库分表的64个库做物理拆分,其中编号1-32库放到一个物理空间,33-64库放到一个物理空间。
网上的方案大致有二种:
1.将不同库操作分开放进不同的mapper,配置两个数据源
2. 配置动态数据源,使用aop进行动态切换,真正实现动态数据源
很显然我的系统都是同一套mapper对象,不能拆分,因此采用第二种,一般第二种比较多用于实现读写分离,但是这里我用来做拆分分库分表
其中一个系统因为用的当当的sharding-jdbc做的分库分表,直接修改1-32库的数据库url指向新的地址,另外一个因为用的dbcp的基础BasicDataSource,这里给出实现方案:
1. 首先写一个动态数据源对象继承自AbstractRoutingDataSource
public class NewDynamicDataSource extends AbstractRoutingDataSource { private final Logger logger = LoggerFactory.getLogger(NewDynamicDataSource.class); @Override protected Object determineCurrentLookupKey() { logger.info(String.format("数据源为 %s", NewDataSourceContextHolder.getDB())); return NewDataSourceContextHolder.getDB(); } }
2. 然后配置多数据源 其中
mysqlDataSource3和mysqlDataSource4分别是1-32编号的库,33-64编号的库,都是dbcp的Datasource,也可以是DHCP、C3P0、Druid连接池的数据源连接。
<bean id="dynamicDatasource" class="com.ppdai.realtime.dataservices.dbcp.NewDynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry value-ref="mysqlDataSource3" key="db1_32"></entry> <entry value-ref="mysqlDataSource4" key="db33_64"></entry> </map> </property> <property name="defaultTargetDataSource" ref="mysqlDataSource3"></property> </bean>
3. 第三步 自定义拆分数据库注解
这个注解上有三个属性 tableTotalNum:总表数 pertableNum:每个库表数量
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface DBSource { String tableTotalNum() default "64"; String pertableNum() default "1"; String value() default "db1_32"; }
4. 第四部 数据源设置Holder
这个Holder用来设置当前线程(当前请求,一个请求一个线程处理)中所使用的datasource 名称,其实这里有个漏洞,如果一个请求需要跨库进行查询,这里是满足不了的,不过我这里业务没有这种情况。
public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setDbType(String dbType) { contextHolder.set(dbType); } public static String getDbType() { return ((String) contextHolder.get()); } public static void clearDbType() { contextHolder.remove(); } @Override protected void finalize() throws Throwable { super.finalize(); clearDbType(); } }
5. AOP实现 对查询做切面
在有@DBSource注解的方法上做AOP,然后根据方法参数userid做分库分表。逻辑是(userid % 总表数)/ 每个库表数
@Aspect @Component public class DynamicDataSourceAspect { @Before("@annotation(com.ppdai.realtime.dataservices.dbcp.DBSource)") public void beforeSwitchDS(JoinPoint point){ //获得当前访问的class Class<?> className = point.getTarget().getClass(); //获得访问的方法名 String methodName = point.getSignature().getName(); //得到方法的参数的类型 Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes(); Object[] args = point.getArgs(); String dataSource = NewDataSourceContextHolder.DB_1_32; try { // 得到访问的方法对象 Method method = className.getMethod(methodName, argClass); // 判断是否存在@DS注解 if (method.isAnnotationPresent(DBSource.class)) { DBSource dbSource = method.getAnnotation(DBSource.class); if(args != null &&args.length != 0){ if(args[0] instanceof Integer) { Integer userid = (Integer) args[0]; if(((userid % Integer.valueOf(dbSource.tableTotalNum()))/Integer.valueOf(dbSource.pertableNum())) < 32){ dataSource = NewDataSourceContextHolder.DB_1_32; } else { dataSource = NewDataSourceContextHolder.DB_33_64; } } else if(args[0] instanceof Long) { Long userid = (Long) args[0]; if(((userid % Integer.valueOf(dbSource.tableTotalNum()))/Integer.valueOf(dbSource.pertableNum())) < 32){ dataSource = NewDataSourceContextHolder.DB_1_32; } else { dataSource = NewDataSourceContextHolder.DB_33_64; } } } } } catch (Exception e) { e.printStackTrace(); } // 切换数据源 NewDataSourceContextHolder.setDB(dataSource); } @After("@annotation(com.ppdai.realtime.dataservices.dbcp.DBSource)") public void afterSwitchDS(JoinPoint point){ NewDataSourceContextHolder.clearDB(); } }
6. 最后就是在需要动态选库的函数上使用@DBSource注解 bingo
@DBSource(tableTotalNum = "2048", pertableNum = "32") public Collection<Emailbills> get***(Integer userid) { ** }
函数方法体和方法隐去了。