Spring Boot實現SAAS平臺的基本思路

 1、SAAS什麼java

 SaaS是Software-as-a-service(軟件即服務)它是一種經過Internet提供軟件的模式,廠商將應用軟件統一部署在本身的服務器mysql

   上,客戶能夠根據本身實際需求,經過互聯網向廠商定購所需的應用軟件服務,按定購的服務多少和時間長短向廠商支付費用,git

 並經過互聯網得到廠商提供的服務。用戶不用再購買軟件,而改用向提供商租用基於Web的軟件,來管理企業經營活動,且無需github

 對軟件進行維護,服務提供商會全權管理和維護軟件。web

 2、SAAS模式有哪些角色redis

 ①服務商:服務商主要是管理租戶信息,按照不一樣的平臺需求可能還須要統合整個平臺的數據,做爲大數據的基礎。服務商在SAASspring

 模式中是提供服務的廠商。sql

 ②租戶:租戶就是購買/租用服務商提供服務的用戶,租戶購買服務後能夠享受相應的產品服務。如今不少SAAS化的產品都會劃分數據庫

 系統版本,不一樣的版本開放不一樣的功能,還有基於功能收費之類的,不一樣的租戶購買不一樣版本的系統後享受的服務也不同。json

   3、SAAS模式有哪些特色

 ①獨立性:每一個租戶的系統相互獨立。

 ②平臺性:全部租戶歸平臺統一管理。

 ③隔離性:每一個租戶的數據相互隔離。

   在以上三個特性裏面,SAAS系統中最重要的一個標誌就是數據隔離性,租戶間的數據徹底獨立隔離。

   4、數據隔離有哪些方案

   ①獨立數據庫

   即一個租戶一個數據庫,這種方案的用戶數據隔離級別最高,安全性最好,但成本較高。

   優勢:

   爲不一樣的租戶提供獨立的數據庫,有助於簡化數據模型的擴展設計,知足不一樣租戶的獨特需求,若是出現故障,恢復數據比較簡單。

   缺點:

   增多了數據庫的安裝數量,隨之帶來維護成本和購置成本的增長。 若是訂價較低,產品走低價路線,這種方案通常對運營商來講是沒法承受的。

   ②共享數據庫,隔離數據架構

   即多個或全部租戶共享數據庫,可是每一個租戶一個Schema。

   優勢:

   爲安全性要求較高的租戶提供了必定程度的邏輯數據隔離,並非徹底隔離,每一個數據庫可支持更多的租戶數量。

   缺點:

   若是出現故障,數據恢復比較困難,由於恢復數據庫將牽涉到其餘租戶的數據 若是須要跨租戶統計數據,存在必定困難。

   ③共享數據庫,共享數據架構

   即租戶共享同一個數據庫、同一個Schema,但在表中增長TenantID多租戶的數據字段。這是共享程度最高、隔離級別最低的模式。 

   優勢:

   三種方案比較,第三種方案的維護和購置成本最低,容許每一個數據庫支持的租戶數量最多。

   缺點:

   隔離級別最低,安全性最低,須要在設計開發時加大對安全的開發量,數據備份和恢復最困難,須要逐表逐條備份和還原。

   若是但願以最少的服務器爲最多的租戶提供服務,而且租戶接受犧牲隔離級別換取下降成本,這種方案最適合。

 5、基於spring boot 、spring-data-jpa實現共享數據庫,隔離數據架構的SAAS系統。

   在實現系統以前咱們須要明白這套實現是共享數據庫,隔離數據架構的,在上面三個方案裏面的第二種,爲何選擇第二種。

 第一種基本上只有對數據的隔離性要求很是高,而且有燒錢買服務器的覺悟才能搞。第三種對數據的隔離性太差,只要在程序實現

 上出現些問題就可能致使數據混亂的問題,而且數據備份還原的代價很是高。因此折中咱們選擇第二種。

   首先在SAAS系統中,通常都是一套系統多個租戶,也就是說全部的租戶共享同一套系統,可是每一個租戶看的數據又要不同。

 肯定了數據隔離級別以後,咱們就須要明確SAAS系統在實現上的難點:①動態建立數據庫;②動態切換數據庫;咱們都知道傳統的

 系統中數據源的信息通常都是寫死在系統配置文件的,在啓動系統的時候加載配置信息建立數據源,這樣的系統是單數據源的。這明顯不適用

 SAAS系統,SAAS系統是有多少個租戶就須要多少個數據源的,而且會根據租戶的信息動態的切換數據源。

 技術準備:spring boot , spring-data-jpa , redis,消息隊列, mysql,maven等。

 工具準備:IDEA,PostMan

 項目結構:這裏準備了兩套系統,平臺管理端和租戶端,這兩套系統是獨立存在的能夠單獨運行。

 

 在demo裏面,管理端(saas-admin)建立的是一個獨立的spring boot項目,這裏只是實現了租戶的註冊,及經過消息隊列通知租戶端建立數據庫。

 首先在saas-admin系統的POM.xml裏面添加依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.31</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

 配置spring boot的全局配置文件application.properties,須要注意spring.jpa.properties.hibernate.hbm2ddl.auto=update屬性,首次啓動須要先建立saas_admin數據庫,不須要建表。

