我是靠谱客的博主 寂寞菠萝,这篇文章主要介绍利用Spring Cloud开发微服务并实现动态数据源路由详解,现在分享给大家,希望可以做个参考。

  一个典型的微服务架构中,服务应该是没有状态的,但是对于一个多租户的SAAS类系统来说,每个租户都有自己的配置和业务数据,并且不同租户的之间的数据应该要满足一定程度的隔离性。

隔离方案一般有以下三种:

 描述优点缺点
独立数据库一个租户一个数据库隔离级别最高,安全性最好成本较高
共享数据库,隔离数据架构多个或所有租户共享Database,但是每个租户一个Schema为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据
共享数据库,共享数据架构租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段成本最低,允许每个数据库支持的租户数量最多隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;
数据备份和恢复最困难,需要逐表逐条备份和还原

处于安全性和经济性的综合考虑,第二种“共享数据库,隔离数据架构”是大多数SAAS类系统采用的方案。


由于服务是没有状态的且各租户共享的,即一个服务实例可以处理来自不同租户中的用户发起的请求,那么服务必须要支持动态数据源,即当不同租户的用户访问服务时,服务可以动态路由到访问这个租户对应的数据源。

为了模拟这种应用场景,我在2个mysql数据库服务器上建立了几个租户数据库,如图所示:

其中jg6(代表机构6)在192.168.2.135上,jg3、jg4、jg5在192.168.2.143上,每个数据库里都有一个叫userinfo的表,

jg6上数据为:

jg3上的数据为:

jg4上的数据为:

jg5上的数据为:


接下来就是重点了,如何在Spring Cloud体系中优雅的实现动态数据源,思路如下:


  1.  开发一个支持动态配置的Spring Boot Starter 来支持动态数据源,我把它命名为 dynamicDS-spring-boot-starter,
    这个starter中包含一个@EnableDynamicDS注解。
  2. 需要动态数据源支持的微服务需要在pom.xml添加dynamicDS-spring-boot-starter依赖,并在应用启动类上添加
    @EnableDynamicDS注解。


