SpringBoot多租户系统的5种架构设计方案
多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务于多个客户(租户),同时保持租户数据的隔离性和安全性。
通过合理的多租户设计,企业可以显著降低运维成本、提升资源利用率,并实现更高效的服务交付。
本文将分享SpringBoot环境下实现多租户系统的5种架构设计方案
方案一:独立数据库模式
原理与特点
独立数据库模式为每个租户提供完全独立的数据库实例,是隔离级别最高的多租户方案。在这种模式下,租户数据完全分离,甚至可以部署在不同的服务器上。
实现步骤
- 创建多数据源配置:为每个租户配置独立的数据源
@Configuration
public class MultiTenantDatabaseConfig {@Autowiredprivate TenantDataSourceProperties properties;@Beanpublic DataSource dataSource() {AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource();Map<Object, Object> targetDataSources = new HashMap<>();// 为每个租户创建数据源for (TenantDataSourceProperties.TenantProperties tenant : properties.getTenants()) {DataSource tenantDataSource = createDataSource(tenant);targetDataSources.put(tenant.getTenantId(), tenantDataSource);}multiTenantDataSource.setTargetDataSources(targetDataSources);return multiTenantDataSource;}private DataSource createDataSource(TenantDataSourceProperties.TenantProperties tenant) {HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl(tenant.getUrl());dataSource.setUsername(tenant.getUsername());dataSource.setPassword(tenant.getPassword());dataSource.setDriverClassName(tenant.getDriverClassName());return dataSource;}
}
- 实现租户感知的数据源路由:
public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return TenantContextHolder.getTenantId();}
}
- 租户上下文管理:
public class TenantContextHolder {private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();public static void setTenantId(String tenantId) {CONTEXT.set(tenantId);}public static String getTenantId() {return CONTEXT.get();}public static void clear() {CONTEXT.remove();}
}
- 添加租户识别拦截器:
@Component
public class TenantIdentificationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantId = extractTenantId(request);if (tenantId != null) {TenantContextHolder.setTenantId(tenantId);return true;}response.setStatus(HttpServletResponse.SC_BAD_REQUEST);return false;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContextHolder.clear();}private String extractTenantId(HttpServletRequest request) {// 从请求头中获取租户IDString tenantId = request.getHeader("X-TenantID");// 或者从子域名提取if (tenantId == null) {String host = request.getServerName();if (host.contains(".")) {tenantId = host.split("\.")[0];}}return tenantId;}
}
- 配置拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate TenantIdentificationInterceptor tenantInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tenantInterceptor).addPathPatterns("/api/**");}
}
- 实现动态租户管理:
@Entity
@Table(name = "tenant")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Column(nullable = false)private String databaseUrl;@Column(nullable = false)private String username;@Column(nullable = false)private String password;@Column(nullable = false)private String driverClassName;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}@Service
public class TenantManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate DataSource dataSource;@Autowiredprivate ApplicationContext applicationContext;// 用ConcurrentHashMap存储租户数据源private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();@PostConstructpublic void initializeTenants() {List<Tenant> activeTenants = tenantRepository.findByActive(true);for (Tenant tenant : activeTenants) {addTenant(tenant);}}public void addTenant(Tenant tenant) {// 创建新的数据源HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl(tenant.getDatabaseUrl());dataSource.setUsername(tenant.getUsername());dataSource.setPassword(tenant.getPassword());dataSource.setDriverClassName(tenant.getDriverClassName());// 存储数据源tenantDataSources.put(tenant.getId(), dataSource);// 更新路由数据源updateRoutingDataSource();// 保存租户信息到数据库tenantRepository.save(tenant);}public void removeTenant(String tenantId) {DataSource dataSource = tenantDataSources.remove(tenantId);if (dataSource != null && dataSource instanceof HikariDataSource) {((HikariDataSource) dataSource).close();}// 更新路由数据源updateRoutingDataSource();// 从数据库移除租户tenantRepository.deleteById(tenantId);}private void updateRoutingDataSource() {try {TenantAwareRoutingDataSource routingDataSource = (TenantAwareRoutingDataSource) dataSource;// 使用反射访问AbstractRoutingDataSource的targetDataSources字段Field targetDataSourcesField = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");targetDataSourcesField.setAccessible(true);Map<Object, Object> targetDataSources = new HashMap<>(tenantDataSources);targetDataSourcesField.set(routingDataSource, targetDataSources);// 调用afterPropertiesSet初始化数据源routingDataSource.afterPropertiesSet();} catch (Exception e) {throw new RuntimeException("Failed to update routing data source", e);}}
}
- 提供租户管理API:
@RestController
@RequestMapping("/admin/tenants")
public class TenantAdminController {@Autowiredprivate TenantManagementService tenantService;@GetMappingpublic List<Tenant> getAllTenants() {return tenantService.getAllTenants();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.addTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.removeTenant(tenantId);return ResponseEntity.noContent().build();}
}
优缺点分析
优点:
- 数据隔离级别最高,安全性最佳
- 租户可以使用不同的数据库版本或类型
- 易于实现租户特定的数据库优化
- 故障隔离,一个租户的数据库问题不影响其他租户
- 便于独立备份、恢复和迁移
缺点:
- 资源利用率较低,成本较高
- 运维复杂度高,需要管理多个数据库实例
- 跨租户查询困难
- 每增加一个租户需要创建新的数据库实例
- 数据库连接池管理复杂
适用场景
- 高要求的企业级SaaS应用
- 租户数量相对较少但数据量大的场景
- 租户愿意支付更高费用获得更好隔离性的场景
方案二:共享数据库,独立Schema模式
原理与特点
在这种模式下,所有租户共享同一个数据库实例,但每个租户拥有自己独立的Schema(在PostgreSQL中)或数据库(在MySQL中)。这种方式在资源共享和数据隔离之间取得了平衡。
实现步骤
- 创建租户Schema配置:
@Configuration
public class MultiTenantSchemaConfig {@Autowiredprivate DataSource dataSource;@Autowiredprivate TenantRepository tenantRepository;@PostConstructpublic void initializeSchemas() {for (Tenant tenant : tenantRepository.findByActive(true)) {createSchemaIfNotExists(tenant.getSchemaName());}}private void createSchemaIfNotExists(String schema) {try (Connection connection = dataSource.getConnection()) {// PostgreSQL语法,MySQL使用CREATE DATABASE IF NOT EXISTSString sql = "CREATE SCHEMA IF NOT EXISTS " + schema;try (Statement stmt = connection.createStatement()) {stmt.execute(sql);}} catch (SQLException e) {throw new RuntimeException("Failed to create schema: " + schema, e);}}
}
- 租户实体和存储:
@Entity
@Table(name = "tenant")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Column(nullable = false, unique = true)private String schemaName;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);Optional<Tenant> findBySchemaName(String schemaName);
}
- 配置Hibernate多租户支持:
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {@Autowiredprivate DataSource dataSource;@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {Map<String, Object> properties = new HashMap<>();properties.put(org.hibernate.cfg.Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider());properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver());// 其他Hibernate配置...return builder.dataSource(dataSource).packages("com.example.entity").properties(properties).build();}@Beanpublic MultiTenantConnectionProvider multiTenantConnectionProvider() {return new SchemaBasedMultiTenantConnectionProvider();}@Beanpublic CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {return new TenantSchemaIdentifierResolver();}
}
- 实现多租户连接提供者:
public class SchemaBasedMultiTenantConnectionProvider implements MultiTenantConnectionProvider {private static final long serialVersionUID = 1L;@Autowiredprivate DataSource dataSource;@Overridepublic Connection getAnyConnection() throws SQLException {return dataSource.getConnection();}@Overridepublic void releaseAnyConnection(Connection connection) throws SQLException {connection.close();}@Overridepublic Connection getConnection(String tenantIdentifier) throws SQLException {final Connection connection = getAnyConnection();try {// PostgreSQL语法,MySQL使用USE database_nameconnection.createStatement().execute(String.format("SET SCHEMA '%s'", tenantIdentifier));} catch (SQLException e) {throw new HibernateException("Could not alter JDBC connection to schema [" + tenantIdentifier + "]", e);}return connection;}@Overridepublic void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {try {// 恢复到默认Schemaconnection.createStatement().execute("SET SCHEMA 'public'");} catch (SQLException e) {// 忽略错误,确保连接关闭}connection.close();}@Overridepublic boolean supportsAggressiveRelease() {return false;}@Overridepublic boolean isUnwrappableAs(Class unwrapType) {return false;}@Overridepublic <T> T unwrap(Class<T> unwrapType) {return null;}
}
- 实现租户标识解析器:
public class TenantSchemaIdentifierResolver implements CurrentTenantIdentifierResolver {private static final String DEFAULT_TENANT = "public";@Overridepublic String resolveCurrentTenantIdentifier() {String tenantId = TenantContextHolder.getTenantId();return tenantId != null ? tenantId : DEFAULT_TENANT;}@Overridepublic boolean validateExistingCurrentSessions() {return true;}
}
- 动态租户管理服务:
@Service
public class TenantSchemaManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate DataSource dataSource;@Autowiredprivate EntityManagerFactory entityManagerFactory;public void createTenant(Tenant tenant) {// 1. 创建SchemacreateSchemaIfNotExists(tenant.getSchemaName());// 2. 保存租户信息tenantRepository.save(tenant);// 3. 初始化Schema的表结构initializeSchema(tenant.getSchemaName());}public void deleteTenant(String tenantId) {Tenant tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));// 1. 删除SchemadropSchema(tenant.getSchemaName());// 2. 删除租户信息tenantRepository.delete(tenant);}private void createSchemaIfNotExists(String schema) {try (Connection connection = dataSource.getConnection()) {String sql = "CREATE SCHEMA IF NOT EXISTS " + schema;try (Statement stmt = connection.createStatement()) {stmt.execute(sql);}} catch (SQLException e) {throw new RuntimeException("Failed to create schema: " + schema, e);}}private void dropSchema(String schema) {try (Connection connection = dataSource.getConnection()) {String sql = "DROP SCHEMA IF EXISTS " + schema + " CASCADE";try (Statement stmt = connection.createStatement()) {stmt.execute(sql);}} catch (SQLException e) {throw new RuntimeException("Failed to drop schema: " + schema, e);}}private void initializeSchema(String schemaName) {// 设置当前租户上下文String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(schemaName);// 使用JPA/Hibernate工具初始化Schema// 可以使用SchemaExport或更推荐使用Flyway/LiquibaseSession session = entityManagerFactory.createEntityManager().unwrap(Session.class);session.doWork(connection -> {// 执行DDL语句});} finally {// 恢复之前的租户上下文if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}
}
- 租户管理API:
@RestController
@RequestMapping("/admin/tenants")
public class TenantSchemaController {@Autowiredprivate TenantSchemaManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}
优缺点分析
优点:
- 资源利用率高于独立数据库模式
- 较好的数据隔离性
- 运维复杂度低于独立数据库模式
- 容易实现租户特定的表结构
- 数据库级别的权限控制
缺点:
- 数据库管理复杂度增加
- 可能存在Schema数量限制
- 跨租户查询仍然困难
- 无法为不同租户使用不同的数据库类型
- 所有租户共享数据库资源,可能出现资源争用
适用场景
- 中型SaaS应用
- 租户数量中等但增长较快的场景
- 需要较好数据隔离但成本敏感的应用
- PostgreSQL或MySQL等支持Schema/数据库隔离的数据库环境
方案三:共享数据库,共享Schema,独立表模式
原理与特点
在这种模式下,所有租户共享同一个数据库和Schema,但每个租户有自己的表集合,通常通过表名前缀或后缀区分不同租户的表。
实现步骤
- 实现多租户命名策略:
@Component
public class TenantTableNameStrategy extends PhysicalNamingStrategyStandardImpl {private static final long serialVersionUID = 1L;@Overridepublic Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {String tenantId = TenantContextHolder.getTenantId();if (tenantId != null && !tenantId.isEmpty()) {String tablePrefix = tenantId + "_";return new Identifier(tablePrefix + name.getText(), name.isQuoted());}return super.toPhysicalTableName(name, context);}
}
- 配置Hibernate命名策略:
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {@Autowiredprivate TenantTableNameStrategy tableNameStrategy;@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,DataSource dataSource) {Map<String, Object> properties = new HashMap<>();properties.put("hibernate.physical_naming_strategy", tableNameStrategy);// 其他Hibernate配置...return builder.dataSource(dataSource).packages("com.example.entity").properties(properties).build();}
}
- 租户实体和仓库:
@Entity
@Table(name = "tenant_info") // 避免与租户表前缀冲突
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}
- 表初始化管理器:
@Component
public class TenantTableManager {@Autowiredprivate EntityManagerFactory entityManagerFactory;@Autowiredprivate TenantRepository tenantRepository;@PersistenceContextprivate EntityManager entityManager;public void initializeTenantTables(String tenantId) {String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(tenantId);// 使用JPA/Hibernate初始化表结构// 在生产环境中,推荐使用Flyway或Liquibase进行更精细的控制Session session = entityManager.unwrap(Session.class);session.doWork(connection -> {// 执行建表语句// 这里可以使用Hibernate的SchemaExport,但为简化,直接使用SQL// 示例:创建用户表String createUserTable = "CREATE TABLE IF NOT EXISTS " + tenantId + "_users (" +"id BIGINT NOT NULL AUTO_INCREMENT, " +"username VARCHAR(255) NOT NULL, " +"email VARCHAR(255) NOT NULL, " +"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +"PRIMARY KEY (id)" +")";try (Statement stmt = connection.createStatement()) {stmt.execute(createUserTable);// 创建其他表...}});} finally {if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}public void dropTenantTables(String tenantId) {// 获取数据库中所有表try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {DatabaseMetaData metaData = connection.getMetaData();String tablePrefix = tenantId + "_";try (ResultSet tables = metaData.getTables(connection.getCatalog(), connection.getSchema(), tablePrefix + "%", new String[]{"TABLE"})) {List<String> tablesToDrop = new ArrayList<>();while (tables.next()) {tablesToDrop.add(tables.getString("TABLE_NAME"));}// 删除所有表for (String tableName : tablesToDrop) {try (Statement stmt = connection.createStatement()) {stmt.execute("DROP TABLE " + tableName);}}}} catch (SQLException e) {throw new RuntimeException("Failed to drop tenant tables", e);}}
}
- 租户管理服务:
@Service
public class TenantTableManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate TenantTableManager tableManager;@PostConstructpublic void initializeAllTenants() {for (Tenant tenant : tenantRepository.findByActive(true)) {tableManager.initializeTenantTables(tenant.getId());}}@Transactionalpublic void createTenant(Tenant tenant) {// 1. 保存租户信息tenantRepository.save(tenant);// 2. 初始化租户表tableManager.initializeTenantTables(tenant.getId());}@Transactionalpublic void deleteTenant(String tenantId) {// 1. 删除租户表tableManager.dropTenantTables(tenantId);// 2. 删除租户信息tenantRepository.deleteById(tenantId);}
}
- 提供租户管理API:
@RestController
@RequestMapping("/admin/tenants")
public class TenantTableController {@Autowiredprivate TenantTableManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}
优缺点分析
优点:
- 简单易实现,特别是对现有应用的改造
- 资源利用率高
- 跨租户查询相对容易实现
- 维护成本低
- 租户间表结构可以不同
缺点:
- 数据隔离级别较低
- 随着租户数量增加,表数量会急剧增长
- 数据库对象(如表、索引)数量可能达到数据库限制
- 备份和恢复单个租户数据较为复杂
- 可能需要处理表名长度限制问题
适用场景
- 租户数量适中且表结构相对简单的SaaS应用
- 需要为不同租户提供不同表结构的场景
- 快速原型开发或MVP(最小可行产品)
- 从单租户向多租户过渡的系统
方案四:共享数据库,共享Schema,共享表模式
原理与特点
这是隔离级别最低但资源效率最高的方案。所有租户共享相同的数据库、Schema和表,通过在每个表中添加"租户ID"列来区分不同租户的数据。
实现步骤
- 创建租户感知的实体基类:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class TenantAwareEntity {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "tenant_id", nullable = false)private String tenantId;@CreatedDate@Column(name = "created_at", updatable = false)private LocalDateTime createdAt;@LastModifiedDate@Column(name = "updated_at")private LocalDateTime updatedAt;@PrePersistpublic void onPrePersist() {tenantId = TenantContextHolder.getTenantId();}
}
- 租户实体和仓库:
@Entity
@Table(name = "tenants")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}
- 实现租户数据过滤器:
@Component
public class TenantFilterInterceptor implements HandlerInterceptor {@Autowiredprivate EntityManager entityManager;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantId = TenantContextHolder.getTenantId();if (tenantId != null) {// 设置Hibernate过滤器Session session = entityManager.unwrap(Session.class);Filter filter = session.enableFilter("tenantFilter");filter.setParameter("tenantId", tenantId);return true;}response.setStatus(HttpServletResponse.SC_BAD_REQUEST);return false;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {Session session = entityManager.unwrap(Session.class);session.disableFilter("tenantFilter");}
}
- 为实体添加过滤器注解:
@Entity
@Table(name = "users")
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "string")
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User extends TenantAwareEntity {@Column(name = "username", nullable = false)private String username;@Column(name = "email", nullable = false)private String email;// 其他字段和方法...
}
- 租户管理服务:
@Service
public class SharedTableTenantService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate EntityManager entityManager;@Transactionalpublic void createTenant(Tenant tenant) {// 直接保存租户信息tenantRepository.save(tenant);// 初始化租户默认数据initializeTenantData(tenant.getId());}@Transactionalpublic void deleteTenant(String tenantId) {// 删除该租户的所有数据deleteAllTenantData(tenantId);// 删除租户记录tenantRepository.deleteById(tenantId);}private void initializeTenantData(String tenantId) {String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(tenantId);// 创建默认用户、角色等// ...} finally {if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}private void deleteAllTenantData(String tenantId) {// 获取所有带有tenant_id列的表List<String> tables = getTablesWithTenantIdColumn();// 从每个表中删除该租户的数据for (String table : tables) {entityManager.createNativeQuery("DELETE FROM " + table + " WHERE tenant_id = :tenantId").setParameter("tenantId", tenantId).executeUpdate();}}private List<String> getTablesWithTenantIdColumn() {List<String> tables = new ArrayList<>();try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {DatabaseMetaData metaData = connection.getMetaData();try (ResultSet rs = metaData.getTables(connection.getCatalog(), connection.getSchema(), "%", new String[]{"TABLE"})) {while (rs.next()) {String tableName = rs.getString("TABLE_NAME");// 检查表是否有tenant_id列try (ResultSet columns = metaData.getColumns(connection.getCatalog(), connection.getSchema(), tableName, "tenant_id")) {if (columns.next()) {tables.add(tableName);}}}}} catch (SQLException e) {throw new RuntimeException("Failed to get tables with tenant_id column", e);}return tables;}
}
- 租户管理API:
@RestController
@RequestMapping("/admin/tenants")
public class SharedTableTenantController {@Autowiredprivate SharedTableTenantService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}
优缺点分析
优点:
- 资源利用率最高
- 维护成本最低
- 实现简单,对现有单租户系统改造容易
- 跨租户查询简单
- 节省存储空间,特别是当数据量小时
缺点:
- 数据隔离级别最低
- 安全风险较高,一个错误可能导致跨租户数据泄露
- 所有租户共享相同的表结构
- 需要在所有数据访问层强制租户过滤
适用场景
- 租户数量多但每个租户数据量小的场景
- 成本敏感的应用
- 原型验证或MVP阶段
方案五:混合租户模式
原理与特点
混合租户模式结合了多种隔离策略,根据租户等级、重要性或特定需求为不同租户提供不同级别的隔离。例如,免费用户可能使用共享表模式,而付费企业用户可能使用独立数据库模式。
实现步骤
- 租户类型和存储:
@Entity
@Table(name = "tenants")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Enumerated(EnumType.STRING)@Column(nullable = false)private TenantType type;@Columnprivate String databaseUrl;@Columnprivate String username;@Columnprivate String password;@Columnprivate String driverClassName;@Columnprivate String schemaName;@Columnprivate boolean active = true;public enum TenantType {DEDICATED_DATABASE,DEDICATED_SCHEMA,DEDICATED_TABLE,SHARED_TABLE}// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);List<Tenant> findByType(Tenant.TenantType type);
}
- 创建租户分类策略:
@Component
public class TenantIsolationStrategy {@Autowiredprivate TenantRepository tenantRepository;private final Map<String, Tenant> tenantCache = new ConcurrentHashMap<>();@PostConstructpublic void loadTenants() {tenantRepository.findByActive(true).forEach(tenant -> tenantCache.put(tenant.getId(), tenant));}public Tenant.TenantType getIsolationTypeForTenant(String tenantId) {Tenant tenant = tenantCache.get(tenantId);if (tenant == null) {tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));tenantCache.put(tenantId, tenant);}return tenant.getType();}public Tenant getTenant(String tenantId) {Tenant tenant = tenantCache.get(tenantId);if (tenant == null) {tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));tenantCache.put(tenantId, tenant);}return tenant;}public void evictFromCache(String tenantId) {tenantCache.remove(tenantId);}
}
- 实现混合数据源路由:
@Component
public class HybridTenantRouter {@Autowiredprivate TenantIsolationStrategy isolationStrategy;private final Map<String, DataSource> dedicatedDataSources = new ConcurrentHashMap<>();@Autowiredprivate DataSource sharedDataSource;public DataSource getDataSourceForTenant(String tenantId) {Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) {// 对于独立数据库的租户,查找或创建专用数据源return dedicatedDataSources.computeIfAbsent(tenantId, this::createDedicatedDataSource);}return sharedDataSource;}private DataSource createDedicatedDataSource(String tenantId) {Tenant tenant = isolationStrategy.getTenant(tenantId);HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl(tenant.getDatabaseUrl());dataSource.setUsername(tenant.getUsername());dataSource.setPassword(tenant.getPassword());dataSource.setDriverClassName(tenant.getDriverClassName());return dataSource;}public void removeDedicatedDataSource(String tenantId) {DataSource dataSource = dedicatedDataSources.remove(tenantId);if (dataSource instanceof HikariDataSource) {((HikariDataSource) dataSource).close();}}
}
- 混合租户路由数据源:
public class HybridRoutingDataSource extends AbstractRoutingDataSource {@Autowiredprivate HybridTenantRouter tenantRouter;@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Overrideprotected Object determineCurrentLookupKey() {String tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return "default";}Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) {return tenantId;}return "shared";}@Overrideprotected DataSource determineTargetDataSource() {String tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return super.determineTargetDataSource();}return tenantRouter.getDataSourceForTenant(tenantId);}
}
- 混合租户拦截器:
@Component
public class HybridTenantInterceptor implements HandlerInterceptor {@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Autowiredprivate EntityManager entityManager;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantId = extractTenantId(request);if (tenantId != null) {TenantContextHolder.setTenantId(tenantId);Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);// 根据隔离类型应用不同策略switch (isolationType) {case DEDICATED_DATABASE:// 已由数据源路由处理break;case DEDICATED_SCHEMA:setSchema(isolationStrategy.getTenant(tenantId).getSchemaName());break;case DEDICATED_TABLE:// 由命名策略处理break;case SHARED_TABLE:enableTenantFilter(tenantId);break;}return true;}response.setStatus(HttpServletResponse.SC_BAD_REQUEST);return false;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {String tenantId = TenantContextHolder.getTenantId();if (tenantId != null) {Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);if (isolationType == Tenant.TenantType.SHARED_TABLE) {disableTenantFilter();}}TenantContextHolder.clear();}private void setSchema(String schema) {try {entityManager.createNativeQuery("SET SCHEMA '" + schema + "'").executeUpdate();} catch (Exception e) {// 处理异常}}private void enableTenantFilter(String tenantId) {Session session = entityManager.unwrap(Session.class);Filter filter = session.enableFilter("tenantFilter");filter.setParameter("tenantId", tenantId);}private void disableTenantFilter() {Session session = entityManager.unwrap(Session.class);session.disableFilter("tenantFilter");}private String extractTenantId(HttpServletRequest request) {// 从请求中提取租户ID的逻辑return request.getHeader("X-TenantID");}
}
- 综合租户管理服务:
@Service
public class HybridTenantManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Autowiredprivate HybridTenantRouter tenantRouter;@Autowiredprivate EntityManager entityManager;@Autowiredprivate DataSource dataSource;// 不同隔离类型的初始化策略private final Map<Tenant.TenantType, TenantInitializer> initializers = new HashMap<>();@PostConstructpublic void init() {initializers.put(Tenant.TenantType.DEDICATED_DATABASE, this::initializeDedicatedDatabase);initializers.put(Tenant.TenantType.DEDICATED_SCHEMA, this::initializeDedicatedSchema);initializers.put(Tenant.TenantType.DEDICATED_TABLE, this::initializeDedicatedTables);initializers.put(Tenant.TenantType.SHARED_TABLE, this::initializeSharedTables);}@Transactionalpublic void createTenant(Tenant tenant) {// 1. 保存租户基本信息tenantRepository.save(tenant);// 2. 根据隔离类型初始化TenantInitializer initializer = initializers.get(tenant.getType());if (initializer != null) {initializer.initialize(tenant);}// 3. 更新缓存isolationStrategy.evictFromCache(tenant.getId());}@Transactionalpublic void deleteTenant(String tenantId) {Tenant tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));// 1. 根据隔离类型清理资源switch (tenant.getType()) {case DEDICATED_DATABASE:cleanupDedicatedDatabase(tenant);break;case DEDICATED_SCHEMA:cleanupDedicatedSchema(tenant);break;case DEDICATED_TABLE:cleanupDedicatedTables(tenant);break;case SHARED_TABLE:cleanupSharedTables(tenant);break;}// 2. 删除租户信息tenantRepository.delete(tenant);// 3. 更新缓存isolationStrategy.evictFromCache(tenantId);}// 独立数据库初始化private void initializeDedicatedDatabase(Tenant tenant) {// 创建数据源DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId());// 初始化数据库结构try (Connection conn = dedicatedDs.getConnection()) {// 执行DDL脚本// ...} catch (SQLException e) {throw new RuntimeException("Failed to initialize database for tenant: " + tenant.getId(), e);}}// Schema初始化private void initializeDedicatedSchema(Tenant tenant) {try (Connection conn = dataSource.getConnection()) {// 创建Schematry (Statement stmt = conn.createStatement()) {stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName());}// 切换到该Schemaconn.setSchema(tenant.getSchemaName());// 创建表结构// ...} catch (SQLException e) {throw new RuntimeException("Failed to initialize schema for tenant: " + tenant.getId(), e);}}// 独立表初始化private void initializeDedicatedTables(Tenant tenant) {// 设置线程上下文中的租户ID以使用正确的表名前缀String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(tenant.getId());// 创建表// ...} finally {if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}// 共享表初始化private void initializeSharedTables(Tenant tenant) {// 共享表模式下,只需插入租户特定的初始数据String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(tenant.getId());// 插入初始数据// ...} finally {if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}// 清理方法private void cleanupDedicatedDatabase(Tenant tenant) {// 关闭并移除数据源tenantRouter.removeDedicatedDataSource(tenant.getId());// 注意:通常不会自动删除实际的数据库,这需要DBA手动操作}private void cleanupDedicatedSchema(Tenant tenant) {try (Connection conn = dataSource.getConnection()) {try (Statement stmt = conn.createStatement()) {stmt.execute("DROP SCHEMA IF EXISTS " + tenant.getSchemaName() + " CASCADE");}} catch (SQLException e) {throw new RuntimeException("Failed to drop schema for tenant: " + tenant.getId(), e);}}private void cleanupDedicatedTables(Tenant tenant) {// 查找并删除该租户的所有表try (Connection conn = dataSource.getConnection()) {DatabaseMetaData metaData = conn.getMetaData();String tablePrefix = tenant.getId() + "_";try (ResultSet tables = metaData.getTables(conn.getCatalog(), conn.getSchema(), tablePrefix + "%", new String[]{"TABLE"})) {while (tables.next()) {String tableName = tables.getString("TABLE_NAME");try (Statement stmt = conn.createStatement()) {stmt.execute("DROP TABLE " + tableName);}}}} catch (SQLException e) {throw new RuntimeException("Failed to drop tables for tenant: " + tenant.getId(), e);}}private void cleanupSharedTables(Tenant tenant) {// 从所有带有tenant_id列的表中删除该租户的数据entityManager.createNativeQuery("SELECT table_name FROM information_schema.columns " +"WHERE column_name = 'tenant_id'").getResultList().forEach(tableName -> entityManager.createNativeQuery("DELETE FROM " + tableName + " WHERE tenant_id = :tenantId").setParameter("tenantId", tenant.getId()).executeUpdate());}// 租户初始化策略接口@FunctionalInterfaceprivate interface TenantInitializer {void initialize(Tenant tenant);}
}
- 提供租户管理API:
@RestController
@RequestMapping("/admin/tenants")
public class HybridTenantController {@Autowiredprivate HybridTenantManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@PutMapping("/{tenantId}")public ResponseEntity<Tenant> updateTenant(@PathVariable String tenantId, @RequestBody Tenant tenant) {tenant.setId(tenantId);tenantService.updateTenant(tenant);return ResponseEntity.ok(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}@GetMapping("/types")public ResponseEntity<List<Tenant.TenantType>> getTenantTypes() {return ResponseEntity.ok(Arrays.asList(Tenant.TenantType.values()));}
}
优缺点分析
优点:
- 最大的灵活性,可根据租户需求提供不同隔离级别
- 可以实现资源和成本的平衡
- 可以根据业务价值分配资源
- 适应不同客户的安全和性能需求
缺点:
- 实现复杂度最高
- 维护和测试成本高
- 需要处理多种数据访问模式
- 可能引入不一致的用户体验
- 错误处理更加复杂
适用场景
- 需要提供灵活定价模型的应用
- 资源需求差异大的租户集合
方案对比
隔离模式 | 数据隔离级别 | 资源利用率 | 成本 | 复杂度 | 适用场景 |
---|---|---|---|---|---|
独立数据库 | 最高 | 低 | 高 | 中 | 企业级应用、金融/医疗行业 |
独立Schema | 高 | 中 | 中 | 中 | 中型SaaS、安全要求较高的场景 |
独立表 | 中 | 中高 | 中低 | 低 | 中小型应用、原型验证 |
共享表 | 低 | 最高 | 低 | 低 | 大量小租户、成本敏感场景 |
混合模式 | 可变 | 可变 | 中高 | 高 | 多层级服务、复杂业务需求 |
总结
多租户架构是构建现代SaaS应用的关键技术,选择多租户模式需要平衡数据隔离、资源利用、成本和复杂度等多种因素。
通过深入理解这些架构模式及其权衡,可以根据实际情况选择适合的多租户架构,构建可扩展、安全且经济高效的企业级应用。
相关文章:
SpringBoot多租户系统的5种架构设计方案
多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务于多个客户(租户),同时保持租户数据的隔离性和安全性。 通过合理的多租户设计,企业可以显著降低运维成本、提升资源利用率,并实现更高效的服务交付。 本文将分享S…...

数据分析实战1(Excel制作报表)
Excel数据链接:【课程4.0】第2章_Excel.zip - 飞书云文档 1、拿到数据第一步 备份数据 ctrlshiftL:筛选 相关快捷键:(alt:自动求和、ctrlshift5:转换为%) 2、环比、同比 环比(本…...

本地部署大模型llm+RAG向量检索问答系统 deepseek chatgpt
项目视频讲解: 本地部署大模型llm+RAG向量检索问答系统 deepseek chatgpt_哔哩哔哩_bilibili 运行结果:...
设备健康管理的战略升维:用预测性维护重构企业竞争力
第一章 传统维护的沉默成本:被低估的利润黑洞 当轴承振动值突破安全阈值时,制造企业损失的远非维修费用。某重型装备制造厂的案例揭示了典型多米诺效应:传动系统突发故障导致36小时停产,触发订单违约金(合约金额的9%&…...
Redis事务详解:原理、使用与注意事项
文章目录 Redis事务详解:原理、使用与注意事项什么是Redis事务Redis事务的基本使用基本事务示例事务执行过程 Redis事务的错误处理1. 入队错误2. 执行错误 WATCH命令:乐观锁实现Redis事务的局限性事务的最佳实践Lua脚本总结 Redis事务详解:原…...
提升 GitHub Stats 的 6 个关键策略
哈哈,GitHub 的 “B-” 评级 其实是个玄学问题,但确实有一些 快速提升的技巧!你的数据看起来 提交数(147)和 PR(9)不算少,但 Stars(21)和贡献项目数ÿ…...
CSS Animation 详解
CSS Animation 允许元素平滑地从一个样式状态过渡到另一个样式状态。通过设置关键帧(keyframes),可以控制动画序列中的中间步骤。 一、核心概念 1.关键帧(Keyframes) 使用 keyframes 规则定义动画序列通过百分比或 …...

LabVIEW 中内存释放相关问题
在LabVIEW 编程领域,内存管理是一个关键且复杂的议题。我们常常关注 LabVIEW 如何将内存释放回操作系统(OS),以及是否有方法确保在特定数据结构(如队列、变体属性、动态数据引用 DVR 等)销毁、删除或清空后…...
【HarmonyOS 5】鸿蒙中的UIAbility详解(三)
【HarmonyOS 5】鸿蒙中的UIAbility详解(三) 一、前言 本文是鸿蒙中的UIAbility详解系列的最终章。主要针对UIAbility的冷启动和热启动,对于want数据的处理。UIAbility的备份恢复,UIAbility的接续等高级功能的概念和使用讲解。 …...

基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
在使用 OpenAI、Claude、Gemini 等大语言模型 API 构建对话系统时,开发者普遍面临成本不断上升的挑战。无论是基于检索增强生成(RAG)的应用还是独立的对话系统,这些系统都需要维护对话历史以确保上下文的连贯性,类似于…...
vue-11(命名路由和命名视图)
命名路由和命名视图 命名路由和命名视图提供了组织和导航 Vue.js 应用程序的强大方法,尤其是在它们的复杂性增加时。它们提供了一种语义更合理、可维护的路由方法,使您的代码更易于理解和修改。命名路由允许您按名称引用路由,而不是依赖 URL…...
(附代码)自定义 LangChain 文档分割器,深入探索 LangChain 文档分割策略与应用
自定义文档分割器 在 LangChain 中,如果内置的文档分割器均没办法完成需求,还可以根据特定的需求实现自定义文档分割器(一般极少),实现的方法也非常简单,继承文本分割器基类 TextSplitter,在构造…...

Python打卡训练营Day42
DAY 42 Grad-CAM与Hook函数 知识点回顾 回调函数lambda函数hook函数的模块钩子和张量钩子Grad-CAM的示例 作业:理解下今天的代码即可 import torch import torch.nn as nn import torch.nn.functional as F import torchvision import torchvision.transforms as tr…...

基于微信小程序的scratch学习系统
博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言࿰…...
MATLAB实战:机器学习分类回归示例
以下是一个使用MATLAB的Statistics and Machine Learning Toolbox实现分类和回归任务的完整示例代码。代码包含鸢尾花分类、手写数字分类和汽车数据回归任务,并评估模型性能。 %% 加载内置数据集 % 鸢尾花数据集(分类) load fisheriris; X_i…...
动态库导出符号与extern “C“
1. windows下动态库导出符号 根据C/C语法规则,函数声明中的修饰符(如__declspec(dllexport))可以放在返回类型之前或返回类型之后、函数名之前。这两种方式在功能上是等价的,编译器会以相同的方式处理。 __declspec(dllexport) …...
小知识:STM32 printf 重定向(串口输出)--让数据 “开口说话” 的关键技巧
引言 在 C 语言开发中,printf函数是我们调试程序、输出数据的得力助手,它能将格式化的数据输出到标准输出设备(通常是屏幕)。然而,在嵌入式领域,STM32 单片机并没有默认的显示设备,要让printf函…...
`docker commit` 和 `docker save`区别
理解 docker commit 和 docker save 之间的区别对于正确管理 Docker 镜像非常重要。让我们详细解释一下这两个命令的作用及其区别。 1. docker commit 作用: docker commit roop-builder roop:v1 命令的作用是基于一个正在运行的容器 roop-builder 创建一个新的镜…...

【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“
欢迎来到一整颗红豆的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡 本文由一整颗红豆原创✍️,感谢支持❤️!请尊重原创…...

SCSAI平台面向对象建模技术的设计与实现
一、核心设计思想 SCSAI平台的核心目标是通过元建模(Meta-Modeling)技术实现面向对象建模的零编码化。其核心思想为: 自反性设计:定义ObjectClassInfo (OCI)为元类(Meta-Class),所有对象类均为…...

pikachu通关教程-CSRF
CSRF(get) 用bp进行抓包 选择action value值的修改 点击test in browser copy然后放在bp代理的浏览器上,会出现一个提交按钮,这时候点击之后信息就被修改了。 CSRF(post) 请求的方式不同,其他都是一样 CSRF Token 存在cookie 首先要先下载一…...

智能体觉醒:AI开始自己“动手”了-自主进化开启任务革命时代
1. 智能体:AI从“工具”到“伙伴”的关键跃迁 1.1 什么是智能体? 智能体(Agent)是AI的“进化版”——它不再局限于生成文字或图像,而是能像人类一样“规划任务”“调用工具”甚至“协同合作”。例如,一个…...
Python爬虫实战:研究Aiohttp库相关技术
1. 引言 1.1 研究背景与意义 随着互联网的快速发展,网络上的数据量呈爆炸式增长。爬虫作为一种自动获取网络信息的工具,在数据挖掘、信息检索、舆情分析等领域有着广泛的应用。传统的同步爬虫在面对大量 URL 时效率低下,无法充分利用现代计算机的多核资源和网络带宽。而异…...

【C++指南】C++ list容器完全解读(二):list模拟实现,底层架构揭秘
. 💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《C指南》 期待您的关注 文章目录 引言一、链表节点设计:双向链表的基石1.1 节点类的实现 二、list框架与核心成员函…...

[神经网络]使用olivettiface数据集进行训练并优化,观察对比loss结果
结合归一化和正则化来优化网络模型结构,观察对比loss结果 搭建的神经网络,使用olivettiface数据集进行训练,结合归一化和正则化来优化网络模型结构,观察对比loss结果 from sklearn.datasets import fetch_olivetti_faces #倒入数…...
小明的Java面试奇遇之智能家装平台架构设计与JVM调优实战
一、文章标题 小明的Java面试奇遇之智能家装平台架构设计与JVM调优实战 二、文章标签 Java面试, 智能家装, 微服务架构, 高并发设计, JVM调优, SpringCloud, 消息队列, 分布式缓存, 架构设计, 面试技巧 三、文章概述 本文模拟了程序员小明应聘智能家装平台后端架构师的5轮…...
n8n:技术团队的智能工作流自动化助手
在当前数字化时代,自动化已经成为提高效率和减轻人工工作负担的一大推动力。今天,我们要为大家介绍一款极具潜力的开源项目——n8n,它不仅拥有广泛的应用场景,还具备内置AI功能,能够完全满足技术团队的高效工作需求。n8n的出现,为技术团队提供了自由编程与快速自动化构建…...
Flink 核心机制与源码剖析系列
Flink 核心机制与源码剖析系列 目录 第一篇:Flink 状态管理原理与源码深度剖析第二篇:水位线、事件时间与定时器源码全流程第三篇:Flink CEP 模式建模与高效事件匹配机制 第一篇:Flink 状态管理原理与源码深度剖析 1. 背景与意…...

华院计算出席信创论坛,分享AI教育创新实践并与燧原科技共同推出教育一体机
5月21日,信创论坛于上海漕河泾会议中心举办。本次论坛以“聚力融合,繁荣生态”为主题,话题聚焦工业制造、交通运输、金融、教育、医疗等领域。华院计算技术(上海)股份有限公司(以下简称“华院计算”&#x…...

华为OD机试真题——会议接待 /代表团坐车(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
2025 A卷 200分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析; 并提供Java、python、JavaScript、C++、C语言、GO六种语言的最佳实现方式! 本文收录于专栏:《2025华为OD真题目录+全流程解析/备考攻略/经验分享》 华为OD机试真题《会议…...