server.port= 8080 spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 # Database spring.datasource.url=jdbc:mysql://127.0.0.1:3306/saas_admin?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false
spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.show-sql=true spring.jpa.properties.hibernate.hbm2ddl.auto=update # Session spring.session.store-type=none # Redis spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= spring.redis.pool.max-active=8 spring.redis.pool.max-wait=-1 spring.redis.pool.max-idle=8 spring.redis.pool.min-idle=0 spring.redis.timeout=1200

準備租戶實體類,這裏使用了spring-data-jpa技術,相似hibernate的用法,使用過hibernate的應該很容易看懂。

@Entity @Table(name = "tenant") public class Tenant implements Serializable { @Id @Column(name = "id",length = 32) private String id; @Column(name = "account",length = 30) private String account; @Column(name = "token",length = 32) private String token; @Column(name = "url",length = 125) private String url; @Column(name = "data_base",length = 30) private String database; @Column(name = "username",length = 30) private String username; @Column(name = "password",length = 32) private String password; @Column(name = "domain_name",length = 64) private String domainName; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDatabase() { return database; } public void setDatabase(String database) { this.database = database; } public String getDomainName() { return domainName; } public void setDomainName(String domainName) { this.domainName = domainName; } public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getToken() { return token; } public void setToken(String token) { this.token = token; }

