RuoYi-Vue-Plus租户同步:数据同步

【免费下载链接】RuoYi-Vue-Plus 多租户后台管理系统 重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Warm-Flow工作流、SpringDoc、Hutool、OSS 定期同步 【免费下载链接】RuoYi-Vue-Plus 项目地址: https://gitcode.com/dromara/RuoYi-Vue-Plus

引言

在多租户(Multi-Tenancy)系统中,数据同步是一个至关重要的功能。RuoYi-Vue-Plus作为一款优秀的多租户后台管理系统,提供了完善的租户数据同步机制。本文将深入解析RuoYi-Vue-Plus的租户数据同步实现原理、使用方法和最佳实践。

租户同步架构设计

RuoYi-Vue-Plus采用基于数据库隔离的多租户架构,每个租户拥有独立的数据空间。同步机制主要涉及两个核心场景:

  1. 租户套餐同步:同步租户的菜单权限配置
  2. 字典数据同步:同步系统字典数据到各个租户

mermaid

核心同步功能详解

1. 租户套餐同步

租户套餐同步用于将套餐的菜单权限配置同步到指定租户的管理员角色。

实现原理
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean syncTenantPackage(String tenantId, Long packageId) {
    // 获取套餐配置
    SysTenantPackage tenantPackage = tenantPackageMapper.selectById(packageId);
    List<SysRole> roles = roleMapper.selectList(
        new LambdaQueryWrapper<SysRole>().eq(SysRole::getTenantId, tenantId));
    
    List<Long> roleIds = new ArrayList<>(roles.size() - 1);
    List<Long> menuIds = StringUtils.splitTo(tenantPackage.getMenuIds(), Convert::toLong);
    
    // 更新管理员角色菜单权限
    roles.forEach(item -> {
        if (TenantConstants.TENANT_ADMIN_ROLE_KEY.equals(item.getRoleKey())) {
            List<SysRoleMenu> roleMenus = new ArrayList<>(menuIds.size());
            menuIds.forEach(menuId -> {
                SysRoleMenu roleMenu = new SysRoleMenu();
                roleMenu.setRoleId(item.getRoleId());
                roleMenu.setMenuId(menuId);
                roleMenus.add(roleMenu);
            });
            // 删除旧权限,插入新权限
            roleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>()
                .eq(SysRoleMenu::getRoleId, item.getRoleId()));
            roleMenuMapper.insertBatch(roleMenus);
        } else {
            roleIds.add(item.getRoleId());
        }
    });
    
    // 同步其他角色的菜单权限(移除不在套餐中的权限)
    if (!roleIds.isEmpty()) {
        roleMenuMapper.delete(
            new LambdaQueryWrapper<SysRoleMenu>()
                .in(SysRoleMenu::getRoleId, roleIds)
                .notIn(!menuIds.isEmpty(), SysRoleMenu::getMenuId, menuIds));
    }
    return true;
}
使用方式

通过HTTP API调用套餐同步:

GET /system/tenant/syncTenantPackage?tenantId=100001&packageId=1

2. 字典数据同步

字典数据同步用于将默认租户(超管租户)的字典数据同步到所有正常状态的租户。

