精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

SpringBoot多租戶系統的五種架構設計方案

開發 前端
多租戶(Multi-tenancy)是一種軟件架構模式,允許單個應用實例服務于多個客戶(租戶),同時保持租戶數據的隔離性和安全性。通過合理的多租戶設計,企業可以顯著降低運維成本、提升資源利用率,并實現更高效的服務交付。

多租戶(Multi-tenancy)是一種軟件架構模式,允許單個應用實例服務于多個客戶(租戶),同時保持租戶數據的隔離性和安全性。

通過合理的多租戶設計,企業可以顯著降低運維成本、提升資源利用率,并實現更高效的服務交付。

本文將分享SpringBoot環境下實現多租戶系統的5種架構設計方案

方案一:獨立數據庫模式

原理與特點

獨立數據庫模式為每個租戶提供完全獨立的數據庫實例,是隔離級別最高的多租戶方案。在這種模式下,租戶數據完全分離,甚至可以部署在不同的服務器上。

實現步驟

1、創建多數據源配置:為每個租戶配置獨立的數據源

@Configuration
public class MultiTenantDatabaseConfig {
    
    @Autowired
    private TenantDataSourceProperties properties;
    
    @Bean
    public 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;
    }
}

2、實現租戶感知的數據源路由

public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContextHolder.getTenantId();
    }
}

3、租戶上下文管理

public classTenantContextHolder {
    
    privatestatic final ThreadLocal<String> CONTEXT = newThreadLocal<>();
    
    publicstaticvoidsetTenantId(String tenantId) {
        CONTEXT.set(tenantId);
    }
    
    publicstaticStringgetTenantId() {
        returnCONTEXT.get();
    }
    
    publicstaticvoidclear() {
        CONTEXT.remove();
    }
}

4、添加租戶識別攔截器

@Component
publicclassTenantIdentificationInterceptorimplementsHandlerInterceptor {
    
    @Override
    publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = extractTenantId(request);
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
            returntrue;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        returnfalse;
    }
    
    @Override
    publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) {
        TenantContextHolder.clear();
    }
    
    privateStringextractTenantId(HttpServletRequest request) {
        // 從請求頭中獲取租戶ID
        String tenantId = request.getHeader("X-TenantID");
        
        // 或者從子域名提取
        if (tenantId == null) {
            String host = request.getServerName();
            if (host.contains(".")) {
                tenantId = host.split("\.")[0];
            }
        }
        
        return tenantId;
    }
}

5、配置攔截器

@Configuration
publicclassWebConfigimplementsWebMvcConfigurer {
    
    @Autowired
    privateTenantIdentificationInterceptor tenantInterceptor;
    
    @Override
    publicvoidaddInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor)
                .addPathPatterns("/api/**");
    }
}

6、實現動態租戶管理

@Entity
@Table(name = "tenant")
publicclassTenant {
    
    @Id
    privateString id;
    
    @Column(nullable = false)
    privateString name;
    
    @Column(nullable = false)
    privateString databaseUrl;
    
    @Column(nullable = false)
    privateString username;
    
    @Column(nullable = false)
    privateString password;
    
    @Column(nullable = false)
    privateString driverClassName;
    
    @Column
    privateboolean active = true;
    
    // getters and setters
}

@Repository
publicinterfaceTenantRepositoryextendsJpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

@Service
publicclassTenantManagementService {
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    @Autowired
    privateDataSource dataSource;
    
    @Autowired
    privateApplicationContext applicationContext;
    
    // 用ConcurrentHashMap存儲租戶數據源
    private final Map<String, DataSource> tenantDataSources = newConcurrentHashMap<>();
    
    @PostConstruct
    publicvoidinitializeTenants() {
        List<Tenant> activeTenants = tenantRepository.findByActive(true);
        for (Tenant tenant : activeTenants) {
            addTenant(tenant);
        }
    }
    
    publicvoidaddTenant(Tenant tenant) {
        // 創建新的數據源
        HikariDataSource dataSource = newHikariDataSource();
        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);
    }
    
    publicvoidremoveTenant(String tenantId) {
        DataSource dataSource = tenantDataSources.remove(tenantId);
        if (dataSource != null && dataSource instanceofHikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
        
        // 更新路由數據源
        updateRoutingDataSource();
        
        // 從數據庫移除租戶
        tenantRepository.deleteById(tenantId);
    }
    
    privatevoidupdateRoutingDataSource() {
        try {
            TenantAwareRoutingDataSource routingDataSource = (TenantAwareRoutingDataSource) dataSource;
            
            // 使用反射訪問AbstractRoutingDataSource的targetDataSources字段
            Field targetDataSourcesField = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
            targetDataSourcesField.setAccessible(true);
            
            Map<Object, Object> targetDataSources = newHashMap<>(tenantDataSources);
            targetDataSourcesField.set(routingDataSource, targetDataSources);
            
            // 調用afterPropertiesSet初始化數據源
            routingDataSource.afterPropertiesSet();
        } catch (Exception e) {
            thrownewRuntimeException("Failed to update routing data source", e);
        }
    }
}

7、提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantAdminController {
    
    @Autowired
    private TenantManagementService tenantService;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        returntenantService.getAllTenants();
    }
    
    @PostMapping
    publicResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.addTenant(tenant);
        returnResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    publicResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.removeTenant(tenantId);
        returnResponseEntity.noContent().build();
    }
}

優缺點分析

優點:

? 數據隔離級別最高,安全性最佳

? 租戶可以使用不同的數據庫版本或類型

? 易于實現租戶特定的數據庫優化

? 故障隔離,一個租戶的數據庫問題不影響其他租戶

? 便于獨立備份、恢復和遷移

缺點:

? 資源利用率較低,成本較高

? 運維復雜度高,需要管理多個數據庫實例