 接下來直接看註冊租戶的的實現,其實就是保存租戶信息,而後使用redis的消息隊列通知租戶端建立租戶的數據庫,redis消息隊列的實現代碼會放到github上面。

管理端就這樣了,在實際的系統的中租戶通常也是註冊信息到管理端,而且註冊信息的時候能夠選擇使用版本,而且若是系統須要收費的話,也是在支付費用以後纔會發送建立

數據庫的消息。

下面主要看下租戶端,動態建立數據庫和切換數據庫都是發生在租戶端的。

租戶端也是一個獨立的spring boot項目,能夠獨立運行部署,使用的技術徹底和管理端同樣,POM配置徹底相同。

server.port= 9090 spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 # Database spring.datasource.url=jdbc:mysql://127.0.0.1:3306/saas_tenant?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false
spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver # Hibernate spring.jpa.show-sql=true spring.jpa.properties.hibernate.hbm2ddl.auto=update spring.jpa.properties.hibernate.multiTenancy=SCHEMA spring.jpa.properties.hibernate.tenant_identifier_resolver=com.michael.saas.tenant.config.MultiTenantIdentifierResolver spring.jpa.properties.hibernate.multi_tenant_connection_provider=com.michael.saas.tenant.config.MultiTenantConnectionProviderImpl # Session spring.session.store-type=none # Redis spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=finance123 spring.redis.pool.max-active=8 spring.redis.pool.max-wait=-1 spring.redis.pool.max-idle=8 spring.redis.pool.min-idle=0 spring.redis.timeout=1200

全局配置文件application.properties有幾個須要注意的地方。

①spring.jpa.properties.hibernate.multiTenancy=SCHEMA;

這個是hibernate的多租戶模式的支持,咱們這裏配置SCHEMA,表示獨立數據庫;

spring.jpa.properties.hibernate.tenant_identifier_resolver;

租戶ID解析器,簡單來講就是這個設置指定的類負責每次執行sql語句的時候獲取租戶ID;

spring.jpa.properties.hibernate.multi_tenant_connection_provider;

這個設置指定的類負責按照租戶ID來提供相應的數據源;

其中②和③是須要本身實現的;

租戶ID解析器:

public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { // 在沒有提供tenantId的狀況下返回默認數據源
 @Override protected DataSource selectAnyDataSource() { return TenantDataSourceProvider.getTenantDataSource("Default"); } // 提供了tenantId的話就根據ID來返回數據源
 @Override protected DataSource selectDataSource(String tenantIdentifier) { return TenantDataSourceProvider.getTenantDataSource(tenantIdentifier); } }

切換數據源的操做是經過spring的aop機制實現的,能夠看到切換數據源的操做發生在業務層。經過租戶的ID獲取存儲在本地線程中相應數據源完成業務操做。

@Aspect @Order(-1) @Component public class TenantAspect { @Pointcut("execution(public * com.michael.saas.tenant.service..*.*(..))") public void switchTenant(){} @Before("switchTenant()") public void doBefore(JoinPoint joinPoint) throws Throwable { System.out.println(SpObserver.getTenantId()); TenantDataSourceProvider.getTenantDataSource(SpObserver.getTenantId()); } @AfterReturning(returning = "object", pointcut = "switchTenant()") public void doAfterReturning(Object object) throws Throwable { } }

過濾器獲取存放session中的租戶ID存放本地線程

  public class BaseFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; Object obj = req.getSession().getAttribute("TENANTID"); if (null != obj){ SpObserver.putTenantId(obj.toString()); } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }

值得一提,租戶ID是登錄時存放到session中的,也表明着切換數據源的前提是先登錄。

  @PostMapping(value = "/login") public boolean login(@RequestParam("username")String username, @RequestParam("password")String password, HttpServletRequest request ){ try { Tenant tenant = tenantService.findByAccountAndToken(username, password); if (null != tenant){ request.getSession().setAttribute("TENANTID",tenant.getId()); }else { return false; } } catch (Exception e) { e.printStackTrace(); return false; } return true; }

既然是獨立數據庫,就避免不了生成獨立數據庫的問題,我這邊是經過jdbc建立新的數據庫,不必定是好的實現,但能完成基本要求,建立新數據庫的操做在租戶註冊帳號時完成,這裏使用了redis消息隊列去異步生成數據庫。

  @PostMapping(value = "/register") public boolean register(Tenant tenant){ try { tenant.setUrl("jdbc:mysql://127.0.0.1:3306/"); tenant.setDatabase("saas_tenant_" + tenant.getAccount()); tenant.setUsername("root"); tenant.setPassword("root"); tenant = tenantService.save(tenant); queueService.send("register",tenant); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
@Component public class Receiver { private static final Logger logger = Logger.getLogger(Receiver.class); private CountDownLatch latch; @Autowired public Receiver(CountDownLatch latch) { this.latch = latch; } @Autowired private TenantService tenantService; /** * 消息處理 * @param message */
    public void objectMessage(String message) { QueueTemplate queueTemplate = JSON.parseObject(message,QueueTemplate.class); if ("register".equals(queueTemplate.getMethod())){ Tenant tenant = JSON.parseObject(JSON.toJSONString(queueTemplate.getObject()),Tenant.class); try { DBUtils.createDataBase(tenant.getUrl(),tenant.getDatabase(),tenant.getUsername(),tenant.getPassword()); TenantDataSourceProvider.addDataSource(tenant); tenantService.save(tenant); } catch (Exception e) { e.printStackTrace(); } } latch.countDown(); } }

SaaS模式基本實現就介紹到這裏,github地址奉上https://github.com/gm-xiao/saas-demo.git

相關文章
相關標籤/搜索