简介
- 场景:在实际业务中,数据量迅速增长,一个库一个表已经满足不了我们的需求了,因此考虑分库分表的操作。
- 目的:在Springboot中实现多数据源,并动态切换数据源,写数据用主数据库,查数据用从数据库,实现读写分离。
- 原理:
- 首先要将主、从数据库都加入自定义的数据源库DynamicRouterDataSource中(继承自springboot自己的数据源库AbstractRoutingDataSource。
- 然后需要将此数据源库注册成名为datasource的bean,取代springboot原装的datasource这个bean,那么就要借助ImportBeanDefinitionRegistrar注册bean的能力,把DynamicRouterDataSource放进ImportBeanDefinitionRegistrar的实现类去注册成bean。
- 由于项目开始时会找到datasource这个bean,将主数据源设置为数据源,然后每次执行SQL时调用其determineCurrentLookupKey(),将返回的数据源设置为接下来要使用的数据源,所以我们应该在数据库操作方法执行前将设法使其返回值变为我们指定的数据源,那么可以自定义一个管理不同线程当前数据源的DynamicDataSourceContextHolder类,再自定义一个注解,其值为指定数据源,然后通过aop拦截带此注解的方法,获取注解值,将注解值替换DynamicDataSourceContextHolder类中本线程的当前数据源变量,并将determineCurrentLookupKey()重写为从DynamicDataSourceContextHolder类中获取本线程当前数据源变量。PS:我们使用一个string类的key来标识每个数据源。
数据库
- 三个数据库:一个主,两个从,都是维护一张user表,主的user表为空,等待程序写入,从的数据库有数据,等待读。
示例
-
DynamicDataSourceContextHolder:持有线程当前使用的数据源的key。
public class DynamicDataSourceContextHolder { private static final Logger logger= LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); //存储已经注册的数据源的key public static List<String> dataSourceIds=new ArrayList<String>(); //线程级别的key副本,存储在本线程的副本变量集合中, //每条记录的键为类似HOLDER的ThreadLocal变量,值为类似数据源key的object对象 private static final ThreadLocal<String> HOLDER=new ThreadLocal<String>(); /** * @Author haien * @Description 获取当前线程当前数据源的key * @Date 2019/6/10 * @Param [] * @return java.lang.String **/ public static String getDataSourceRouterKey(){ return HOLDER.get(); } /** * @Author haien * @Description 切换当前线程的数据源 * @Date 2019/6/10 * @Param [dataSourceRouterKey] * @return void **/ public static void setDataSourceRouterKey(String dataSourceRouterKey){ logger.info("切换至{}数据源",dataSourceRouterKey); HOLDER.set(dataSourceRouterKey); } /** * @Author haien * @Description 设置完数据源后移除当前数据源,因为每个线程的HOLDER变量只能有一个, 防止下次切换数据源set不进来 * @Date 2019/6/9 * @Param [] * @return void **/ public static void removeDataSourceRouterKey(){ HOLDER.remove(); } /** * @Author haien * @Description 判断指定DataSource是否存在 * @Date 2019/6/9 * @Param [dataSourceId] * @return boolean **/ public static boolean containsDataSource(String dataSourceId){ return dataSourceIds.contains(dataSourceId); } }
-
DynamicRouterDataSource:通知程序使用DynamicDataSourceContextHolder当前持有的数据源key作为数据源;继承AbstractRoutingDataSource,重写determineCurrentLookupKey(),每执行一次前都会自动调用该方法来重新设置数据源。
public class DynamicRouterDataSource extends AbstractRoutingDataSource { private static final Logger logger= LoggerFactory.getLogger(DynamicRouterDataSource.class); /** * @Author haien * @Description 查找当前线程当前数据源,AbstractRoutingDataSource会自动 * 在每次执行SQL时调用该方法设置数据源 * @Date 2019/6/10 * @Param [] **/ @Override protected Object determineCurrentLookupKey() { //默认数据源没有key,打印null String dataSourceName=DynamicDataSourceContextHolder.getDataSourceRouterKey(); logger.info("当前数据源:{}",dataSourceName); return DynamicDataSourceContextHolder.getDataSourceRouterKey(); } }
-
DynamicDataSourceRegister:把DynamicRouterDataSource注册成名为datasource的bean,取代原装datasource;主要是读取配置文件中的数据源属性,绑定到数据源类上,再把这些数据源类加入数据源库DynamicRouterDataSource中,然后把数据源库注册成bean。
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar,EnvironmentAware { private static final Logger logger= LoggerFactory.getLogger(DynamicDataSourceRegister.class); //配置文件的获取工具 private Environment evn; //别名 private final static ConfigurationPropertyNameAliases aliases= new ConfigurationPropertyNameAliases(); //存储所有数据源 private Map<String,DataSource> customDataSources=new HashMap<String, DataSource>(); //属性绑定工具,springboot 2.0新推出 private Binder binder; /** * @Author haien * @Description 由于部分数据源配置不同,所以在此处添加别名, * 避免切换数据源出现某些参数无法注入的情况 * @Date 2019/6/9 **/ static { aliases.addAliases("url",new String[]{"jdbc-url"}); aliases.addAliases("username",new String[]{"user"}); } /** * @Author haien * @Description 实现ImportBeanDefinitionRegistrar接口的方法, * 该方法可以按照自己的方式注册bean * @Date 2019/6/9 * @Param [annotationMetadata, beanDefinitionRegistry] * @return void **/ @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { //主、从数据源属性的Map对象 Map dataSourceProperties,config; //主、从数据源的DataSource对象 DataSource defaultDataSource,consumerDataSource; /* * 绑定主数据源 */ //获取主数据源配置的map对象 dataSourceProperties= binder.bind("spring.datasource.master",Map.class).get(); //获取数据源类型;type:com.zaxxer.hikari.HikariDataSource String typeStr=evn.getProperty("spring.datasource.master.type"); //根据数据源类型获取数据源类对象 Class<? extends DataSource> clazz=getDataSourceType(typeStr); //绑定默认数据源参数,也就是主数据源 defaultDataSource=bind(clazz,dataSourceProperties); DynamicDataSourceContextHolder.dataSourceIds.add("master"); logger.info("注册默认数据源成功"); /* * 绑定从数据源 */ //获取其他数据源配置 List<Map> configs=binder.bind("spring.datasource.cluster", Bindable.listOf(Map.class)).get(); //遍历从数据源 for(int i=0; i<configs.size(); i++){ config=configs.get(i); clazz=getDataSourceType((String)config.get("type")); //将从数据库属性映射为数据源对象 consumerDataSource=bind(clazz,config); //获取数据源的key,通过该key可以定位到数据源 String key=config.get("key").toString(); //加入数据库大本营,key为键,数据源对象为值 customDataSources.put(key,consumerDataSource); //数据源上下文,用于管理数据源与记录已经注册的数据源key DynamicDataSourceContextHolder.dataSourceIds.add(key); logger.info("注册数据源{}成功",key); } /* * 注册bean */ //将需要注册的类用GenericBeanDefinition包装一下 GenericBeanDefinition define=new GenericBeanDefinition(); //设置bean的类型 define.setBeanClass(DynamicRouterDataSource.class); //需要注入的参数 MutablePropertyValues mpv=define.getPropertyValues(); //添加默认数据源(即主数据源),AbstractRoutingDataSource中维护一个 //Object类型的defaultTargetDataSource,和一个Map类型的targetDataSources, //初始时设置defaultTargetDataSource为项目数据源,此后每执行一次SQL //都自动切换这两者之中的指定数据源 mpv.add("defaultTargetDataSource",defaultDataSource); //添加其他数据源 mpv.add("targetDataSources",customDataSources); //将define注册为bean并命名为datasource,不使用springboot自动生成的DataSource beanDefinitionRegistry.registerBeanDefinition("datasource",define); logger.info("注册数据源成功,共注册{}个数据源", customDataSources.keySet().size()+1); } /** * @Author haien * @Description 通过字符串获取数据源class对象 * @Date 2019/6/10 * @Param [typeStr] * @return java.lang.Class<? extends javax.sql.DataSource> **/ private Class<? extends DataSource> getDataSourceType(String typeStr){ Class<? extends DataSource> type; try { //如果字符串不为空 if(StringUtils.hasLength(typeStr)){ //将字符串映射为对应的class对象 type=(Class<? extends DataSource>) Class.forName(typeStr); } else{ //默认为hikariCP数据源,与springboot默认数据源保持一致 type=HikariDataSource.class; } return type; } catch (ClassNotFoundException e) { //无法通过反射获取class对象时抛出异常, //该情况一般是写错了,故抛出一个RuntimeException throw new IllegalArgumentException( "can not resolve class with type:"+typeStr); } } /** * @Author haien * @Description 绑定属性到数据源对象 * @Date 2019/6/10 * @Param [result, properties] * @return void **/ private void bind(DataSource result,Map properties){ //将map对象properties封装为属性对象 ConfigurationPropertySource source= new MapConfigurationPropertySource(properties); Binder binder=new Binder( new ConfigurationPropertySource[]{source.withAliases(aliases)}); //将属性绑定到对象 binder.bind(ConfigurationPropertyName.EMPTY,Bindable.ofInstance(result)); } /** * @Author haien * @Description 绑定属性到指定对象 * @Date 2019/7/2 * @Param [clazz, properties] * @return T **/ public <T extends DataSource> T bind(Class<T> clazz,Map properties){ //将properties封装为属性对象 ConfigurationPropertySource source= new MapConfigurationPropertySource(properties); Binder binder=new Binder( new ConfigurationPropertySource[]{source.withAliases(aliases)} ); //通过类型绑定属性并获取实例对象 return binder.bind(ConfigurationPropertyName.EMPTY,Bindable.of(clazz)).get(); } /** * @Author haien * @Description * @Date 2019/6/10 * @Param [clazz, sourcePath属性路径,如,spring.datasource] * @return T **/ private <T extends DataSource> T bind(Class<T> clazz,String sourcePath){ Map properties=binder.bind(sourcePath,Map.class).get(); return bind(clazz,properties); } /** * @Author haien * @Description 实现EnvironmentAware接口的方法,通过aware的方式注入Environment对象 * @Date 2019/6/10 * @Param [environment] * @return void **/ @Override public void setEnvironment(Environment environment) { logger.info("开始注册数据源"); this.evn=environment; //绑定配置器 binder=Binder.get(evn); } }
-
然后需要在启动类增加以下代码,发现一下这个有能力注册bean的类,否则它也注册不了bean。
@Import(DynamicDataSourceRegister.class)
-
DataSource注解:功能是被注解的方法都能切换数据源。
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { //该值即key值 String value() default "master"; }
-
UserMapper:操作数据库的类,方法加上了@DataSource注解。
public interface UserMapper { /** * @Author haien * @Description 新增用户 * @Date 2019/6/12 * @Param [user] * @return int **/ @DataSource //默认数据源 int save(User user); /** * @Author haien * @Description 根据id查询 * @Date 2019/6/12 * @Param [id] * @return com.haien.dynamic.entity.User **/ @DataSource("slave1") User selectById(Long id); /** * @Author haien * @Description 查询所有用户信息 * @Date 2019/6/12 * @Param [] * @return java.util.List<com.haien.dynamic.entity.User> **/ @DataSource("slave2") List<User> selectAll(); }
-
dao接口的映射文件略。
-
最后使用aop来拦截带注解的类或方法,以下提供两种方案。
- 注解式aop。
-
DynamicDataSourceAspect:使用aop拦截带有@Datasource的类和方法。
@Aspect @Component public class DynamicDataSourceAspect { private static final Logger logger= LoggerFactory.getLogger(DynamicDataSourceAspect.class); //在注解方法执行前先切换数据源 //@annotation(ds):会拦截注解ds的方法,根据方法签名,参数ds即DataSource注解类 @Before("@annotation(ds)") public void changeDataSource(JoinPoint point, DataSource ds){ String dsId=ds.value(); if(DynamicDataSourceContextHolder.dataSourceIds.contains(dsId)){ logger.debug("切换数据源:{} > {}",dsId,point.getSignature()); //切换数据源 DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId); }else{ logger.info("数据源[{}]不存在,使用默认数据源 > {}", dsId,point.getSignature()); } } //方法执行后删除键值对,防止下次切换时该键已有值导致set不进来 @After("@annotation(ds)") public void restoreDataSource(JoinPoint point,DataSource ds){ logger.debug("切换数据源:"+ds.value()+">"+point.getSignature()); DynamicDataSourceContextHolder.removeDataSourceRouterKey(); } }
- 编程式aop
-
DynamicDataSourceAnnotationInterceptor:实现MethodInterceptor接口,定义around型切面。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { private static final Logger logger= LoggerFactory.getLogger(DynamicDataSourceAnnotationInterceptor.class); //缓存方法注解值 private static final Map<Method,String> METHOD_CACHE=new HashMap<>(); @Override public Object invoke(MethodInvocation invocation) throws Throwable { try { //获取方法注解值 String datasource = determineDatasource(invocation); if (!DynamicDataSourceContextHolder.containsDataSource(datasource)) { logger.info("数据源[{}]不存在,使用默认数据源", datasource); } DynamicDataSourceContextHolder.setDataSourceRouterKey(datasource); //继续执行被拦截的方法 return invocation.proceed(); } finally { DynamicDataSourceContextHolder.removeDataSourceRouterKey(); } } /** * @Author haien * @Description 判断当前方法是否含有注解,是则获取其注解值 * @Date 2019/7/2 * @Param [invocation] * @return java.lang.String **/ private String determineDatasource(MethodInvocation invocation){ //获取被拦截的方法 Method method=invocation.getMethod(); //若方法-注解值集合中含有该方法 if(METHOD_CACHE.containsKey(method)){ //直接返回其注解值 return METHOD_CACHE.get(method); } else { //若该方法含有注解,则获取注解值,否则不知如何 DataSource ds=method.isAnnotationPresent(DataSource.class) ? method.getAnnotation(DataSource.class): AnnotationUtils.findAnnotation(method.getDeclaringClass(), DataSource.class); //加入集合 METHOD_CACHE.put(method,ds.value()); //返回注解值 return ds.value(); } } }
-
DynamicDataSourceAnnotationAdvisor:,继承AbstractPointcutAdvisor,定义切面加切点。
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { //切面 private Advice advice; //切点 private Pointcut pointcut; public DynamicDataSourceAnnotationAdvisor( DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) { this.advice = dynamicDataSourceAnnotationInterceptor; this.pointcut = buildPointcut(); } @Override public Pointcut getPointcut() { return this.pointcut; } @Override public Advice getAdvice() { return this.advice; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (this.advice instanceof BeanFactoryAware) { ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); } } /** * @Author haien * @Description 切点是带有DataSource注解的类或方法 * @Date 2019/7/3 * @Param [] * @return org.springframework.aop.Pointcut **/ private Pointcut buildPointcut() { //类注解 Pointcut cpc = (Pointcut) new AnnotationMatchingPointcut( DataSource.class, true); // 类注解 Pointcut clpc = (Pointcut) AnnotationMatchingPointcut .forClassAnnotation(DataSource.class); // 方法注解 Pointcut mpc = (Pointcut) AnnotationMatchingPointcut .forMethodAnnotation(DataSource.class); return new ComposablePointcut(cpc).union(clpc).union(mpc); } }
-
最后需要在启动类将此增强类注册为bean:
@Bean public DynamicDataSourceAnnotationAdvisor dynamicDataSourceAnnotationAdvisor(){ return new DynamicDataSourceAnnotationAdvisor( new DynamicDataSourceAnnotationInterceptor()); }
-
代码示例:ideaProjects/dynamic-switch-datasource