精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

精品国产户外野外| 在线一区免费观看| 欧美日韩高清一区二区三区| 色视频一区二区三区| 日本视频免费观看| 亚洲丁香日韩| 欧美少妇xxx| 手机福利在线视频| 国产毛片在线视频| 亚洲一级电影| 亚洲欧美国产日韩天堂区| 欧美日韩亚洲自拍| 亚洲综合影视| 久久夜色精品国产欧美乱极品| 国产精品成人v| 日本精品人妻无码77777| 久久久久久久性潮| 亚洲国产你懂的| 人偷久久久久久久偷女厕| 日韩精品在线免费视频| 成人羞羞视频在线看网址| 日韩一级大片在线观看| 少妇高潮喷水在线观看| 成a人片在线观看www视频| 免费成人在线影院| 久久久久久久久国产| 日韩女同一区二区三区| 成人激情视屏| 五月天亚洲精品| 一区二区av| 午夜视频在线免费播放| 国内一区二区视频| 欧美激情一区二区三区在线视频观看| 在线免费观看日韩av| 国产一区2区在线观看| 精品久久久久久久久久ntr影视 | 日本一级黄色录像| 精品国内自产拍在线观看视频| 91麻豆精品国产91久久久久| 俄罗斯av网站| 羞羞视频在线观看免费| 国产欧美一区二区三区在线看蜜臀 | 91人人爽人人爽人人精88v| 欧美人禽zoz0强交| 精品产国自在拍| 亚洲国产欧美在线成人app| 亚洲av无日韩毛片久久| 日日av拍夜夜添久久免费| 亚洲国产欧美另类丝袜| 五月天综合婷婷| www.91在线| 91在线播放网址| 亚洲综合第一页| 亚洲性猛交富婆| 性色一区二区三区| 国内偷自视频区视频综合| 男人av资源站| 大胆日韩av| 亚洲男人天堂网站| 欧美成人三级伦在线观看| 伊人www22综合色| 这里只有精品99re| 潘金莲激情呻吟欲求不满视频| 日韩欧美看国产| 亚洲国产人成综合网站| 狠狠噜天天噜日日噜| www久久日com| 国产天堂亚洲国产碰碰| 欧美日韩精品免费看| 日本又骚又刺激的视频在线观看| 国产乱码精品一区二区三 | 日韩成人在线一区| 欧美熟乱第一页| 热久久精品免费视频| 欧美激情喷水| 欧美自拍偷拍午夜视频| 成人短视频在线观看免费| av在线免费网站| 亚洲精品五月天| xxxxxx在线观看| 日本不卡影院| 亚洲成av人影院| 国产毛片视频网站| 日韩av一卡| 色狠狠av一区二区三区| 国产精彩免费视频| 久久精品资源| 日韩欧美激情一区| 黑森林av导航| 国产精品一区免费在线 | www.久久成人| av在线不卡网| 日韩欧美三级一区二区| 看电影就来5566av视频在线播放| 国产午夜精品久久| 亚洲欧美国产不卡| 丝袜综合欧美| 午夜精品福利一区二区三区蜜桃| 可以免费观看av毛片| 日韩一区精品| 日韩免费看网站| 亚洲av成人无码一二三在线观看| 精品视频国产| 美日韩在线视频| 国产又黄又爽又色| 麻豆视频一区二区| 国产精品免费一区二区三区| 亚洲av成人精品毛片| 中文幕一区二区三区久久蜜桃| 久久久成人精品一区二区三区| 丰满的护士2在线观看高清| 婷婷成人综合网| 亚洲欧美自偷自拍另类| 大桥未久女教师av一区二区| 亚洲天天在线日亚洲洲精| 99精品中文字幕| 一本久道久久久| 成人黄色av网站| 国产精品欧美亚洲| 成人精品鲁一区一区二区| 午夜精品美女久久久久av福利| 免费在线观看的电影网站| 在线影院国内精品| 无码人妻丰满熟妇区毛片蜜桃精品 | 国产高清不卡一区二区| 欧美激情一区二区三区在线视频| 国产在线观看av| 一本色道久久综合精品竹菊| 男女啪啪网站视频| 成人网ww555视频免费看| 欧美一卡2卡三卡4卡5免费| 又黄又色的网站| 婷婷中文字幕一区| 国产精品2018| 天堂在线视频观看| 亚洲免费观看在线视频| 成年在线观看视频| 国产亚洲欧美日韩精品一区二区三区 | 一女二男一黄一片| 91碰在线视频| 日本香蕉视频在线观看| 国产一区二区三区四区五区3d| gogo高清在线播放免费| 久久你懂得1024| 免费网站永久免费观看| 国产成人精品亚洲日本在线观看| 欧美大胆一级视频| 一级二级黄色片| 视频一区国产视频| 国产女主播一区二区| 制服丝袜中文字幕在线| 7777精品伊人久久久大香线蕉完整版| 精品无码在线视频| 欧美激情第10页| 国产精品视频最多的网站| 深夜影院在线观看| 亚洲主播在线播放| 亚洲欧美天堂在线| 91嫩草亚洲精品| 国产精品色视频| 第一页在线观看| 91久久精品一区二区| 国产精品无码在线| 亚洲福利精品| 国产一区二区在线网站| 国产精品蜜芽在线观看| 亚洲国产中文字幕久久网| 久久免费播放视频| 成人午夜碰碰视频| 女人帮男人橹视频播放| www久久久| 日韩在线观看av| av中文字幕观看| 亚洲午夜激情网站| 污污污www精品国产网站| 一区免费视频| 国产在线精品二区| 亚洲黄色免费av| 亚洲欧美日韩网| 中文字幕在线观看1| 国产精品成人一区二区艾草| 免费在线观看污网站| 欧美99在线视频观看| 99在线看视频| 国产激情视频在线看| 亚洲欧美日韩精品久久| 五月婷婷丁香在线| 中文字幕精品一区| 国产精品一区二区小说| 国产专区一区| 久久久久久久久一区| 日本综合视频| 欧美成人精品在线| 日批免费在线观看| 色爱区综合激月婷婷| 小向美奈子av| 国产99一区视频免费 | 欧美久久成人| 国产欧美丝袜| 精品欧美日韩精品| 欧美美女18p| 日韩精品123| 欧美剧在线免费观看网站| 欧美激情一区二区视频| 2017欧美狠狠色| 伊人色在线观看| 亚洲视频福利| 亚洲永久一区二区三区在线| 日韩精品一区国产| 日本精品久久久| 欧美猛烈性xbxbxbxb| 亚洲电影中文字幕| 中国a一片一级一片| 亚洲猫色日本管| 亚洲国产果冻传媒av在线观看| 全国精品久久少妇| 免费看毛片的网址| 日本欧美国产| 国产一区视频观看| 欧洲午夜精品| 全球成人中文在线| 欧美xxxx免费虐| 最近2019中文字幕在线高清| 免费观看的毛片| 在线国产亚洲欧美| 欧美一区二区三区网站| 一区二区三区在线观看动漫| 极品人妻videosss人妻| 成人午夜视频福利| 天天色综合社区| 翔田千里一区二区| 久久亚洲a v| 围产精品久久久久久久| 欧美极品视频一区二区三区| 天堂av一区| 国产欧美久久一区二区| 在线人成日本视频| 久久久久久久97| 黄色精品免费看| 国产一区二区三区四区福利| 污污网站免费在线观看| 欧美成人精精品一区二区频| 97精品人妻一区二区三区| 日本韩国一区二区| 国产小视频在线免费观看| 亚洲精品视频免费观看| 911国产在线| 中文字幕不卡在线| 在线国产视频一区| 91麻豆免费观看| 天天插天天射天天干| 国产成人在线电影| 天天影视色综合| 韩国欧美国产一区| 天堂av在线8| 韩日欧美一区二区三区| 国产免费中文字幕| 另类欧美日韩国产在线| 在线看的黄色网址| 久久99精品国产.久久久久| 午夜两性免费视频| 男女男精品视频| 色乱码一区二区三区在线| 日韩av一区二区三区四区| 欧美精品成人网| 日日摸夜夜添夜夜添国产精品| 日本免费不卡一区二区| 亚洲国产精品第一区二区| 成品人视频ww入口| 欧美搞黄网站| 日本福利视频在线观看| 亚洲无线视频| 日本手机在线视频| 亚洲国产精品第一区二区三区 | 亚洲人视频在线观看| 亚洲精品www久久久| 性xxxx视频播放免费| 精品无人区太爽高潮在线播放| 欧美日韩国产亚洲沙发| 亚洲人午夜精品免费| 都市激情一区| 蜜臀久久99精品久久久久久宅男| av免费在线观看网站| 久久久久久中文字幕| 午夜伦理福利在线| 国产精品678| av一级久久| 国产精品初高中精品久久| 婷婷五月色综合香五月| 日韩av在线电影观看| 欧美丰满日韩| 91视频 - 88av| 亚洲免费影视| 日本 片 成人 在线| 国产一区999| 中文字幕无码人妻少妇免费| 久久久99久久| 1024手机在线视频| 欧美午夜精品久久久久久人妖| 欧美精品韩国精品| 制服丝袜中文字幕一区| 婷婷丁香一区二区三区| 宅男66日本亚洲欧美视频| 黄色网页在线播放| 91福利视频网| 爱情电影网av一区二区| 精品在线观看一区二区| 欧美r级电影| 免费看国产一级片| 男女视频一区二区| 香蕉视频污视频| 国产精品三级av| 福利一区二区三区四区| 欧美日韩不卡在线| 日韩在线视频第一页| 亚洲四色影视在线观看| а√中文在线8| 国产精品精品一区二区三区午夜版| 一区二区三区四区精品视频| 亚洲精品不卡| 国产精品尤物| 爱情岛论坛亚洲自拍| 国产午夜精品一区二区三区视频| 免费在线看黄网址| 精品1区2区3区| 无码国精品一区二区免费蜜桃| 久久精品国产99国产精品澳门| 忘忧草在线影院两性视频| 国产一区深夜福利| 欧美三级美国一级| 青青草精品视频在线| 国产在线视频一区二区| 亚洲精品国产91| 亚洲国产视频网站| 97精品久久人人爽人人爽| 亚洲人成电影在线播放| 日本乱理伦在线| 成人激情视频小说免费下载| 国产一区毛片| 天天夜碰日日摸日日澡性色av| 韩国三级电影一区二区| 日本污视频网站| 欧美性少妇18aaaa视频| 免费av网站在线播放| 欧美另类在线观看| 成人黄色91| 亚洲欧洲日韩综合二区| 日韩亚洲国产精品| 一区二区三区人妻| 亚洲欧洲国产日本综合| 国产99久久久| 精品亚洲国产成av人片传媒 | 高清欧美性猛交xxxx黑人猛| 艳母动漫在线免费观看| 蜜臀av性久久久久蜜臀aⅴ流畅| 国产sm调教视频| 午夜电影一区二区三区| 四虎影院在线播放| 2019国产精品自在线拍国产不卡| 国产成人av毛片| 91免费黄视频| 不卡视频在线看| 欧美亚洲天堂网| 亚洲国产精品久久精品怡红院| 欧美bbbxxxxx| 国产精品久久亚洲7777| 亚洲欧美bt| 一区二区三区伦理片| 欧洲人成人精品| 91官网在线| 成人免费福利在线| 亚洲女同另类| 深夜视频在线观看| 一区二区三区在线免费观看| 草草视频在线播放| 欧美激情小视频| 欧美黄色网视频| www.玖玖玖| 国产偷国产偷精品高清尤物| 国产精品露脸视频| 亚洲天堂色网站| 欧美一级大片| 国产免费xxx| av网站免费线看精品| 成人公开免费视频| 三级精品视频久久久久| 国产精品日韩精品在线播放| 蜜桃视频一区二区在线观看| 99国产精品一区| 福利网址在线观看| 久久av资源网站| 天天躁日日躁狠狠躁欧美| 三级a在线观看| 亚洲色图欧美激情| 无码国产精品高潮久久99| 国产精品国产三级国产aⅴ9色| 国产精品久久久乱弄 | 国产成人精品一区二区三区在线| 美国av在线播放| 99久久久精品|