? 跨租戶查詢困難

? 每增加一個租戶需要創建新的數據庫實例

? 數據庫連接池管理復雜

適用場景

? 高要求的企業級SaaS應用

? 租戶數量相對較少但數據量大的場景

? 租戶愿意支付更高費用獲得更好隔離性的場景

方案二:共享數據庫,獨立Schema模式

原理與特點

在這種模式下,所有租戶共享同一個數據庫實例,但每個租戶擁有自己獨立的Schema(在PostgreSQL中)或數據庫(在MySQL中)。這種方式在資源共享和數據隔離之間取得了平衡。

實現步驟

1、創建租戶Schema配置

@Configuration
publicclassMultiTenantSchemaConfig {
    
    @Autowired
    privateDataSource dataSource;
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    @PostConstruct
    publicvoidinitializeSchemas() {
        for (Tenant tenant : tenantRepository.findByActive(true)) {
            createSchemaIfNotExists(tenant.getSchemaName());
        }
    }
    
    privatevoidcreateSchemaIfNotExists(String schema) {
        try (Connection connection = dataSource.getConnection()) {
            // PostgreSQL語法,MySQL使用CREATE DATABASE IF NOT EXISTS
            String sql = "CREATE SCHEMA IF NOT EXISTS " + schema;
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sql);
            }
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to create schema: " + schema, e);
        }
    }
}

2、租戶實體和存儲

@Entity
@Table(name = "tenant")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false, unique = true)
    private String schemaName;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
    Optional<Tenant> findBySchemaName(String schemaName);
}

3、配置Hibernate多租戶支持

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder) {
        
        Map<String, Object> properties = newHashMap<>();
        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配置...
        
        returnbuilder
                .dataSource(dataSource)
                .packages("com.example.entity")
                .properties(properties)
                .build();
    }
    
    @Bean
    publicMultiTenantConnectionProvidermultiTenantConnectionProvider() {
        returnnewSchemaBasedMultiTenantConnectionProvider();
    }
    
    @Bean
    publicCurrentTenantIdentifierResolvercurrentTenantIdentifierResolver() {
        returnnewTenantSchemaIdentifierResolver();
    }
}

4、實現多租戶連接提供者

public classSchemaBasedMultiTenantConnectionProvider
        implementsMultiTenantConnectionProvider {
    
    privatestaticfinallongserialVersionUID=1L;
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    public Connection getAnyConnection()throws SQLException {
        return dataSource.getConnection();
    }
    
    @Override
    publicvoidreleaseAnyConnection(Connection connection)throws SQLException {
        connection.close();
    }
    
    @Override
    public Connection getConnection(String tenantIdentifier)throws SQLException {
        finalConnectionconnection= getAnyConnection();
        try {
            // PostgreSQL語法,MySQL使用USE database_name
            connection.createStatement()
                    .execute(String.format("SET SCHEMA '%s'", tenantIdentifier));
        } catch (SQLException e) {
            thrownewHibernateException("Could not alter JDBC connection to schema ["
                    + tenantIdentifier + "]", e);
        }
        return connection;
    }
    
    @Override
    publicvoidreleaseConnection(String tenantIdentifier, Connection connection)
            throws SQLException {
        try {
            // 恢復到默認Schema
            connection.createStatement().execute("SET SCHEMA 'public'");
        } catch (SQLException e) {
            // 忽略錯誤,確保連接關閉
        }
        connection.close();
    }
    
    @Override
    publicbooleansupportsAggressiveRelease() {
        returnfalse;
    }
    
    @Override
    publicbooleanisUnwrappableAs(Class unwrapType) {
        returnfalse;
    }
    
    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        returnnull;
    }
}

5、實現租戶標識解析器

public classTenantSchemaIdentifierResolverimplementsCurrentTenantIdentifierResolver {
    
    privatestatic final StringDEFAULT_TENANT = "public";
    
    @Override
    publicStringresolveCurrentTenantIdentifier() {
        String tenantId = TenantContextHolder.getTenantId();
        return tenantId != null ? tenantId : DEFAULT_TENANT;
    }
    
    @Override
    publicbooleanvalidateExistingCurrentSessions() {
        returntrue;
    }
}

6、動態租戶管理服務

@Service
publicclassTenantSchemaManagementService {
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    @Autowired
    privateDataSource dataSource;
    
    @Autowired
    privateEntityManagerFactory entityManagerFactory;
    
    publicvoidcreateTenant(Tenant tenant) {
        // 1. 創建Schema
        createSchemaIfNotExists(tenant.getSchemaName());
        
        // 2. 保存租戶信息
        tenantRepository.save(tenant);
        
        // 3. 初始化Schema的表結構
        initializeSchema(tenant.getSchemaName());
    }
    
    publicvoiddeleteTenant(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
                .orElseThrow(() -> newRuntimeException("Tenant not found: " + tenantId));
        
        // 1. 刪除Schema
        dropSchema(tenant.getSchemaName());
        
        // 2. 刪除租戶信息
        tenantRepository.delete(tenant);
    }
    
    privatevoidcreateSchemaIfNotExists(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) {
            thrownewRuntimeException("Failed to create schema: " + schema, e);
        }
    }
    
    privatevoiddropSchema(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) {
            thrownewRuntimeException("Failed to drop schema: " + schema, e);
        }
    }
    
    privatevoidinitializeSchema(String schemaName) {
        // 設置當前租戶上下文
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(schemaName);
            
            // 使用JPA/Hibernate工具初始化Schema
            // 可以使用SchemaExport或更推薦使用Flyway/Liquibase
            Session session = entityManagerFactory.createEntityManager().unwrap(Session.class);
            session.doWork(connection -> {
                // 執行DDL語句
            });
            
        } finally {
            // 恢復之前的租戶上下文
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
}