dynamicDS-spring-boot-starter的组成,如图所示:

  • Application.java,自动生成的类,没啥意义,仅仅为了支持mvn clean install 编译而已,不然会不成功,提示没有Main Class.
  • DynamicDatasourceConfigProperties,这个类是为了接受微服务中动态数据源的配置(application.yml),具体怎么定义一个类接受application.yml的配置,请查阅相关文档,简单讲就是定义Map<String, String>和嵌套的Map--Map<String, Map<String,String>>,就能解决绝大多数问题了。
    package com.tay.dynamicds;
    
    import java.util.Map;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    import lombok.Data;
    
    @ConfigurationProperties(prefix = "dynamicds")
    @Data
    public class DynamicDatasourceConfigProperties {
    	private String orgCodeHeader;
    	private Map<String, String> general;
    	private Map<String, Map<String, String>> tenants;
    }
    
    微服务中application.yml中对应的动态数据源配置部分为:
    dynamicds:
      orgCodeHeader: orgCode  
      general:
        maxPoolSize: 10
        minIdle: 3
        defaultTenant : jg3
      tenants:
        jg3:
          url: jdbc:mysql://192.168.2.143:3306/jg3
          userName: root
          password: password
        jg4:
          url: jdbc:mysql://192.168.2.143:3306/jg4
          userName: root
          password: password
        jg5:
          url: jdbc:mysql://192.168.2.143:3306/jg5
          userName: root
          password: password
          maxPoolSize: 20
          minIdle: 6
        jg6:
          url: jdbc:mysql://192.168.2.135:3306/jg6
          userName: root
          password: password   
    你仔细对照阅读配置读取类和配置,就会明白他们之间的对应关系。
  • DynamicDSAutoConfiguration.java,动态数据源自动配置类,在里面定义了一些必要的Bean。
    package com.tay.dynamicds;
    
    import javax.sql.DataSource;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @EnableConfigurationProperties(DynamicDatasourceConfigProperties.class)
    public class DynamicDSAutoConfiguration {
    	
    	@Autowired
    	private DynamicDatasourceConfigProperties properties;
    	
    	@Bean
        @ConditionalOnMissingBean
        @ConditionalOnClass(SaasDynamicDatasource.class)
    	DataSource dataSource (){
    		SaasDynamicDatasource ds = new SaasDynamicDatasource();
    		ds.setDsProperties(properties);
    		return ds;
        }
    	
    	@Bean
        @ConditionalOnMissingBean
        @ConditionalOnClass(SaasDynamicDatasource.class)
    	OrgCodeInterceptor orgCodeInterceptor() {
    		OrgCodeInterceptor interceptor = new OrgCodeInterceptor();
    		interceptor.setOrgCodeHeaderName(properties.getOrgCodeHeader());
    		interceptor.setValidOrgCodes(properties.getTenants().keySet());
    		return interceptor;
    	}
    	
    	@Bean
        @ConditionalOnMissingBean
        @ConditionalOnClass(SaasDynamicDatasource.class)
    	InterceptorRegister interceptorRegister() {
    		InterceptorRegister interceptorRegister = new InterceptorRegister();
    		return interceptorRegister;
    	}
    }
    
    其中的@EnableConfigurationProperties(DynamicDatasourceConfigProperties.class) 这句很重要,是把配置读取类注入到本自动配置类。这个自动配置类中定义本方案中三个重要核心类,一个是datasource,实现类为SaasDynamicDatasource,一个orgCodeInterceptor,实现类为OrgCodeInterceptor,一个是interceptorRegister,实现类为InterceptorRegister。
        注意这三个Bean的声明上都加了这两个条件注解
    @ConditionalOnMissingBean
    @ConditionalOnClass(SaasDynamicDatasource.class)
    第一个意思为,只有不存在同名同类bean时,此bean才生效
    第二个意思为,只有classpath中存在SaasDynamicDatasource类时,此bean才生效。
  • EnableDynamicDS.java, 动态数据源自动配置引入注解
    package com.tay.dynamicds;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    
    import org.springframework.context.annotation.Import;
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Import(DynamicDSAutoConfiguration.class)
    public @interface EnableDynamicDS {
    
    }
    
    最重要就是这个@Import(DynamicDSAutoConfiguration.class) 注解。
  • OrgCodeInterceptor.java ,该类定义服务请求拦截器,主要是实现preHandle、postHandle两个方法,preHandle会在服务请求执行前执行,它负责析取请求header中的orgCode(机构码),并把它塞入ThreadLocal全局变量中。 postHandle方法则负责在服务请求执行后(就算服务抛出异常也会被执行),主要是清除ThreadLocal全局变量中的内容,为下一个请求到来做准备。
    package com.tay.dynamicds;
    
    import java.util.Set;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    public class OrgCodeInterceptor implements HandlerInterceptor{
    	private static final Logger LOGGER = LoggerFactory.getLogger(HandlerInterceptor.class);
    	private String orgCodeHeaderName = "orgCode";
    	
    	private Set<String> validOrgCodes;
    	
    	
    	
    	public void setOrgCodeHeaderName(String orgCodeName) {
    		orgCodeHeaderName = orgCodeName;
    	}
    	
    	public void setValidOrgCodes(Set<String> validOrgCodes) {
    		this.validOrgCodes = validOrgCodes;
    	}
    	
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
            System.out.printf("preHandle被调用");
            String orgCodeVal = httpServletRequest.getHeader(orgCodeHeaderName);
            if(orgCodeVal == null) {
            	LOGGER.error("The request without a header named as " + orgCodeHeaderName);
            	return false;
            }
            if(!validOrgCodes.contains(orgCodeVal)) {
            	LOGGER.error(String.format(" the orgCode %s is not valid.", orgCodeVal));
            	return false;
            }
            OrgCodeHolder.putOrgCode(httpServletRequest.getHeader(orgCodeHeaderName));
            return true;    
        }
        @Override
        public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
            System.out.println("postHandle被调用");
            OrgCodeHolder.remove();
        }
    	
    	
    }
    
  • InterceptorRegister.java 拦截器的注册类,负责让orgCodeInterceptor生效。
    package com.tay.dynamicds;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    
    public class InterceptorRegister implements WebMvcConfigurer{
    	@Autowired
    	private OrgCodeInterceptor orgCodeInterceptor;
    	
    	@Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(orgCodeInterceptor);
        }
    	
    }
    
  • OrgCodeHolder.java, orgCode(机构码)的ThreadLocal全局静态变量。
    package com.tay.dynamicds;
    
    public class OrgCodeHolder {
    	static final ThreadLocal<String> holder = new ThreadLocal<String>();
    
    	public static void putOrgCode(String orgCode) {
    		holder.set(orgCode);
    	}
    	
    	public static void remove() {
    		holder.remove();
    	}
    
    	public static String getOrgCode() {
    		return holder.get();
    	}
    }
    
  • SaasDynamicDatasource.java 动态数据源真正实现类,继承了org.springframework.jdbc.datasource.AbstractDataSource,最重要的方法有两个,一个是private void parse(DynamicDatasourceConfigProperties dsProperties2),它负责将动态数据源的配置信息解析。另外一个重要方法为getConnection(),当一个请求需要访问数据源时,它会根据OrgCodeHolder.getOrgCode()得到当前请求线程的orgCode,尝试从缓存中按照orgCode查找是否存在对应的数据源,如果不存在,则会根据配置信息构造一个HikariDataSource数据源实例塞入缓存中并得到Connection,然后把得到的Connection对象返回。
    package com.tay.dynamicds;
    
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.sql.SQLFeatureNotSupportedException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.WeakHashMap;
    
    import javax.sql.DataSource;
    
    import org.springframework.jdbc.datasource.AbstractDataSource;
    
    import com.zaxxer.hikari.HikariConfig;
    import com.zaxxer.hikari.HikariDataSource;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    public class SaasDynamicDatasource extends AbstractDataSource{
    
    	private Map<String, DataSource> dataSourceMap = new WeakHashMap<String, DataSource>();
    	
    	private GeneralAttributes generalAttributes;
    	private Map<String, TenantDatasourceAttributes> tenantDatasourceAttributesMap;
    	
    	
    	public void setDsProperties(DynamicDatasourceConfigProperties dsProperties) {
    		parse(dsProperties);
    	}
    	
    	private void parse(DynamicDatasourceConfigProperties dsProperties2) {
    		Map<String, String> generalMap = dsProperties2.getGeneral();
    		generalAttributes = new GeneralAttributes();
    		generalAttributes.setMaxPoolSize(Integer.parseInt(generalMap.get("maxPoolSize")));
    		generalAttributes.setMinIdle(Integer.parseInt(generalMap.get("minIdle")));
    		generalAttributes.setDefaultTenant(generalMap.get("defaultTenant"));
    
    		Map<String, Map<String, String>> tenants = dsProperties2.getTenants();
    		tenantDatasourceAttributesMap = new HashMap<String, TenantDatasourceAttributes>();
    		
    		for (String orgCode : tenants.keySet()) {
    			Map<String, String> tenantDSAttr = tenants.get(orgCode);
    			TenantDatasourceAttributes tenantDatasourceAttributes = new TenantDatasourceAttributes();
    			tenantDatasourceAttributes.setUrl(tenantDSAttr.get("url"));
    			tenantDatasourceAttributes.setUserName(tenantDSAttr.get("userName"));
    			tenantDatasourceAttributes.setPassword(tenantDSAttr.get("password"));
    			if(tenantDSAttr.containsKey("maxPoolSize")) {
    				tenantDatasourceAttributes.setMaxPoolSize(Integer.parseInt(tenantDSAttr.get("maxPoolSize")));
    			}
    			else {
    				tenantDatasourceAttributes.setMaxPoolSize(generalAttributes.getMaxPoolSize());
    			}
    			if(tenantDSAttr.containsKey("minIdle")) {
    				tenantDatasourceAttributes.setMinIdle(Integer.parseInt(tenantDSAttr.get("minIdle")));
    			}
    			else {
    				tenantDatasourceAttributes.setMinIdle(generalAttributes.getMinIdle());
    			}
    			tenantDatasourceAttributesMap.put(orgCode, tenantDatasourceAttributes);
    		}
    		
    	}
    
    	@Data
    	@NoArgsConstructor
    	@AllArgsConstructor
    	private static class GeneralAttributes {
    		private int maxPoolSize;
    		private int minIdle;
    		private String defaultTenant;
    	}
    	
    	@Data
    	@NoArgsConstructor
    	@AllArgsConstructor
    	private static class TenantDatasourceAttributes {
    		private String url;
    		private String userName;
    		private String password;
    		private int maxPoolSize;
    		private int minIdle;
    	}
    
    	@Override
    	public Connection getConnection() throws SQLException {
    		String currentOrgCode = OrgCodeHolder.getOrgCode();
    		if(currentOrgCode == null) {
    			currentOrgCode = generalAttributes.getDefaultTenant();
    		}
    		if(!tenantDatasourceAttributesMap.containsKey(currentOrgCode)) {
    			throw new SQLException("there is no datasource configuration for the organization with code " + currentOrgCode);
    		}
    		TenantDatasourceAttributes tenantDatasourceAttributes = tenantDatasourceAttributesMap.get(currentOrgCode);
    		DataSource ds = dataSourceMap.get(currentOrgCode);
    		//double check
    		if(ds == null) {
    			synchronized(this) {
    				ds = dataSourceMap.get(currentOrgCode);
    				if(ds == null) {
    					HikariConfig config = new HikariConfig();
    					config.setDriverClassName("com.mysql.jdbc.Driver");
    					config.setJdbcUrl(tenantDatasourceAttributes.getUrl());
    					config.setUsername(tenantDatasourceAttributes.getUserName());
    					config.setPassword(tenantDatasourceAttributes.getPassword());
    					config.setMaximumPoolSize(tenantDatasourceAttributes.getMaxPoolSize());
    					config.setMinimumIdle(tenantDatasourceAttributes.getMinIdle());
    					ds =  new HikariDataSource(config);
    					dataSourceMap.put(currentOrgCode, ds);
    				}
    			}
    		}
    		return ds.getConnection();
    	}
    
    	@Override
    	public Connection getConnection(String username, String password) throws SQLException {
    		throw new SQLFeatureNotSupportedException();
    	}
    }
    
  • 在这个工程目录下执行mvn clean install -U


    可以看到dynamicDS-spring-boot-starter-0.0.1-SNAPSHOT.jar生成了,并被放入了maven本地库中。

    下面开讲如何利用这个动态数据源starter开发自己的微服务并能支持动态数据源,步骤如下:
  • 利用wizard开启一个新的Spring Starter Project


  • 在pom.xml添加dynamicDS-spring-boot-starter依赖


  • Spring Boot 启动类上添加@EnableDynamicDS注解


