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