7、租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantSchemaController {
    
    @Autowired
    private TenantSchemaManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        returntenantRepository.findAll();
    }
    
    @PostMapping
    publicResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        returnResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    publicResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        returnResponseEntity.noContent().build();
    }
}

優缺點分析

優點:

? 資源利用率高于獨立數據庫模式

? 較好的數據隔離性

? 運維復雜度低于獨立數據庫模式

? 容易實現租戶特定的表結構

? 數據庫級別的權限控制

缺點:

? 數據庫管理復雜度增加

? 可能存在Schema數量限制

? 跨租戶查詢仍然困難

? 無法為不同租戶使用不同的數據庫類型

? 所有租戶共享數據庫資源,可能出現資源爭用

適用場景

? 中型SaaS應用

? 租戶數量中等但增長較快的場景

? 需要較好數據隔離但成本敏感的應用

? PostgreSQL或MySQL等支持Schema/數據庫隔離的數據庫環境

方案三:共享數據庫,共享Schema,獨立表模式

原理與特點

在這種模式下,所有租戶共享同一個數據庫和Schema,但每個租戶有自己的表集合,通常通過表名前綴或后綴區分不同租戶的表。

實現步驟

1、實現多租戶命名策略

@Component
public class TenantTableNameStrategy extends PhysicalNamingStrategyStandardImpl {
    
    private static final long serialVersionUID = 1L;
    
    @Override
    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null && !tenantId.isEmpty()) {
            String tablePrefix = tenantId + "_";
            returnnewIdentifier(tablePrefix + name.getText(), name.isQuoted());
        }
        returnsuper.toPhysicalTableName(name, context);
    }
}

2、配置Hibernate命名策略

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {
    
    @Autowired
    private TenantTableNameStrategy tableNameStrategy;
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            DataSource dataSource) {
        
        Map<String, Object> properties = newHashMap<>();
        properties.put("hibernate.physical_naming_strategy", 
                tableNameStrategy);
        
        // 其他Hibernate配置...
        
        returnbuilder
                .dataSource(dataSource)
                .packages("com.example.entity")
                .properties(properties)
                .build();
    }
}

3、租戶實體和倉庫

@Entity
@Table(name = "tenant_info") // 避免與租戶表前綴沖突
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

4、表初始化管理器

@Component
publicclassTenantTableManager {
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @PersistenceContext
    private EntityManager entityManager;
    
    publicvoidinitializeTenantTables(String tenantId) {
        StringpreviousTenant= TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenantId);
            
            // 使用JPA/Hibernate初始化表結構
            // 在生產環境中,推薦使用Flyway或Liquibase進行更精細的控制
            Sessionsession= entityManager.unwrap(Session.class);
            session.doWork(connection -> {
                // 執行建表語句
                // 這里可以使用Hibernate的SchemaExport,但為簡化,直接使用SQL
                
                // 示例:創建用戶表
                StringcreateUserTable="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 (Statementstmt= connection.createStatement()) {
                    stmt.execute(createUserTable);
                    // 創建其他表...
                }
            });
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    publicvoiddropTenantTables(String tenantId) {
        // 獲取數據庫中所有表
        try (Connectionconnection= entityManager.unwrap(SessionImplementor.class).connection()) {
            DatabaseMetaDatametaData= connection.getMetaData();
            StringtablePrefix= tenantId + "_";
            
            try (ResultSettables= metaData.getTables(
                    connection.getCatalog(), connection.getSchema(), tablePrefix + "%", newString[]{"TABLE"})) {
                
                List<String> tablesToDrop = newArrayList<>();
                while (tables.next()) {
                    tablesToDrop.add(tables.getString("TABLE_NAME"));
                }
                
                // 刪除所有表
                for (String tableName : tablesToDrop) {
                    try (Statementstmt= connection.createStatement()) {
                        stmt.execute("DROP TABLE " + tableName);
                    }
                }
            }
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to drop tenant tables", e);
        }
    }
}

5、租戶管理服務

@Service
publicclassTenantTableManagementService {
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    @Autowired
    privateTenantTableManager tableManager;
    
    @PostConstruct
    publicvoidinitializeAllTenants() {
        for (Tenant tenant : tenantRepository.findByActive(true)) {
            tableManager.initializeTenantTables(tenant.getId());
        }
    }
    
    @Transactional
    publicvoidcreateTenant(Tenant tenant) {
        // 1. 保存租戶信息
        tenantRepository.save(tenant);
        
        // 2. 初始化租戶表
        tableManager.initializeTenantTables(tenant.getId());
    }
    
    @Transactional
    publicvoiddeleteTenant(String tenantId) {
        // 1. 刪除租戶表
        tableManager.dropTenantTables(tenantId);
        
        // 2. 刪除租戶信息
        tenantRepository.deleteById(tenantId);
    }
}

6、提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantTableController {
    
    @Autowired
    private TenantTableManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        returntenantRepository.findAll();
    }
    
    @PostMapping
    publicResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        returnResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    publicResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        returnResponseEntity.noContent().build();
    }
}

優缺點分析

優點:

? 簡單易實現,特別是對現有應用的改造

? 資源利用率高

? 跨租戶查詢相對容易實現

? 維護成本低

? 租戶間表結構可以不同

缺點:

? 數據隔離級別較低

? 隨著租戶數量增加,表數量會急劇增長

? 數據庫對象(如表、索引)數量可能達到數據庫限制

? 備份和恢復單個租戶數據較為復雜

? 可能需要處理表名長度限制問題

適用場景

? 租戶數量適中且表結構相對簡單的SaaS應用

? 需要為不同租戶提供不同表結構的場景

? 快速原型開發或MVP(最小可行產品)

? 從單租戶向多租戶過渡的系統

方案四:共享數據庫,共享Schema,共享表模式

