多租户数据迁移:MyBatis-Plus在多租户架构下的数据迁移策略

多租户数据迁移:MyBatis-Plus在多租户架构下的数据迁移策略

【免费下载链接】mybatis-plus An powerful enhanced toolkit of MyBatis for simplify development 【免费下载链接】mybatis-plus 项目地址: https://gitcode.com/gh_mirrors/my/mybatis-plus

引言:多租户架构的挑战与机遇

在现代SaaS(Software as a Service)应用中,多租户架构已成为主流设计模式。这种架构允许多个客户(租户)共享同一套应用程序实例,同时保持数据隔离性。然而,当涉及到数据迁移、租户数据拆分或合并时,传统的数据库操作方式往往力不从心。

痛点场景:你是否遇到过以下困境?

  • 需要将某个租户的数据完整迁移到新环境
  • 租户合并时数据冲突难以处理
  • 批量数据操作时租户隔离失效
  • 迁移过程中数据一致性难以保证

MyBatis-Plus作为MyBatis的增强工具,提供了强大的多租户支持,本文将深入探讨在多租户架构下的数据迁移策略。

MyBatis-Plus多租户核心机制解析

租户拦截器工作原理

MyBatis-Plus通过TenantLineInnerInterceptor实现行级租户隔离,其核心原理如下:

mermaid

核心接口与配置

TenantLineHandler接口
public interface TenantLineHandler {
    // 获取当前租户ID表达式
    Expression getTenantId();
    
    // 获取租户字段名(默认tenant_id)
    default String getTenantIdColumn() {
        return "tenant_id";
    }
    
    // 忽略特定表的租户处理
    default boolean ignoreTable(String tableName) {
        return false;
    }
    
    // 忽略插入时的租户字段处理
    default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
        return columns.stream()
            .map(Column::getColumnName)
            .anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));
    }
}
拦截器配置示例
@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 添加租户拦截器
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 从安全上下文中获取当前租户ID
                return new LongValue(SecurityUtils.getCurrentTenantId());
            }
            
            @Override
            public boolean ignoreTable(String tableName) {
                // 系统表不进行租户过滤
                return tableName.startsWith("sys_");
            }
        }));
        
        return interceptor;
    }
}

多租户数据迁移策略

策略一:基于InterceptorIgnore的临时绕过

MyBatis-Plus提供了@InterceptorIgnore注解,可以在特定方法上临时禁用租户拦截:

public interface DataMigrationMapper extends BaseMapper<User> {
    
    @InterceptorIgnore(tenantLine = "1")
    @Select("SELECT * FROM user WHERE tenant_id = #{sourceTenantId}")
    List<User> selectBySourceTenant(@Param("sourceTenantId") Long sourceTenantId);
    
    @InterceptorIgnore(tenantLine = "1")
    @Insert({
        "<script>",
        "INSERT INTO user (id, name, tenant_id) VALUES ",
        "<foreach collection='users' item='user' separator=','>",
        "(#{user.id}, #{user.name}, #{targetTenantId})",
        "</foreach>",
        "</script>"
    })
    int batchInsertWithTargetTenant(@Param("users") List<User> users, 
                                   @Param("targetTenantId") Long targetTenantId);
}

策略二:编程式忽略拦截器

对于复杂的迁移逻辑,可以使用InterceptorIgnoreHelper进行编程式控制:

@Service
public class TenantDataMigrationService {
    
    @Autowired
    private UserMapper userMapper;
    
    public void migrateUserData(Long sourceTenantId, Long targetTenantId) {
        // 临时禁用租户拦截
        InterceptorIgnoreHelper.execute(
            IgnoreStrategy.builder().tenantLine(true).build(),
            () -> {
                List<User> sourceUsers = userMapper.selectList(
                    Wrappers.<User>query()
                        .eq("tenant_id", sourceTenantId)
                );
                
                // 批量更新租户ID
                sourceUsers.forEach(user -> user.setTenantId(targetTenantId));
                
                userMapper.updateBatchById(sourceUsers);
                return null;
            }
        );
    }
}

策略三:分阶段迁移模式

对于大规模数据迁移,建议采用分阶段策略:

阶段操作说明风险控制
准备阶段数据校验、容量评估验证源数据和目标环境备份源数据
迁移阶段分批数据转移使用批量操作减少数据库压力每批提交后验证
验证阶段数据一致性检查对比迁移前后数据异常回滚机制
切换阶段流量切换逐步将流量切换到新租户监控系统性能
public class StagedMigrationExecutor {
    
