SpringBoot实现SAAS平台的基本思路
 ⼀、SAAS是什么
 SaaS是Software-as-a-service(软件即服务)它是⼀种通过Internet提供软件的模式,⼚商将应⽤软件统⼀部署在⾃⼰的服务器
上,客户可以根据⾃⼰实际需求,通过互联⽹向⼚商定购所需的应⽤软件服务,按定购的服务多少和时间长短向⼚商⽀付费⽤,
 并通过互联⽹获得⼚商提供的服务。⽤户不⽤再购买软件,⽽改⽤向提供商租⽤基于Web的软件,来管理企业经营活动,且⽆需
 对软件进⾏维护,服务提供商会全权管理和维护软件。
 ⼆、SAAS模式有哪些⾓⾊
 ①服务商:服务商主要是管理租户信息,按照不同的平台需求可能还需要统合整个平台的数据,作为⼤数据的基础。服务商在SAAS
 模式中是提供服务的⼚商。
 ②租户:租户就是购买/租⽤服务商提供服务的⽤户,租户购买服务后可以享受相应的产品服务。现在很多SAAS化的产品都会划分
 系统版本,不同的版本开放不同的功能,还有基于功能收费之类的,不同的租户购买不同版本的系统后享受的服务也不⼀样。
三、SAAS模式有哪些特点
 ①独⽴性:每个租户的系统相互独⽴。
 ②平台性:所有租户归平台统⼀管理。
 ③隔离性:每个租户的数据相互隔离。
在以上三个特性⾥⾯,SAAS系统中最重要的⼀个标志就是数据隔离性,租户间的数据完全独⽴隔离。
四、数据隔离有哪些⽅案
①独⽴数据库
即⼀个租户⼀个数据库,这种⽅案的⽤户数据隔离级别最⾼,安全性最好,但成本较⾼。
优点:
为不同的租户提供独⽴的数据库,有助于简化数据模型的扩展设计,满⾜不同租户的独特需求,如果出现故障,恢复数据⽐较简单。
缺点:
增多了数据库的安装数量,随之带来维护成本和购置成本的增加。如果定价较低,产品⾛低价路线,这种⽅案⼀般对运营商来说是⽆法承受的。
②共享数据库,隔离数据架构
即多个或所有租户共享数据库,但是每个租户⼀个Schema。
优点:
为安全性要求较⾼的租户提供了⼀定程度的逻辑数据隔离,并不是完全隔离,每个数据库可⽀持更多的租户数量。
缺点:
如果出现故障,数据恢复⽐较困难,因为恢复数据库将牵涉到其他租户的数据如果需要跨租户统计数据,存在⼀定困难。
③共享数据库,共享数据架构
即租户共享同⼀个数据库、同⼀个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最⾼、隔离级别最低的模式。
优点:
三种⽅案⽐较,第三种⽅案的维护和购置成本最低,允许每个数据库⽀持的租户数量最多。
缺点:
隔离级别最低,安全性最低,需要在设计开发时加⼤对安全的开发量,数据备份和恢复最困难,需要逐表逐条备份和还原。
如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种⽅案最适合。
 五、基于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系统的l⾥⾯添加依赖
<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>
springboot切换log4j2<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
ding.charset=UTF-8
abled=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.sql.jdbc.Driver
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=update
# Session
spring.session.store-type=none
# Redis
准备租户实体类,这⾥使⽤了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) {
}
接下来直接看注册租户的的实现,其实就是保存租户信息,然后使⽤redis的消息队列通知租户端创建租户的数据库,redis消息队列的实现代码会放到github上⾯。
管理端就这样了,在实际的系统的中租户⼀般也是注册信息到管理端,并且注册信息的时候可以选择使⽤版本,并且如果系统需要收费的话,也是在⽀付费⽤之后才会发送创建
数据库的消息。
下⾯主要看下租户端,动态创建数据库和切换数据库都是发⽣在租户端的。
租户端也是⼀个独⽴的spring boot项⽬,可以独⽴运⾏部署,使⽤的技术完全和管理端⼀样,POM配置完全相同。
server.port= 9090
ding.charset=UTF-8
abled=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.sql.jdbc.Driver
# Hibernate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.ant_identifier_resolver=com.fig.MultiTenantIdentifierResolver
spring.jpa.properties.hibernate.multi_tenant_connection_provider=com.fig.MultiTenantConnectionProviderImpl
# Session
spring.session.store-type=none
# Redis
全局配置⽂件application.properties有⼏个需要注意的地⽅。
①spring.jpa.properties.hibernate.multiTenancy=SCHEMA;
这个是hibernate的多租户模式的⽀持,我们这⾥配置SCHEMA,表⽰独⽴数据库;
②spring.jpa.ant_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() {
TenantDataSource("Default");
}
// 提供了tenantId的话就根据ID来返回数据源
@Override
protected DataSource selectDataSource(String tenantIdentifier) {
TenantDataSource(tenantIdentifier);
}
}
切换数据源的操作是通过spring的aop机制实现的,可以看到切换数据源的操作发⽣在业务层。通过租户的ID获取存储在本地线程中相应数据源完成业务操作。