原理與特點

這是隔離級別最低但資源效率最高的方案。所有租戶共享相同的數據庫、Schema和表,通過在每個表中添加"租戶ID"列來區分不同租戶的數據。

實現步驟

1、創建租戶感知的實體基類

@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;
    
    @PrePersist
    public void onPrePersist() {
        tenantId = TenantContextHolder.getTenantId();
    }
}

2、租戶實體和倉庫

@Entity
@Table(name = "tenants")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

3、實現租戶數據過濾器

@Component
publicclassTenantFilterInterceptorimplementsHandlerInterceptor {
    
    @Autowired
    privateEntityManager entityManager;
    
    @Override
    publicbooleanpreHandle(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);
            returntrue;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        returnfalse;
    }
    
    @Override
    publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter("tenantFilter");
    }
}

4、為實體添加過濾器注解

@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;
    
    // 其他字段和方法...
}

5、租戶管理服務

@Service
publicclassSharedTableTenantService {
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    @Autowired
    privateEntityManager entityManager;
    
    @Transactional
    publicvoidcreateTenant(Tenant tenant) {
        // 直接保存租戶信息
        tenantRepository.save(tenant);
        
        // 初始化租戶默認數據
        initializeTenantData(tenant.getId());
    }
    
    @Transactional
    publicvoiddeleteTenant(String tenantId) {
        // 刪除該租戶的所有數據
        deleteAllTenantData(tenantId);
        
        // 刪除租戶記錄
        tenantRepository.deleteById(tenantId);
    }
    
    privatevoidinitializeTenantData(String tenantId) {
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenantId);
            
            // 創建默認用戶、角色等
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    privatevoiddeleteAllTenantData(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();
        }
    }
    
    privateList<String> getTablesWithTenantIdColumn() {
        List<String> tables = newArrayList<>();
        
        try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {
            DatabaseMetaData metaData = connection.getMetaData();
            
            try (ResultSet rs = metaData.getTables(
                    connection.getCatalog(), connection.getSchema(), "%", newString[]{"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) {
            thrownewRuntimeException("Failed to get tables with tenant_id column", e);
        }
        
        return tables;
    }
}

6、租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class SharedTableTenantController {
    
    @Autowired
    private SharedTableTenantService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        returntenantRepository.findAll();
    }
    
    @PostMapping
    publicResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        returnResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    publicResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        returnResponseEntity.noContent().build();
    }
}

優缺點分析

優點:

? 資源利用率最高

? 維護成本最低

? 實現簡單,對現有單租戶系統改造容易

? 跨租戶查詢簡單

? 節省存儲空間,特別是當數據量小時

缺點:

? 數據隔離級別最低

? 安全風險較高,一個錯誤可能導致跨租戶數據泄露

? 所有租戶共享相同的表結構

? 需要在所有數據訪問層強制租戶過濾

適用場景

? 租戶數量多但每個租戶數據量小的場景

? 成本敏感的應用

? 原型驗證或MVP階段

方案五:混合租戶模式

原理與特點

混合租戶模式結合了多種隔離策略,根據租戶等級、重要性或特定需求為不同租戶提供不同級別的隔離。例如,免費用戶可能使用共享表模式,而付費企業用戶可能使用獨立數據庫模式。

實現步驟

1、租戶類型和存儲

@Entity
@Table(name = "tenants")
publicclassTenant {
    
    @Id
    privateString id;
    
    @Column(nullable = false)
    privateString name;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    privateTenantTypetype;
    
    @Column
    privateString databaseUrl;
    
    @Column
    privateString username;
    
    @Column
    privateString password;
    
    @Column
    privateString driverClassName;
    
    @Column
    privateString schemaName;
    
    @Column
    privateboolean active = true;
    
    publicenumTenantType {
        DEDICATED_DATABASE,
        DEDICATED_SCHEMA,
        DEDICATED_TABLE,
        SHARED_TABLE
    }
    
    // getters and setters
}

@Repository
publicinterfaceTenantRepositoryextendsJpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
    List<Tenant> findByType(Tenant.TenantTypetype);
}

2、創建租戶分類策略

@Component
publicclassTenantIsolationStrategy {
    
    @Autowired
    privateTenantRepository tenantRepository;
    
    private final Map<String, Tenant> tenantCache = newConcurrentHashMap<>();
    
    @PostConstruct
    publicvoidloadTenants() {
        tenantRepository.findByActive(true).forEach(tenant -> 
            tenantCache.put(tenant.getId(), tenant));
    }
    
    publicTenant.TenantTypegetIsolationTypeForTenant(String tenantId) {
        Tenant tenant = tenantCache.get(tenantId);
        if (tenant == null) {
            tenant = tenantRepository.findById(tenantId)
                    .orElseThrow(() -> newRuntimeException("Tenant not found: " + tenantId));
            tenantCache.put(tenantId, tenant);
        }
        return tenant.getType();
    }
    
    publicTenantgetTenant(String tenantId) {
        Tenant tenant = tenantCache.get(tenantId);
        if (tenant == null) {
            tenant = tenantRepository.findById(tenantId)
                    .orElseThrow(() -> newRuntimeException("Tenant not found: " + tenantId));
            tenantCache.put(tenantId, tenant);
        }
        return tenant;
    }
    
    publicvoidevictFromCache(String tenantId) {
        tenantCache.remove(tenantId);
    }
}

3、實現混合數據源路由