    private static final int BATCH_SIZE = 1000;
    
    public void executeStagedMigration(Long sourceTenantId, Long targetTenantId) {
        // 阶段1:数据准备和校验
        prepareMigration(sourceTenantId, targetTenantId);
        
        // 阶段2:分批迁移
        migrateInBatches(sourceTenantId, targetTenantId);
        
        // 阶段3:数据验证
        validateMigration(sourceTenantId, targetTenantId);
    }
    
    private void migrateInBatches(Long sourceTenantId, Long targetTenantId) {
        int offset = 0;
        List<User> batchUsers;
        
        do {
            batchUsers = InterceptorIgnoreHelper.execute(
                IgnoreStrategy.builder().tenantLine(true).build(),
                () -> userMapper.selectList(
                    Wrappers.<User>query()
                        .eq("tenant_id", sourceTenantId)
                        .last("LIMIT " + BATCH_SIZE + " OFFSET " + offset)
                )
            );
            
            if (!batchUsers.isEmpty()) {
                // 更新租户ID并批量保存
                batchUsers.forEach(user -> user.setTenantId(targetTenantId));
                userMapper.updateBatchById(batchUsers);
                
                offset += batchUsers.size();
            }
        } while (!batchUsers.isEmpty());
    }
}

高级迁移场景处理

场景一:租户数据合并

当需要将多个租户数据合并时,需要处理可能的数据冲突:

public class TenantMergeService {
    
    public void mergeTenants(List<Long> sourceTenantIds, Long targetTenantId) {
        sourceTenantIds.forEach(sourceTenantId -> {
            // 处理用户数据合并(处理用户名冲突等)
            mergeUserData(sourceTenantId, targetTenantId);
            
            // 处理业务数据合并
            mergeBusinessData(sourceTenantId, targetTenantId);
        });
    }
    
    private void mergeUserData(Long sourceTenantId, Long targetTenantId) {
        InterceptorIgnoreHelper.execute(
            IgnoreStrategy.builder().tenantLine(true).build(),
            () -> {
                List<User> sourceUsers = userMapper.selectList(
                    Wrappers.<User>query().eq("tenant_id", sourceTenantId)
                );
                
                sourceUsers.forEach(user -> {
                    // 检查用户名是否冲突
                    Long existingUser = userMapper.selectCount(
                        Wrappers.<User>query()
                            .eq("username", user.getUsername())
                            .eq("tenant_id", targetTenantId)
                    );
                    
                    if (existingUser == 0) {
                        user.setTenantId(targetTenantId);
                        userMapper.insert(user);
                    } else {
                        // 处理冲突逻辑
                        handleUserConflict(user, targetTenantId);
                    }
                });
                
                return null;
            }
        );
    }
}

场景二:跨数据库迁移

对于跨数据库的迁移,需要结合MyBatis-Plus的多数据源支持:

@DS("source-db") // 指定源数据库
public interface SourceDbMapper {
    @InterceptorIgnore(tenantLine = "1")
    @Select("SELECT * FROM user WHERE tenant_id = #{tenantId}")
    List<User> selectByTenant(@Param("tenantId") Long tenantId);
}

@DS("target-db") // 指定目标数据库  
public interface TargetDbMapper {
    @InterceptorIgnore(tenantLine = "1")
    void batchInsert(@Param("users") List<User> users);
}

@Service
public class CrossDatabaseMigrationService {
    
    @Autowired
    private SourceDbMapper sourceDbMapper;
    
    @Autowired
    private TargetDbMapper targetDbMapper;
    
    @Transactional
    public void crossDatabaseMigration(Long tenantId) {
        List<User> users = sourceDbMapper.selectByTenant(tenantId);
        
        // 数据处理和转换
        users.forEach(this::transformUserData);
        
        targetDbMapper.batchInsert(users);
    }
}

迁移过程中的监控与回滚

迁移状态跟踪

建立完善的迁移状态跟踪机制:

@Entity
@TableName("data_migration_log")
public class DataMigrationLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String migrationType;
    private Long sourceTenantId;
    private Long targetTenantId;
    private Integer totalRecords;
    private Integer successRecords;
    private Integer failedRecords;
    private String status; // INIT, PROCESSING, SUCCESS, FAILED
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private String errorMessage;
}

public class MigrationMonitor {
    