package com.tay.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.tay.dynamicds.EnableDynamicDS;

@SpringBootApplication
@EnableDynamicDS
public class DynamicDsSampleApplication {
	public static void main(String[] args) {
		SpringApplication.run(DynamicDsSampleApplication.class, args);
	}
}

  • 在application.yml中添加动态数据源配置
    server:
      port: 8888
    
    spring:
      application:
        name: dynamicDSSample
          
    dynamicds:
      orgCodeHeader: orgCode                            #orgCode HTTP header name
      general:                                          #通用默认配置,目前仅支持两项,具体机构数据源可覆盖通用配置 
        maxPoolSize: 10                                 #数据源连接池最大连接数
        minIdle: 3                                      #数据源连接池最小idle数 
        defaultTenant : jg3                             #默认数据源,可设置为一个空的database,仅供Spring cloud检查数据源的可连接性 
      tenants:                                          #具体机构的数据源配置
        jg3:
          url: jdbc:mysql://192.168.2.143:3306/jg3
          userName: root
          password: password
        jg4:
          url: jdbc:mysql://192.168.2.143:3306/jg4
          userName: root
          password: password
        jg5:
          url: jdbc:mysql://192.168.2.143:3306/jg5
          userName: root
          password: password
          maxPoolSize: 20                               #覆盖默认配置 
          minIdle: 6                                    #覆盖默认配置
        jg6:
          url: jdbc:mysql://192.168.2.135:3306/jg6
          userName: root
          password: password   

  • 工程的目录结构
  • UserInfo.java,标准的pojo bean兼Entity
    package com.tay.demo.entity;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.Table;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "userinfo")
    public class UserInfo {
    	@Id
        @GeneratedValue(strategy=GenerationType.AUTO)
    	private Integer id;
    	private String name;
    	private String password;
    	private String orgcode;
    }
    
  • UserInfoRepository.java, Spring JPA DAO层
    package com.tay.demo.dao;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    import com.tay.demo.entity.UserInfo;
    
    @Repository
    public interface UserInfoRepository extends JpaRepository<UserInfo, Integer>{
    
    }
    
  • MainController.java, RestController类
    package com.tay.demo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tay.demo.dao.UserInfoRepository;
    import com.tay.demo.entity.UserInfo;
    
    
    
    
    @RestController
    public class MainController {
    
    	@Autowired
    	private UserInfoRepository userInfoRepository;
    
    	@GetMapping("/{id}")
    	public UserInfo findById(@PathVariable Integer id) {
    		UserInfo findOne = userInfoRepository.findById(id).get();
    		return findOne;
    	}
    }
    
  • 从DynamicDsSampleApplication启动demo项目,可以启动后占用端口

  • 利用RESTED Client 或者Postman等工具测试


可以发现,请求中headers的orgCode从jg3、jg4、jg5、jg6,返回不同数据源的数据,动态数据源大功告成。


改进建议:

目前数据源的配置是放在application.yml里的,如要达成真正的动态数据源,比如不停服务增加新机构和新数据源,则动态数据源的配置信息必须放入一个可动态监听更新的环境中去,比如zookeeper中,比如Spring Cloud Config Server中,阿里云的ACM中等,一旦数据源配置信息发生变动时,则运行中微服务可以接受到变更通知刷新配置,以便可实时动态支持新的机构和数据源,这个就等着读者自己去完成了。


项目完整代码:
dynamicDS-spring-boot-starter : https://github.com/tangaiyun/dynamicDS-spring-boot-starter

dynamicDSSample: https://github.com/tangaiyun/dynamicDSSample






最后

以上就是寂寞菠萝最近收集整理的关于利用Spring Cloud开发微服务并实现动态数据源路由详解的全部内容,更多相关利用Spring内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(39)

评论列表共有 0 条评论

立即
投稿
返回
顶部