@Component
publicclassHybridTenantRouter {
    
    @Autowired
    privateTenantIsolationStrategy isolationStrategy;
    
    private final Map<String, DataSource> dedicatedDataSources = newConcurrentHashMap<>();
    
    @Autowired
    privateDataSource sharedDataSource;
    
    publicDataSourcegetDataSourceForTenant(String tenantId) {
        Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
        
        if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) {
            // 對于獨立數據庫的租戶,查找或創建專用數據源
            return dedicatedDataSources.computeIfAbsent(tenantId, this::createDedicatedDataSource);
        }
        
        return sharedDataSource;
    }
    
    privateDataSourcecreateDedicatedDataSource(String tenantId) {
        Tenant tenant = isolationStrategy.getTenant(tenantId);
        
        HikariDataSource dataSource = newHikariDataSource();
        dataSource.setJdbcUrl(tenant.getDatabaseUrl());
        dataSource.setUsername(tenant.getUsername());
        dataSource.setPassword(tenant.getPassword());
        dataSource.setDriverClassName(tenant.getDriverClassName());
        
        return dataSource;
    }
    
    publicvoidremoveDedicatedDataSource(String tenantId) {
        DataSource dataSource = dedicatedDataSources.remove(tenantId);
        if (dataSource instanceofHikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
    }
}

4、混合租戶路由數據源

public class HybridRoutingDataSource extends AbstractRoutingDataSource {
    
    @Autowired
    privateHybridTenantRouter tenantRouter;
    
    @Autowired
    privateTenantIsolationStrategy isolationStrategy;
    
    @Override
    protectedObject 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";
    }
    
    @Override
    protectedDataSource determineTargetDataSource() {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            returnsuper.determineTargetDataSource();
        }
        
        return tenantRouter.getDataSourceForTenant(tenantId);
    }
}

5、混合租戶攔截器

@Component
publicclassHybridTenantInterceptorimplementsHandlerInterceptor {
    
    @Autowired
    privateTenantIsolationStrategy isolationStrategy;
    
    @Autowired
    privateEntityManager entityManager;
    
    @Override
    publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = extractTenantId(request);
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
            
            Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
            
            // 根據隔離類型應用不同策略
            switch (isolationType) {
                caseDEDICATED_DATABASE:
                    // 已由數據源路由處理
                    break;
                caseDEDICATED_SCHEMA:
                    setSchema(isolationStrategy.getTenant(tenantId).getSchemaName());
                    break;
                caseDEDICATED_TABLE:
                    // 由命名策略處理
                    break;
                caseSHARED_TABLE:
                    enableTenantFilter(tenantId);
                    break;
            }
            
            returntrue;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        returnfalse;
    }
    
    @Override
    publicvoidafterCompletion(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();
    }
    
    privatevoidsetSchema(String schema) {
        try {
            entityManager.createNativeQuery("SET SCHEMA '" + schema + "'").executeUpdate();
        } catch (Exception e) {
            // 處理異常
        }
    }
    
    privatevoidenableTenantFilter(String tenantId) {
        Session session = entityManager.unwrap(Session.class);
        Filter filter = session.enableFilter("tenantFilter");
        filter.setParameter("tenantId", tenantId);
    }
    
    privatevoiddisableTenantFilter() {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter("tenantFilter");
    }
    
    privateStringextractTenantId(HttpServletRequest request) {
        // 從請求中提取租戶ID的邏輯
        return request.getHeader("X-TenantID");
    }
}

6、綜合租戶管理服務

@Service
publicclassHybridTenantManagementService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private TenantIsolationStrategy isolationStrategy;
    
    @Autowired
    private HybridTenantRouter tenantRouter;
    
    @Autowired
    private EntityManager entityManager;
    
    @Autowired
    private DataSource dataSource;
    
    // 不同隔離類型的初始化策略
    privatefinal Map<Tenant.TenantType, TenantInitializer> initializers = newHashMap<>();
    
    @PostConstruct
    publicvoidinit() {
        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);
    }
    
    @Transactional
    publicvoidcreateTenant(Tenant tenant) {
        // 1. 保存租戶基本信息
        tenantRepository.save(tenant);
        
        // 2. 根據隔離類型初始化
        TenantInitializerinitializer= initializers.get(tenant.getType());
        if (initializer != null) {
            initializer.initialize(tenant);
        }
        
        // 3. 更新緩存
        isolationStrategy.evictFromCache(tenant.getId());
    }
    
    @Transactional
    publicvoiddeleteTenant(String tenantId) {
        Tenanttenant= tenantRepository.findById(tenantId)
                .orElseThrow(() -> newRuntimeException("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);
    }
    
    // 獨立數據庫初始化
    privatevoidinitializeDedicatedDatabase(Tenant tenant) {
        // 創建數據源
        DataSourcededicatedDs= tenantRouter.getDataSourceForTenant(tenant.getId());
        
        // 初始化數據庫結構
        try (Connectionconn= dedicatedDs.getConnection()) {
            // 執行DDL腳本
            // ...
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to initialize database for tenant: " + tenant.getId(), e);
        }
    }
    
    // Schema初始化
    privatevoidinitializeDedicatedSchema(Tenant tenant) {
        try (Connectionconn= dataSource.getConnection()) {
            // 創建Schema
            try (Statementstmt= conn.createStatement()) {
                stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName());
            }
            
            // 切換到該Schema
            conn.setSchema(tenant.getSchemaName());
            
            // 創建表結構
            // ...
            
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to initialize schema for tenant: " + tenant.getId(), e);
        }
    }
    
    // 獨立表初始化
    privatevoidinitializeDedicatedTables(Tenant tenant) {
        // 設置線程上下文中的租戶ID以使用正確的表名前綴
        StringpreviousTenant= TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenant.getId());
            
            // 創建表
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    // 共享表初始化
    privatevoidinitializeSharedTables(Tenant tenant) {
        // 共享表模式下,只需插入租戶特定的初始數據
        StringpreviousTenant= TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenant.getId());
            
            // 插入初始數據
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    // 清理方法
    privatevoidcleanupDedicatedDatabase(Tenant tenant) {
        // 關閉并移除數據源
        tenantRouter.removeDedicatedDataSource(tenant.getId());
        
        // 注意:通常不會自動刪除實際的數據庫,這需要DBA手動操作
    }
    
    privatevoidcleanupDedicatedSchema(Tenant tenant) {
        try (Connectionconn= dataSource.getConnection()) {
            try (Statementstmt= conn.createStatement()) {
                stmt.execute("DROP SCHEMA IF EXISTS " + tenant.getSchemaName() + " CASCADE");
            }
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to drop schema for tenant: " + tenant.getId(), e);
        }
    }
    
    privatevoidcleanupDedicatedTables(Tenant tenant) {
        // 查找并刪除該租戶的所有表
        try (Connectionconn= dataSource.getConnection()) {
            DatabaseMetaDatametaData= conn.getMetaData();
            StringtablePrefix= tenant.getId() + "_";
            
            try (ResultSettables= metaData.getTables(
                    conn.getCatalog(), conn.getSchema(), tablePrefix + "%", newString[]{"TABLE"})) {
                
                while (tables.next()) {
                    StringtableName= tables.getString("TABLE_NAME");
                    try (Statementstmt= conn.createStatement()) {
                        stmt.execute("DROP TABLE " + tableName);
                    }
                }
            }
        } catch (SQLException e) {
            thrownewRuntimeException("Failed to drop tables for tenant: " + tenant.getId(), e);
        }
    }
    
    privatevoidcleanupSharedTables(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()
                );
    }
    
    // 租戶初始化策略接口
    @FunctionalInterface
    privateinterfaceTenantInitializer {
        voidinitialize(Tenant tenant);
    }
}

