一、介绍
什么是多租户架构?
-
共享数据库,独立数据表:此模式下,所有租户共同使用一个数据库实例,但每个租户都拥有自己独立的数据表,以此实现数据的逻辑隔离。 -
共享数据库,共享数据表:在这种实现中,所有租户不仅共享同一个数据库实例,还共享相同的数据表。为了区分不同租户的数据,通常会在数据表中引入租户标识字段,以此实现数据的物理共存、逻辑隔离。 -
独立数据库:这是最为严格的数据隔离方式,每个租户都拥有自己独立的数据库实例。这种方式虽然能够最大限度地保证数据的安全性和隔离性,但也可能带来较高的资源消耗和管理成本。
二、SpringBoot 配置多租户
实现步骤:
-
设置基本项目结构。
-
配置数据源和Hibernate拦截器。
-
创建租户解析器。
-
配置实体和存储库。
-
测试多租户的实现。
基本项目结构:
在项目的pom.xml文件中添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
配置数据源和Hibernate拦截器
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
创建控制类,用来实现添加用户上下文信息和拦截数据库作用:
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Component
public class TenantInterceptor extends EmptyInterceptor {
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
// 在保存实体时添加租户标识
for (int i = 0; i < propertyNames.length; i++) {
if ("tenantId".equals(propertyNames[i])) {
state[i] = TenantContext.getCurrentTenant();
return true;
}
}
return false;
}
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
// 在更新实体时添加租户标识
for (int i = 0; i < propertyNames.length; i++) {
if ("tenantId".equals(propertyNames[i])) {
currentState[i] = TenantContext.getCurrentTenant();
return true;
}
}
return false;
}
}
配置 TenantContext 上下文,保存当前租户唯一标识:
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
currentTenant.set(tenantId);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
创建解析器,从中提取当前租户唯一标识:
租户解析器,作为一个关键组件,其职责在于从传入的请求中提取出租户的唯一标识,并将该标识设置到TenantContext
中,以便应用程序的后续部分能够基于这一租户标识进行数据的隔离访问。为了实现租户标识的自动提取与设置,我们可以巧妙地利用Spring框架提供的过滤器(Filter)机制。通过定义一个自定义的过滤器,我们可以在请求到达控制器之前,就先行完成租户标识的解析与上下文设置工作。
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenantId = httpRequest.getHeader("X-TenantID");
if (tenantId != null) {
TenantContext.setCurrentTenant(tenantId);
}
try {
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
注册过滤器
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TenantConfiguration {
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilter() {
FilterRegistrationBean<TenantFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
创建实体类并配置存储库
//实体类
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String tenantId;
private String name;
private Double price;
// Getter和Setter方法
}
//存储库
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productRepository.save(product);
}
@GetMapping
public List<Product> getProducts() {
String tenantId = TenantContext.getCurrentTenant();
return productRepository.findByTenantId(tenantId);
}
}
接下来创建一个controller 来测试多租户功能:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productRepository.save(product);
}
@GetMapping
public List<Product> getProducts() {
String tenantId = TenantContext.getCurrentTenant();
return productRepository.findByTenantId(tenantId);
}
}
# 创建产品,租户ID为tenant1
curl -H "X-TenantID: tenant1" -X POST -d '{"name": "Product A", "price": 10.99}' -H "Content-Type: application/json" http://localhost:8080/products
# 创建产品,租户ID为tenant2
curl -H "X-TenantID: tenant2" -X POST -d '{"name": "Product B", "price": 15.99}' -H "Content-Type: application/json" http://localhost:8080/products
# 获取产品,租户ID为tenant1
curl -H "X-TenantID: tenant1" http://localhost:8080/products
# 获取产品,租户ID为tenant2
curl -H "X-TenantID: tenant2" http://localhost:8080/products
原文始发于微信公众号(Java技术前沿):SpringBoot实战:SpringBoot多租户配置与实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/299742.html