    public void monitorMigration(Long migrationId) {
        DataMigrationLog log = migrationLogMapper.selectById(migrationId);
        
        if ("PROCESSING".equals(log.getStatus())) {
            // 检查迁移进度
            checkMigrationProgress(log);
            
            // 记录监控指标
            recordMigrationMetrics(log);
        }
    }
    
    private void checkMigrationProgress(DataMigrationLog log) {
        // 实现进度检查逻辑
        Integer processed = calculateProcessedRecords(log);
        log.setSuccessRecords(processed);
        migrationLogMapper.updateById(log);
    }
}

回滚机制设计

public class MigrationRollbackService {
    
    @Transactional(rollbackFor = Exception.class)
    public void rollbackMigration(Long migrationId) {
        DataMigrationLog log = migrationLogMapper.selectById(migrationId);
        
        if (log != null && "SUCCESS".equals(log.getStatus())) {
            try {
                // 执行回滚操作
                executeRollback(log);
                
                log.setStatus("ROLLBACK_SUCCESS");
                migrationLogMapper.updateById(log);
            } catch (Exception e) {
                log.setStatus("ROLLBACK_FAILED");
                log.setErrorMessage(e.getMessage());
                migrationLogMapper.updateById(log);
                throw e;
            }
        }
    }
    
    private void executeRollback(DataMigrationLog log) {
        InterceptorIgnoreHelper.execute(
            IgnoreStrategy.builder().tenantLine(true).build(),
            () -> {
                // 删除目标租户数据
                userMapper.delete(
                    Wrappers.<User>query()
                        .eq("tenant_id", log.getTargetTenantId())
                        .apply("created_time >= {0}", log.getStartTime())
                );
                
                return null;
            }
        );
    }
}

最佳实践与性能优化

批量操作优化

public class BatchMigrationOptimizer {
    
    private static final int OPTIMAL_BATCH_SIZE = 500;
    
    public void optimizedBatchMigration(Long sourceTenantId, Long targetTenantId) {
        // 使用流式处理减少内存占用
        try (Stream<User> userStream = getUserStream(sourceTenantId)) {
            List<List<User>> batches = StreamUtils.batch(
                userStream, 
                OPTIMAL_BATCH_SIZE
            );
            
            batches.forEach(batch -> {
                processBatch(batch, targetTenantId);
                
                // 批量提交,减少事务开销
                if (batch.size() == OPTIMAL_BATCH_SIZE) {
                    SqlHelper.clearCache();
                }
            });
        }
    }
    
    private Stream<User> getUserStream(Long tenantId) {
        return InterceptorIgnoreHelper.execute(
            IgnoreStrategy.builder().tenantLine(true).build(),
            () -> userMapper.selectStream(
                Wrappers.<User>query().eq("tenant_id", tenantId)
            )
        );
    }
}

索引与查询优化

在多租户迁移过程中,合理的索引设计至关重要:

-- 为迁移相关的查询创建复合索引
CREATE INDEX idx_tenant_migration ON user(tenant_id, created_time);
CREATE INDEX idx_tenant_status ON user(tenant_id, status);

-- 迁移过程中临时索引
CREATE INDEX idx_migration_temp ON user(tenant_id) WHERE tenant_id IN (source_tenant_ids);

总结

MyBatis-Plus在多租户数据迁移方面提供了强大的支持,通过TenantLineInnerInterceptor@InterceptorIgnore注解和InterceptorIgnoreHelper工具类,我们可以实现灵活、安全的数据迁移操作。

关键要点总结

  1. 理解拦截机制:掌握MyBatis-Plus多租户拦截器的工作原理
  2. 合理使用忽略策略:在迁移场景中适时禁用租户过滤
  3. 分阶段执行:大规模迁移采用分批处理策略
  4. 完善监控回滚:建立完整的迁移状态跟踪和回滚机制
  5. 性能优化:通过批量操作、流式处理和索引优化提升迁移效率

通过本文介绍的策略和实践,您可以在多租户架构下安全、高效地完成数据迁移任务,确保业务连续性和数据一致性。


温馨提示:在实际生产环境中执行数据迁移前,请务必进行充分的测试和备份,建议先在测试环境验证迁移方案的可行性和性能表现。

【免费下载链接】mybatis-plus An powerful enhanced toolkit of MyBatis for simplify development 【免费下载链接】mybatis-plus 项目地址: https://gitcode.com/gh_mirrors/my/mybatis-plus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值