7、提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class HybridTenantController {
    
    @Autowired
    private HybridTenantManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        returntenantRepository.findAll();
    }
    
    @PostMapping
    publicResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        returnResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @PutMapping("/{tenantId}")
    publicResponseEntity<Tenant> updateTenant(
            @PathVariable String tenantId, 
            @RequestBody Tenant tenant) {
        
        tenant.setId(tenantId);
        tenantService.updateTenant(tenant);
        returnResponseEntity.ok(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    publicResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        returnResponseEntity.noContent().build();
    }
    
    @GetMapping("/types")
    publicResponseEntity<List<Tenant.TenantType>> getTenantTypes() {
        returnResponseEntity.ok(Arrays.asList(Tenant.TenantType.values()));
    }
}

優缺點分析

優點:

? 最大的靈活性,可根據租戶需求提供不同隔離級別

? 可以實現資源和成本的平衡

? 可以根據業務價值分配資源

? 適應不同客戶的安全和性能需求

缺點:

? 實現復雜度最高

? 維護和測試成本高

? 需要處理多種數據訪問模式

? 可能引入不一致的用戶體驗

? 錯誤處理更加復雜

適用場景

? 需要提供靈活定價模型的應用

? 資源需求差異大的租戶集合

方案對比

隔離模式

數據隔離級別

資源利用率

成本

復雜度

適用場景

獨立數據庫

最高

企業級應用、金融/醫療行業

獨立Schema

中型SaaS、安全要求較高的場景

獨立表

中高

中低

中小型應用、原型驗證

共享表

最高

大量小租戶、成本敏感場景

混合模式

可變

可變

中高

多層級服務、復雜業務需求

總結

多租戶架構是構建現代SaaS應用的關鍵技術,選擇多租戶模式需要平衡數據隔離、資源利用、成本和復雜度等多種因素。

通過深入理解這些架構模式及其權衡,可以根據實際情況選擇適合的多租戶架構,構建可擴展、安全且經濟高效的企業級應用。


責任編輯:武曉燕 來源: JAVA日知錄
相關推薦

2024-10-17 08:26:53

ELKmongodb方案

2024-05-28 08:17:54

2025-02-18 16:27:01

2025-10-24 14:18:55

2023-02-24 08:27:56

RabbitMQKafka架構

2025-03-03 00:45:00

2022-06-09 10:34:44

架構數據

2024-10-15 11:04:18

2023-06-07 13:50:00

SaaS多租戶系統

2009-10-19 14:39:10

2024-04-17 08:03:45

架構設計Java

2020-05-14 14:48:15

架構模式單庫

2023-07-05 08:00:52

MetrAuto系統架構

2020-09-15 07:00:00

SaaS架構架構

2025-06-09 01:22:00

2009-10-14 13:19:20

2009-09-25 16:54:02

機房UPS供電系統

2009-10-15 14:21:57

大樓綜合布線系統

2011-05-17 09:15:45

布線光纖快速以太網

2018-09-27 15:56:15

點贊
收藏

51CTO技術棧公眾號

