多租户数据迁移:MyBatis-Plus在多租户架构下的数据迁移策略
引言:多租户架构的挑战与机遇
在现代SaaS(Software as a Service)应用中,多租户架构已成为主流设计模式。这种架构允许多个客户(租户)共享同一套应用程序实例,同时保持数据隔离性。然而,当涉及到数据迁移、租户数据拆分或合并时,传统的数据库操作方式往往力不从心。
痛点场景:你是否遇到过以下困境?
- 需要将某个租户的数据完整迁移到新环境
- 租户合并时数据冲突难以处理
- 批量数据操作时租户隔离失效
- 迁移过程中数据一致性难以保证
MyBatis-Plus作为MyBatis的增强工具,提供了强大的多租户支持,本文将深入探讨在多租户架构下的数据迁移策略。
MyBatis-Plus多租户核心机制解析
租户拦截器工作原理
MyBatis-Plus通过TenantLineInnerInterceptor实现行级租户隔离,其核心原理如下:
核心接口与配置
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工具类,我们可以实现灵活、安全的数据迁移操作。
关键要点总结:
- 理解拦截机制:掌握MyBatis-Plus多租户拦截器的工作原理
- 合理使用忽略策略:在迁移场景中适时禁用租户过滤
- 分阶段执行:大规模迁移采用分批处理策略
- 完善监控回滚:建立完整的迁移状态跟踪和回滚机制
- 性能优化:通过批量操作、流式处理和索引优化提升迁移效率
通过本文介绍的策略和实践,您可以在多租户架构下安全、高效地完成数据迁移任务,确保业务连续性和数据一致性。
温馨提示:在实际生产环境中执行数据迁移前,请务必进行充分的测试和备份,建议先在测试环境验证迁移方案的可行性和性能表现。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



