关于SpringBoot中的多数据源集成
引言
其实对于分库分表这块的场景,目前市场上有很多成熟的开源中间件,eg:MyCAT,Cobar,sharding-JDBC等。
本文主要是介绍基于springboot的多数据源切换,轻量级的一种集成方案,对于小型的应用可以采用这种方案,我之前在项目中用到是因为简单,便于扩展以及优化。
应用场景
假设目前我们有以下几种数据访问的场景:
1.一个业务逻辑中对不同的库进行数据的操作(可能你们系统不存在这种场景,目前都时微服务的架构,每个微服务基本上是对应一个数据库比较多,其他的需要通过服务来方案。),
2.访问分库分表的场景;后面的文章会单独介绍下分库分表的集成。
假设这里,我们以6个库,每个库1000张表的例子来介绍),因为随着业务量的增长,一个库很难抗住这么大的访问量。比如说订单表,我们可以根据userid进行分库分表。
分库策略:userId%6[表的数量];
分库分表策略:库路由[userId/(6*1000)/1000],表路由[ userId/(6*1000)%1000].
集成方案
方案总览:
方案是基于springjdbc中提供的api实现,看下下面两段代码,是我们的切入点,我们都是围绕着这2个核心方法进行集成的。
第一段代码是注册逻辑,会将defaultTargetDataSource和targetDataSources这两个数据源对象注册到resolvedDataSources中。
第二段是具体切换逻辑: 如果数据源为空,那么他就会找我们的默认数据源defaultTargetDataSource,如果设置了数据源,那么他就去读这个值 Object lookupKey = determineCurrentLookupKey();我们后面会重写这个方法。
我们会在配置文件中配置主数据源(默认数据源)和其他数据源,然后在应用启动的时候注册到spring容器中,分别给defaultTargetDataSource和targetDataSources进行赋值,进而进行Bean注册。
真正的使用过程中,我们定义注解,通过切面,定位到当前线程是由访问哪个数据源(维护着一个ThreadLocal的对象),进而调用determineCurrentLookupKey方法,进行数据源的切换。
1 public void afterPropertiesSet() { 2 if (this.targetDataSources == null) { 3 throw new IllegalArgumentException("Property 'targetDataSources' is required"); 4 } 5 this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size()); 6 for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) { 7 Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); 8 DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); 9 this.resolvedDataSources.put(lookupKey, dataSource); 10 } 11 if (this.defaultTargetDataSource != null) { 12 this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); 13 } 14 } 15 16 @Override 17 public Connection getConnection() throws SQLException { 18 return determineTargetDataSource().getConnection(); 19 } 20 21 protected DataSource determineTargetDataSource() { 22 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); 23 Object lookupKey = determineCurrentLookupKey(); 24 DataSource dataSource = this.resolvedDataSources.get(lookupKey); 25 if (dataSource == null && (this.lenientFallback || lookupKey == null)) { 26 dataSource = this.resolvedDefaultDataSource; 27 } 28 if (dataSource == null) { 29 throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); 30 } 31 return dataSource; 32 }
1.动态数据源注册 注册默认数据源以及其他额外的数据源; 这里只是复制了核心的几个方法,具体的大家下载git看代码。
1 // 创建DynamicDataSource 2 GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); 3 beanDefinition.setBeanClass(DyncRouteDataSource.class); 4 beanDefinition.setSynthetic(true); 5 MutablePropertyValues mpv = beanDefinition.getPropertyValues(); 6 //默认数据源 7 mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource); 8 //其他数据源 9 mpv.addPropertyValue("targetDataSources", targetDataSources); 10 //注册到spring bean容器中 11 registry.registerBeanDefinition("dataSource", beanDefinition);
2.在Application中加载动态数据源,这样spring容器启动的时候就会将数据源加载到内存中。
1 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, MybatisAutoConfiguration.class }) 2 @Import(DyncDataSourceRegister.class) 3 @ServletComponentScan 4 @ComponentScan(basePackages = { "com.happy.springboot.api","com.happy.springboot.service"}) 5 public class Application { 6 public static void main(String[] args) { 7 SpringApplication.run(Application.class, args); 8 } 9 }
3.数据源切换核心逻辑:创建一个数据源切换的注解,并且基于该注解实现切面逻辑,这里我们通过threadLocal来实现线程间的数据源切换;
1 import java.lang.annotation.Documented; 2 import java.lang.annotation.ElementType; 3 import java.lang.annotation.Retention; 4 import java.lang.annotation.RetentionPolicy; 5 import java.lang.annotation.Target; 6 7 8 @Target({ ElementType.TYPE, ElementType.METHOD }) 9 @Retention(RetentionPolicy.RUNTIME) 10 @Documented 11 public @interface TargetDataSource { 12 String value(); 13 // 是否分库 14 boolean isSharding() default false; 15 // 获取分库策略 16 String strategy() default ""; 17 } 18 19 // 动态数据源上下文 20 public class DyncDataSourceContextHolder { 21 22 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); 23 24 public static List<String> dataSourceNames = new ArrayList<String>(); 25 26 public static void setDataSource(String dataSourceName) { 27 contextHolder.set(dataSourceName); 28 } 29 30 public static String getDataSource() { 31 return contextHolder.get(); 32 } 33 34 public static void clearDataSource() { 35 contextHolder.remove(); 36 } 37 38 public static boolean containsDataSource(String dataSourceName) { 39 return dataSourceNames.contains(dataSourceName); 40 } 41 } 42 43 /** 44 * 45 * @author jasoHsu 46 * 动态数据源在切换,将数据源设置到ThreadLocal对象中 47 */ 48 @Component 49 @Order(-10) 50 @Aspect 51 public class DyncDataSourceInterceptor { 52 53 @Before("@annotation(targetDataSource)") 54 public void changeDataSource(JoinPoint point, TargetDataSource targetDataSource) throws Exception { 55 String dbIndx = null; 56 57 String targetDataSourceName = targetDataSource.value() + (dbIndx == null ? "" : dbIndx); 58 if (DyncDataSourceContextHolder.containsDataSource(targetDataSourceName)) { 59 DyncDataSourceContextHolder.setDataSource(targetDataSourceName); 60 } 61 } 62 63 @After("@annotation(targetDataSource)") 64 public void resetDataSource(JoinPoint point, TargetDataSource targetDataSource) { 65 DyncDataSourceContextHolder.clearDataSource(); 66 } 67 } 68 69 // 70 public class DyncRouteDataSource extends AbstractRoutingDataSource { 71 @Override 72 protected Object determineCurrentLookupKey() { 73 return DyncDataSourceContextHolder.getDataSource(); 74 } 75 public DataSource findTargetDataSource() { 76 return this.determineTargetDataSource(); 77 } 78 }
4.与mybatis集成,其实这一步与前面的基本一致。
只是将在sessionFactory以及数据管理器中,将动态数据源注册进去【dynamicRouteDataSource】,而非之前的静态数据源【getMasterDataSource()】
1 @Resource 2 DyncRouteDataSource dynamicRouteDataSource; 3 4 @Bean(name = "sqlSessionFactory") 5 public SqlSessionFactory getinvestListingSqlSessionFactory() throws Exception { 6 String configLocation = "classpath:/conf/mybatis/configuration.xml"; 7 String mapperLocation = "classpath*:/com/happy/springboot/service/dao/*/*Mapper.xml"; 8 SqlSessionFactory sqlSessionFactory = createDefaultSqlSessionFactory(dynamicRouteDataSource, configLocation, 9 mapperLocation); 10 11 return sqlSessionFactory; 12 } 13 14 15 @Bean(name = "txManager") 16 public PlatformTransactionManager txManager() { 17 return new DataSourceTransactionManager(dynamicRouteDataSource); 18 }
5.核心配置参数:
1 spring: 2 dataSource: 3 happyboot: 4 driverClassName: com.mysql.jdbc.Driver 5 url: jdbc:mysql://localhost:3306/happy_springboot?characterEncoding=utf8&useSSL=true 6 username: root 7 password: admin 8 extdsnames: happyboot01,happyboot02 9 happyboot01: 10 driverClassName: com.mysql.jdbc.Driver 11 url: jdbc:mysql://localhost:3306/happyboot01?characterEncoding=utf8&useSSL=true 12 username: root 13 password: admin 14 happyboot02: 15 driverClassName: com.mysql.jdbc.Driver 16 url: jdbc:mysql://localhost:3306/happyboot02?characterEncoding=utf8&useSSL=true 17 username: root 18 password: admin
6.应用代码
1 @Service 2 public class UserExtServiceImpl implements IUserExtService { 3 @Autowired 4 private UserInfoMapper userInfoMapper; 5 @TargetDataSource(value="happyboot01") 6 @Override 7 public UserInfo getUserBy(Long id) { 8 return userInfoMapper.selectByPrimaryKey(id); 9 } 10 }
本文转载自:https://blog.csdn.net/xuxian6823091/article/details/81051515