国产精品欧美经典| 久久人人妻人人人人妻性色av| 国产精品嫩草影院精东| 自拍偷拍欧美| 欧美日韩有码| 91丝袜国产在线播放| 欧美在线精品免播放器视频| 亚洲国产天堂av| 97精品资源在线观看| 亚洲精品视频在线| 免费一区二区三区在在线视频| 在线视频你懂得| 欧美天堂亚洲电影院在线观看| 亚洲精品永久免费| 亚洲欧美一区二区三区不卡| 一根才成人网| 亚洲精品一二三四区| 免费国产一区二区| 亚洲黄色一级大片| 美腿丝袜亚洲一区| 2020久久国产精品| 欧美丰满熟妇bbbbbb| 亚洲人成网站77777在线观看| 91精品久久久久久久91蜜桃| av动漫在线观看| 97caopron在线视频| 久久久午夜精品理论片中文字幕| 亚洲一区二区三区四区视频| 免费的毛片视频| 欧美日韩福利| 日日狠狠久久偷偷四色综合免费 | 国产精品婷婷| 亚洲97av| 精品88久久久久88久久久| 亚洲色图久久久| 免费v片在线观看| 一区二区三区波多野结衣在线观看| 欧美日韩精品在线观看| 国产极品jizzhd欧美| 日韩av片在线播放| 国产精品99免费看| 欧美精品手机在线| 国产精品国产三级国产专播精品人 | 韩日午夜在线资源一区二区| 国产三级伦理片| 免费高清视频精品| 国产精品高精视频免费| 麻豆成人免费视频| 国产亚洲福利| 国产91精品不卡视频| 久久久久久久久久免费视频| 好看不卡的中文字幕| 色综合天天综合网国产成人网| 亚洲人与黑人屁股眼交| 日韩精品四区| 日韩视频―中文字幕| av片在线免费看| 四虎国产精品免费观看| 中文字幕日韩在线观看| 少妇高潮在线观看| 欧美性猛交xxxxx少妇| 九九久久国产| 欧美美女一区二区三区| 天天操狠狠操夜夜操| 在线观看亚洲精品福利片| 欧美美女一区二区在线观看| 亚洲精品中文字幕乱码无线| 免费欧美网站| 精品国产伦一区二区三区观看方式 | 日韩电影免费观看在| 免费看男男www网站入口在线| 久久久久久久久蜜桃| 日韩免费一区二区三区| 婷婷激情在线| 亚洲最新视频在线观看| 黄色一级在线视频| 精品成人免费一区二区在线播放| 欧美日韩视频在线第一区| 亚洲免费在线播放视频| 一区二区在线免费播放| 国产视频精品免费播放| 性の欲びの女javhd| 91精品久久久久久久久久不卡| 国产一区二区精品| 欧美成人精品xxx| 日本熟妇毛茸茸丰满| 综合久久伊人| 亚洲先锋影音| 日韩在线观看免费网站| 亚洲精品电影院| 国语精品一区| 国产精品狠色婷| av综合在线观看| a在线播放不卡| 亚洲人成人77777线观看| 新版中文在线官网| 色综合色综合色综合色综合色综合| 五月婷婷激情久久| 国产伦理久久久久久妇女| 亚洲一级黄色片| 久久久久久国产精品视频| 久久精品在线| 亚洲综合日韩在线| 麻豆app在线观看| 亚洲精品五月天| www黄色在线| 日韩成人久久| 中文字幕无线精品亚洲乱码一区 | 日本一区二区在线免费播放| 亚洲中文字幕在线一区| 99r国产精品| www.黄色网址.com| 成人网ww555视频免费看| 日韩欧美国产三级电影视频| 久久久久亚洲av无码a片| 韩国在线视频一区| 成人欧美一区二区三区在线湿哒哒| 神马午夜在线观看| 亚洲欧美色一区| 无码无遮挡又大又爽又黄的视频| 我和岳m愉情xxxⅹ视频| 亚洲国产伊人| 亚洲免费中文字幕| 日本网站免费观看| 国产成人av资源| 精品91一区二区三区| 日韩一级二级| 亚洲视频视频在线| 日韩精品久久久久久久酒店| 国产成人一区二区精品非洲| 一区二区成人国产精品 | 91免费国产视频| 九色视频成人自拍| 懂色av一区二区三区| 人妻换人妻a片爽麻豆| 中文字幕一区二区三区久久网站| 国产精品日韩欧美大师| 国产最新视频在线观看| 欧美视频免费在线| 第四色在线视频| 国内精品99| 国产精品sss| a级片在线免费| 亚洲精品一区二区三区福利| 欧美三根一起进三p| 国产河南妇女毛片精品久久久| 福利网在线观看| 亚洲欧洲日韩精品在线| 久久精品成人动漫| 国产男女无套免费网站| 国产精品每日更新在线播放网址| 福利在线一区二区三区| 欧美精品一区二区三区中文字幕| 国产极品精品在线观看| a中文在线播放| 欧美视频一区二区三区在线观看| 娇妻被老王脔到高潮失禁视频| 日日噜噜夜夜狠狠视频欧美人| 你懂的网址一区二区三区| 欧洲av不卡| 中文字幕欧美日韩| 中文字幕 国产| 自拍偷自拍亚洲精品播放| 91插插插影院| 国产专区一区| 免费观看成人高| 精品久久在线| 欧美片一区二区三区| 天天操天天干天天干| 欧美性xxxxx极品| 性猛交ⅹxxx富婆video| 这里只有精品9| 轻轻草成人在线| 亚洲欧美丝袜| 成人97精品毛片免费看| 九色精品免费永久在线| 三级小视频在线观看| 欧美视频专区一二在线观看| 成人无码av片在线观看| 久久国产福利国产秒拍| 999一区二区三区| 香蕉视频一区二区三区| 国产欧洲精品视频| 久久香蕉一区| 亚洲偷熟乱区亚洲香蕉av| 一区二区三区免费在线视频| 一区二区三区欧美激情| 久久久久9999| 久久er精品视频| 97视频在线免费| av亚洲在线观看| 99电影网电视剧在线观看| 波多野结衣亚洲一二三| 两个人的视频www国产精品| 天天综合在线视频| 欧美军同video69gay| 久久中文字幕无码| 欧美高清在线视频| 麻豆短视频在线观看| 日韩va亚洲va欧美va久久| 国产精品一二三在线观看| 要久久爱电视剧全集完整观看| 成人日韩av在线| 亚洲女同av| 欧美大片免费观看在线观看网站推荐| 欧美日韩影视| 欧美videossexotv100| 欧美人一级淫片a免费播放| 一区二区在线观看视频| 人妻aⅴ无码一区二区三区| 国产成人亚洲综合a∨婷婷图片| 成人免费无码av| 狠狠干成人综合网| 热这里只有精品| 欧美日韩伦理在线免费| 国产在线精品一区| 国产免费久久久| 国产真实久久| 欧美一区二区三区四区在线观看地址 | 国内av免费观看| 久久精品欧洲| 国产午夜福利在线播放| 午夜国产一区| 国产精品亚洲天堂| 成人3d精品动漫精品一二三| 精品人妻少妇嫩草av无码| 五月婷婷六月综合| 秋霞毛片久久久久久久久| 国产精品黄网站| 成人精品视频99在线观看免费| 日韩伦理在线一区| 久久久久国产精品免费| v天堂福利视频在线观看| 中文字幕不卡av| 欧美777四色影视在线| 亚洲国产精品yw在线观看| 国产成人a人亚洲精品无码| 欧美日韩成人在线| 天堂网一区二区| 一本色道亚洲精品aⅴ| 国产成人无码精品亚洲| 亚洲国产成人av好男人在线观看| 黄色a级片在线观看| 1区2区3区精品视频| 色www亚洲国产阿娇yao| 国产欧美日韩在线视频| b站大片免费直播| 91麻豆精品视频| 亚洲av无码成人精品国产| 91免费视频网| 中文字幕在线看高清电影| 激情视频亚洲| 色偷偷成人一区二区三区91| 在线观看亚洲天堂| 精品女同一区二区三区在线播放| 日本天堂在线视频| 黄黄视频在线观看| 欧美a在线观看| 成人妇女免费播放久久久| 日本欧美日韩| 国产日韩精品在线观看| 日韩毛片免费视频一级特黄| 国产精品你懂得| 成人国产精品一区二区免费麻豆 | 国产呦萝稀缺另类资源| 91小视频网站| 韩日欧美一区二区三区| 妖精视频在线观看| www.欧美精品一二区| 亚洲av无码一区二区三区人 | 91精品国产91久久综合桃花 | 日韩成人黄色av| 青春有你2免费观看完整版在线播放高清| 亚洲精品一区二区三区精华液| 丝袜+亚洲+另类+欧美+变态| 亚洲欧美日韩网| 日本欧美在线视频免费观看| 欧美成人免费播放| 欧美激情网站| 国产精品久久久精品| www.成人| 精品久久久久久一区二区里番| 伊人久久大香线蕉| 中国一区二区三区| 亚洲九九精品| 高清一区在线观看| 国产成人在线免费观看| www欧美日韩| 国产jzjzjz丝袜老师水多 | 国产精品第100页| 日本一区二区乱| 欧美福利一区二区三区| 伊人成综合网| 116极品美女午夜一级| 久久97超碰国产精品超碰| 日韩精品视频一区二区| 亚洲国产精品传媒在线观看| 国产精选第一页| 欧美日韩不卡一区| 日韩a级作爱片一二三区免费观看| 综合网中文字幕| yellow字幕网在线| 91精品视频在线免费观看| 亚洲精品进入| 日韩成人三级视频| 青青国产91久久久久久| 久久久国产精品无码| 中文字幕亚洲欧美在线不卡| 精品美女久久久久| 欧美一级艳片视频免费观看| 国产在线观看网站| 韩国三级电影久久久久久| 国产精品一区三区在线观看| 欧美日韩国产高清视频| 影音先锋亚洲精品| 中文字幕55页| 中文在线免费一区三区高中清不卡| 黄网站免费在线| 欧美一区二区视频观看视频| 91在线免费看| 国产成人自拍视频在线观看| 中文字幕在线免费看线人 | 欧美精品tushy高清| 亚洲aⅴ在线观看| 欧美激情视频给我| 国产成年精品| 亚洲av无码久久精品色欲| 亚洲不卡在线| 日韩国产精品一区二区| 伊人久久综合| 五月天婷婷在线观看视频| 国产午夜精品一区二区三区视频| 亚洲国产精品午夜在线观看| 777奇米四色成人影色区| 国产黄色片在线播放| 2019中文字幕在线| 久久电影在线| 97在线国产视频| 成人黄色777网| 精品无码人妻一区二区三区| 日韩一区和二区| 97超碰资源站在线观看| 亚洲专区在线视频| 亚洲色图欧美| 性一交一黄一片| 亚洲一区在线视频| 亚洲国产精品久久久久爰性色| 欧美成人一二三| 精品视频一区二区三区| 国产精品视频一二三四区| 国产成人av网站| 日干夜干天天干| 精品一区二区亚洲| 偷拍视频一区二区三区| 日本一区二区在线视频| 全部av―极品视觉盛宴亚洲| 久久免费手机视频| 欧美区视频在线观看| 超碰个人在线| 91国产丝袜在线放| 亚洲黄页一区| 国产人妻一区二区| 欧美日韩一卡二卡| 香蕉久久aⅴ一区二区三区| 国产精品乱码视频| 亚洲一区二区毛片| 国产午夜福利一区| 在线播放一区二区三区| 日本三级韩国三级欧美三级| 国产精品久久精品视| 国产午夜久久| 黄大色黄女片18免费| 日韩视频一区二区三区| 91禁在线看| 神马影院一区二区三区| 国产一区二区伦理| 精品成人久久久| 香港欧美日韩三级黄色一级电影网站| 欧美人成在线观看| 国产亚洲欧洲一区高清在线观看| 中文字幕精品一区二区精| 另类色图亚洲色图| 日本成人中文| 污污的视频免费| 亚洲国产精品一区二区www| 黄色的视频在线免费观看| 成人精品福利视频| 国产亚洲高清视频| 欧美另类videoxo高潮| 亚洲爱爱爱爱爱| 99亚洲伊人久久精品影院| 国产女主播av| 国产情人综合久久777777| a网站在线观看| 国产91在线播放| 婷婷精品进入| 偷拍女澡堂一区二区三区| 91精品婷婷国产综合久久性色| av日韩中文| 日韩视频在线免费播放|