实现原理
@Transactional(rollbackFor = Exception.class)
@Override
public void syncTenantDict() {
    // 获取默认租户的所有字典数据
    List<SysDictType> dictTypeList = new ArrayList<>();
    List<SysDictData> dictDataList = new ArrayList<>();
    TenantHelper.ignore(() -> {
        dictTypeList.addAll(dictTypeMapper.selectList());
        dictDataList.addAll(dictDataMapper.selectList());
    });
    
    // 按租户分组字典数据
    Map<String, List<SysDictType>> typeMap = StreamUtils.groupByKey(dictTypeList, TenantEntity::getTenantId);
    Map<String, Map<String, List<SysDictData>>> typeDataMap = StreamUtils.groupBy2Key(
        dictDataList, TenantEntity::getTenantId, SysDictData::getDictType);
    
    // 获取所有正常状态的租户
    List<String> tenantIds = baseMapper.selectObjs(
        new LambdaQueryWrapper<SysTenant>()
            .select(SysTenant::getTenantId)
            .eq(SysTenant::getStatus, SystemConstants.NORMAL), 
        x -> Convert.toStr(x));
    
    List<SysDictType> saveTypeList = new ArrayList<>();
    List<SysDictData> saveDataList = new ArrayList<>();
    Set<String> set = new HashSet<>();
    
    // 遍历所有租户,同步字典数据
    for (String tenantId : tenantIds) {
        if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
            continue;
        }
        // 同步逻辑...
    }
    
    // 批量保存并清理缓存
    TenantHelper.ignore(() -> {
        if (CollUtil.isNotEmpty(saveTypeList)) {
            dictTypeMapper.insertBatch(saveTypeList);
        }
        if (CollUtil.isNotEmpty(saveDataList)) {
            dictDataMapper.insertBatch(saveDataList);
        }
    });
    
    for (String tenantId : set) {
        TenantHelper.dynamic(tenantId, () -> CacheUtils.clear(CacheNames.SYS_DICT));
    }
}
使用方式

通过HTTP API调用字典同步:

GET /system/tenant/syncTenantDict

同步策略对比

同步类型 目标对象 同步粒度 性能影响 使用场景
套餐同步 单个租户 菜单权限 修改套餐后立即生效
字典同步 所有租户 字典数据 系统初始化或字典变更

技术实现细节

1. 租户隔离处理

RuoYi-Vue-Plus使用TenantHelper.ignore()方法在同步过程中临时忽略租户隔离:

TenantHelper.ignore(() -> {
    // 这里可以跨租户查询数据
    dictTypeList.addAll(dictTypeMapper.selectList());
});

2. 批量操作优化

系统采用批量插入和删除操作提升同步性能:

// 批量插入角色菜单关系
roleMenuMapper.insertBatch(roleMenus);

// 批量插入字典数据
dictDataMapper.insertBatch(saveDataList);

3. 缓存清理机制

同步完成后自动清理相关缓存,确保数据一致性:

TenantHelper.dynamic(tenantId, () -> CacheUtils.clear(CacheNames.SYS_DICT));

最佳实践

1. 同步时机选择

  • 套餐同步:在租户购买新套餐或管理员修改套餐配置后立即执行
  • 字典同步:在系统初始化或全局字典数据发生重大变更时执行

2. 性能优化建议

mermaid

3. 错误处理策略

  • 使用@Transactional确保数据一致性
  • 捕获并记录同步过程中的异常
  • 提供重试机制应对网络或数据库异常

常见问题解答

Q1: 同步操作会影响现有业务吗?

A: 同步操作采用批量处理和事务机制,对业务影响较小,建议在业务低峰期执行。

Q2: 如何监控同步进度和结果?

A: 可以通过系统日志和数据库监控工具跟踪同步过程,同步API会返回操作结果。

Q3: 同步失败如何处理?

A: 系统会自动回滚事务,管理员可以根据错误日志进行排查和重试。

总结

RuoYi-Vue-Plus的租户数据同步机制提供了灵活、高效的解决方案,能够满足多租户系统中数据一致性管理的需求。通过合理的同步策略和优化措施,可以确保系统在扩展性和性能方面达到最佳平衡。

掌握这些同步技术,将帮助您更好地管理和维护多租户系统,提升系统的稳定性和用户体验。

【免费下载链接】RuoYi-Vue-Plus 多租户后台管理系统 重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Warm-Flow工作流、SpringDoc、Hutool、OSS 定期同步 【免费下载链接】RuoYi-Vue-Plus 项目地址: https://gitcode.com/dromara/RuoYi-Vue-Plus

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