一、微服务架构演进背景

1.1 从单体到微服务

在企业信息化建设中,早期通常采用单体架构部署。随着业务发展,系统逐渐暴露出以下问题:

单体架构痛点

  • 代码耦合严重:HRM、OA、CRM等业务模块共库共部署,牵一发而动全身
  • 扩展性差:无法针对特定模块进行独立扩容,整体资源浪费
  • 技术栈僵化:统一的技术选型限制了不同模块的最优技术选择
  • 部署风险高:任何模块的bug都可能导致整个系统崩溃
  • 团队协作困难:多团队同时维护一个代码库,冲突频繁

1.2 点狮云平台微服务拆分策略

点狮全业务管理平台采用基于业务能力的微服务拆分策略:

模块服务名职责数据库
系统管理pointlion-module-system-server用户、角色、权限、部门、岗位pointlion_cloud_system
人力资源pointlion-module-hrm-server员工档案、薪资、考勤、绩效pointlion_cloud_local_hrm
办公自动化pointlion-module-oa-server审批流程、会议管理、公告pointlion_cloud_local_oa
客户关系pointlion-module-crm-server客户、商机、合同、跟进pointlion_cloud_local_crm
网关服务pointlion-gateway路由转发、鉴权、限流-

🌐 相关链接

🏢 官网与演示站

类型地址说明
官方网站点狮信息官网 http://www.dianshixinxi.com企业官网
在线体验点狮全业务管理平台演示站 http://cloud.dianshixinxi.com:90云平台在线体验
多业务版本Ruoyi多业务版本 http://boot.dianshixinxi.com:90若依多业务版本

🔗 代码仓库

平台地址说明
Gitee点狮多业务管理平台 https://gitee.com/glorylion/JFinalOAGitee代码仓库
Gitcode点狮HRM模块 https://gitcode.com/Glory_Lion/pointlion-HRMHRM独立模块
Gitcode点狮多业务管理平台 https://gitcode.com/Glory_Lion/pointlion-cloud完整平台

在这里插入图片描述
在这里插入图片描述

二、跨模块数据访问的典型场景

2.1 场景一:员工档案需要关联部门信息

业务需求:在员工档案列表中显示所属部门名称,并支持按部门树筛选员工。

数据分布

  • 部门数据(system_dept)存储在 pointlion_cloud_system 数据库
  • 员工数据(hrm_employee)存储在 pointlion_cloud_local_hrm 数据库

问题:HRM模块无法直接JOIN系统模块的部门表。

2.2 场景二:创建审批流程需要关联发起人信息

业务需求:OA模块的审批流程需要显示发起人的姓名、部门、岗位等信息。

数据分布

  • 用户基础信息在系统模块
  • 员工详细信息在HRM模块

2.3 场景三:客户跟进需要关联负责人信息

业务需求:CRM模块的客户跟进记录需要显示跟进负责人的联系方式、部门信息。


三、跨模块数据访问解决方案

3.1 方案对比

方案优点缺点适用场景
直接跨库查询简单直接违反服务边界、紧耦合不推荐
数据冗余查询快速数据一致性问题读多写少场景
服务间调用(Feign)松耦合、可扩展性能开销、网络依赖通用方案
消息队列异步同步最终一致、解耦延迟、复杂度高非实时场景
数据聚合服务统一查询入口单点瓶颈、复杂复杂查询

3.2 方案一:Feign服务间调用(推荐)

实现步骤

Step 1: 定义Feign Client接口

// 在HRM模块中定义
package com.pointlion.cloud.module.hrm.api.system;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.pointlion.cloud.framework.common.pojo.Result;

import java.util.List;

/**
 * 系统模块的Feign Client
 * 用于HRM模块调用系统模块的服务
 */
@FeignClient(
    name = "pointlion-module-system-server",  // 服务名
    path = "/system/dept",                      // 路径前缀
    fallbackFactory = DeptClientFallback.class // 降级处理
)
public interface DeptClient {

    /**
     * 获取部门及其所有子部门的ID列表
     */
    @GetMapping("/get-children-ids")
    Result<List<Long>> getChildrenIds(@RequestParam("deptId") Long deptId);

    /**
     * 批量获取部门信息
     */
    @GetMapping("/list-by-ids")
    Result<List<DeptRespDTO>> listByIds(@RequestParam("ids") List<Long> ids);

    /**
     * 获取部门详情
     */
    @GetMapping("/get")
    Result<DeptRespDTO> getDept(@RequestParam("id") Long id);
}

Step 2: 定义DTO

package com.pointlion.cloud.module.hrm.api.system;

import lombok.Data;
import java.io.Serializable;

@Data
public class DeptRespDTO implements Serializable {
    private Long id;
    private String name;
    private Long parentId;
    private Integer sort;
    private Long leaderUserId;
    private String phone;
    private String email;
    private Integer status;
}

Step 3: 实现降级处理

package com.pointlion.cloud.module.hrm.api.system;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class DeptClientFallback implements FallbackFactory<DeptClient> {

    @Override
    public DeptClient create(Throwable cause) {
        log.error("[DeptClient] 服务调用失败", cause);
        return new DeptClient() {
            @Override
            public Result<List<Long>> getChildrenIds(Long deptId) {
                log.warn("[DeptClient] getChildrenIds 降级处理,deptId={}", deptId);
                return Result.success(Collections.singletonList(deptId)); // 返回自身
            }

            @Override
            public Result<List<DeptRespDTO>> listByIds(List<Long> ids) {
                log.warn("[DeptClient] listByIds 降级处理,ids={}", ids);
                return Result.success(Collections.emptyList());
            }

            @Override
            public Result<DeptRespDTO> getDept(Long id) {
                log.warn("[DeptClient] getDept 降级处理,id={}", id);
                return Result.success(new DeptRespDTO());
            }
        };
    }
}

Step 4: 在Service中使用

package com.pointlion.cloud.module.hrm.service.employee;

import com.pointlion.cloud.module.hrm.api.system.DeptClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Autowired
    private DeptClient deptClient;  // 注入Feign Client

    @Override
    public PageResult<EmployeeDO> selectPage(EmployeePageReqVO reqVO) {
        // 如果选择了部门,通过Feign获取子部门ID列表
        String departmentId = reqVO.getDepartmentId();
        if (departmentId != null && !departmentId.isEmpty()) {
            List<Long> deptIds = deptClient.getChildrenIds(Long.parseLong(departmentId))
                .getData();
            
            // 转换为String列表
            List<String> deptIdStrings = deptIds.stream()
                .map(String::valueOf)
                .collect(Collectors.toList());
            
            // 查询员工
            return employeeMapper.selectPage(reqVO, deptIdStrings);
        }
        
        return employeeMapper.selectPage(reqVO);
    }
}

Step 5: 系统模块提供API

package com.pointlion.cloud.module.system.controller.admin.dept;

import com.pointlion.cloud.framework.common.pojo.Result;
import com.pointlion.cloud.module.system.controller.admin.dept.vo.DeptRespVO;
import com.pointlion.cloud.module.system.service.dept.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/system/dept")
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 获取部门及其所有子部门的ID列表
     * 提供给其他模块(如HRM)调用
     */
    @GetMapping("/get-children-ids")
    public Result<List<Long>> getChildrenIds(@RequestParam("deptId") Long deptId) {
        List<Long> childrenIds = deptService.getChildrenIds(deptId);
        return Result.success(childrenIds);
    }

    /**
     * 批量获取部门信息
     */
    @GetMapping("/list-by-ids")
    public Result<List<DeptRespVO>> listByIds(@RequestParam("ids") List<Long> ids) {
        List<DeptRespVO> depts = deptService.getDeptListByIds(ids);
        return Result.success(depts);
    }

    @GetMapping("/get")
    public Result<DeptRespVO> getDept(@RequestParam("id") Long id) {
        DeptRespVO dept = deptService.getDept(id);
        return Result.success(dept);
    }
}

Step 6: 配置Feign

# application.yml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 10000
          pointlion-module-system-server:
            connectTimeout: 3000
            readTimeout: 5000
      compression:
        request:
          enabled: true
          mime-types: application/json
          min-request-size: 2048
        response:
          enabled: true

3.3 方案二:数据冗余(适合读多写少)

核心思想:在员工表中冗余存储部门名称,减少跨模块调用。

实现方式

-- 在hrm_employee表中添加department_name字段
ALTER TABLE hrm_employee ADD COLUMN department_name VARCHAR(100) COMMENT '部门名称';

-- 添加索引
CREATE INDEX idx_dept_id ON hrm_employee(department_id);

Service层维护数据一致性

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Autowired
    private DeptClient deptClient;

    @Override
    public void createEmployee(EmployeeSaveReqVO createReqVO) {
        // 1. 查询部门名称
        DeptRespDTO dept = deptClient.getDept(Long.parseLong(createReqVO.getDepartmentId()))
            .getData();
        
        // 2. 设置部门名称
        EmployeeDO employee = EmployeeConvert.INSTANCE.convert(createReqVO);
        employee.setDepartmentName(dept.getName());
        
        // 3. 保存员工
        employeeMapper.insert(employee);
    }

    @Override
    public void updateEmployee(EmployeeSaveReqVO updateReqVO) {
        // 1. 如果部门发生变化,更新部门名称
        EmployeeDO existingEmployee = employeeMapper.selectById(Long.parseLong(updateReqVO.getId()));
        if (!existingEmployee.getDepartmentId().equals(updateReqVO.getDepartmentId())) {
            DeptRespDTO dept = deptClient.getDept(Long.parseLong(updateReqVO.getDepartmentId()))
                .getData();
            existingEmployee.setDepartmentName(dept.getName());
        }
        
        // 2. 更新员工
        EmployeeConvert.INSTANCE.copy(updateReqVO, existingEmployee);
        employeeMapper.updateById(existingEmployee);
    }
}

优点

  • 查询时无需跨模块调用,性能最优
  • 实现简单

缺点

  • 数据冗余,占用存储空间
  • 需要维护数据一致性(部门改名时需同步更新)

3.4 方案三:Redis缓存聚合

核心思想:使用Redis缓存部门信息,减少Feign调用频率。

@Service
public class DeptAggregateService {

    @Autowired
    private DeptClient deptClient;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String DEPT_INFO_KEY = "dept:info:";
    private static final long DEPT_INFO_EXPIRE = 24; // 小时

    /**
     * 获取部门信息(优先从缓存)
     */
    public DeptRespDTO getDeptInfo(Long deptId) {
        String key = DEPT_INFO_KEY + deptId;
        
        // 1. 尝试从缓存获取
        DeptRespDTO cached = (DeptRespDTO) redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached;
        }
        
        // 2. 调用系统模块
        DeptRespDTO dept = deptClient.getDept(deptId).getData();
        
        // 3. 写入缓存
        redisTemplate.opsForValue().set(key, dept, DEPT_INFO_EXPIRE, TimeUnit.HOURS);
        
        return dept;
    }

    /**
     * 批量获取部门信息(批量查询+缓存)
     */
    public Map<Long, DeptRespDTO> getDeptInfoBatch(List<Long> deptIds) {
        Map<Long, DeptRespDTO> result = new HashMap<>();
        List<Long> missIds = new ArrayList<>();
        
        // 1. 批量从缓存获取
        List<String> keys = deptIds.stream()
            .map(id -> DEPT_INFO_KEY + id)
            .collect(Collectors.toList());
        
        List<Object> cached = redisTemplate.opsForValue().multiGet(keys);
        
        for (int i = 0; i < deptIds.size(); i++) {
            Long deptId = deptIds.get(i);
            Object value = cached.get(i);
            if (value != null) {
                result.put(deptId, (DeptRespDTO) value);
            } else {
                missIds.add(deptId);
            }
        }
        
        // 2. 批量查询未命中的
        if (!missIds.isEmpty()) {
            List<DeptRespDTO> depts = deptClient.listByIds(missIds).getData();
            
            // 3. 写入缓存
            Map<String, Object> cacheMap = new HashMap<>();
            for (DeptRespDTO dept : depts) {
                result.put(dept.getId(), dept);
                cacheMap.put(DEPT_INFO_KEY + dept.getId(), dept);
            }
            
            if (!cacheMap.isEmpty()) {
                redisTemplate.opsForValue().multiSet(cacheMap);
                // 设置过期时间
                for (String key : cacheMap.keySet()) {
                    redisTemplate.expire(key, DEPT_INFO_EXPIRE, TimeUnit.HOURS);
                }
            }
        }
        
        return result;
    }

    /**
     * 清除部门缓存(部门变更时调用)
     */
    public void clearDeptCache(Long deptId) {
        redisTemplate.delete(DEPT_INFO_KEY + deptId);
    }
}

3.5 方案四:数据聚合服务(BFF模式)

适用场景:前端需要聚合多个模块的数据。

/**
 * Backend for Frontend 服务
 * 负责聚合多个模块的数据返回给前端
 */
@RestController
@RequestMapping("/api/bff/hrm")
public class HrmBffController {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private DeptAggregateService deptAggregateService;

    @Autowired
    private UserClient userClient;

    /**
     * 获取员工列表(包含部门名称、用户信息)
     */
    @GetMapping("/employees")
    public Result<PageResult<EmployeeWithDeptVO>> getEmployees(EmployeePageReqVO reqVO) {
        // 1. 查询员工分页数据
        PageResult<EmployeeDO> employeePage = employeeService.selectPage(reqVO);
        
        // 2. 获取部门ID列表
        List<Long> deptIds = employeePage.getList().stream()
            .map(e -> Long.parseLong(e.getDepartmentId()))
            .distinct()
            .collect(Collectors.toList());
        
        // 3. 批量获取部门信息
        Map<Long, DeptRespDTO> deptMap = deptAggregateService.getDeptInfoBatch(deptIds);
        
        // 4. 获取用户ID列表
        List<Long> userIds = employeePage.getList().stream()
            .map(EmployeeDO::getLinkUserId)
            .filter(Objects::nonNull)
            .map(Long::valueOf)
            .distinct()
            .collect(Collectors.toList());
        
        // 5. 批量获取用户信息
        Map<Long, UserRespDTO> userMap = userClient.listByIds(userIds).getData().stream()
            .collect(Collectors.toMap(UserRespDTO::getId, u -> u));
        
        // 6. 聚合数据
        List<EmployeeWithDeptVO> result = employeePage.getList().stream()
            .map(employee -> {
                EmployeeWithDeptVO vo = new EmployeeWithDeptVO();
                vo.setId(employee.getId());
                vo.setRealName(employee.getRealName());
                vo.setEmployeeCode(employee.getEmployeeCode());
                
                // 设置部门信息
                DeptRespDTO dept = deptMap.get(Long.parseLong(employee.getDepartmentId()));
                if (dept != null) {
                    vo.setDepartmentName(dept.getName());
                }
                
                // 设置用户信息
                if (employee.getLinkUserId() != null) {
                    UserRespDTO user = userMap.get(Long.valueOf(employee.getLinkUserId()));
                    if (user != null) {
                        vo.setUsername(user.getUsername());
                        vo.setMobile(user.getMobile());
                    }
                }
                
                return vo;
            })
            .collect(Collectors.toList());
        
        return Result.success(new PageResult<>(result, employeePage.getTotal()));
    }
}

四、数据一致性保障

4.1 分布式事务问题

当HRM模块创建员工记录,需要同时更新系统模块的关联数据时,如何保证数据一致性?

方案一:最终一致性(推荐)

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void createEmployee(EmployeeSaveReqVO createReqVO) {
        // 1. 创建员工记录(本地事务)
        EmployeeDO employee = EmployeeConvert.INSTANCE.convert(createReqVO);
        employeeMapper.insert(employee);
        
        // 2. 发送消息通知其他模块
        EmployeeCreatedEvent event = new EmployeeCreatedEvent();
        event.setEmployeeId(employee.getId());
        event.setDepartmentId(employee.getDepartmentId());
        event.setLinkUserId(employee.getLinkUserId());
        
        rocketMQTemplate.syncSend("EMPLOYEE_CREATED_TOPIC", event);
    }
}

// 系统模块监听员工创建事件
@RocketMQMessageListener(
    topic = "EMPLOYEE_CREATED_TOPIC",
    consumerGroup = "system-employee-consumer"
)
@Component
public class EmployeeCreatedListener implements RocketMQListener<EmployeeCreatedEvent> {

    @Autowired
    private DeptService deptService;

    @Override
    public void onMessage(EmployeeCreatedEvent event) {
        // 更新部门统计信息
        deptService.incrementEmployeeCount(Long.parseLong(event.getDepartmentId()));
    }
}

方案二:Seata分布式事务

@GlobalTransactional(name = "create-employee-with-dept-update")
@Override
public void createEmployeeWithSeata(EmployeeSaveReqVO createReqVO) {
    // 1. 创建员工(HRM模块)
    employeeMapper.insert(employee);
    
    // 2. 更新部门统计(系统模块,跨服务调用)
    deptClient.updateEmployeeCount(createReqVO.getDepartmentId());
    
    // Seata自动协调两阶段提交
}

4.2 缓存一致性

@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @CacheEvict(value = "dept", key = "#dept.id")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateDept(DeptDO dept) {
        // 1. 更新数据库
        deptMapper.updateById(dept);
        
        // 2. 清除本地缓存
        redisTemplate.delete("dept:info:" + dept.getId());
        
        // 3. 发送缓存清除消息
        DeptUpdatedEvent event = new DeptUpdatedEvent();
        event.setDeptId(dept.getId());
        rocketMQTemplate.syncSend("DEPT_UPDATED_TOPIC", event);
    }
}

// 其他模块监听部门变更事件
@RocketMQMessageListener(
    topic = "DEPT_UPDATED_TOPIC",
    consumerGroup = "hrm-dept-consumer"
)
@Component
public class DeptUpdatedListener implements RocketMQListener<DeptUpdatedEvent> {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void onMessage(DeptUpdatedEvent event) {
        // 清除本地缓存
        redisTemplate.delete("dept:info:" + event.getDeptId());
    }
}

五、性能优化策略

5.1 调用链路优化

// ❌ 不推荐:循环调用
List<EmployeeDO> employees = employeeMapper.selectList(reqVO);
for (EmployeeDO employee : employees) {
    DeptRespDTO dept = deptClient.getDept(Long.parseLong(employee.getDepartmentId())); // N次调用
    employee.setDepartmentName(dept.getName());
}

// ✅ 推荐:批量调用
List<Long> deptIds = employees.stream()
    .map(e -> Long.parseLong(e.getDepartmentId()))
    .distinct()
    .collect(Collectors.toList());
Map<Long, DeptRespDTO> deptMap = deptClient.listByIds(deptIds).getData().stream()
    .collect(Collectors.toMap(DeptRespDTO::getId, Function.identity()));
employees.forEach(e -> {
    DeptRespDTO dept = deptMap.get(Long.parseLong(e.getDepartmentId()));
    e.setDepartmentName(dept.getName());
});

5.2 并发调用优化

// 使用CompletableFuture并发调用
public EmployeeDetailVO getEmployeeDetail(Long employeeId) {
    // 并发查询员工、部门、用户信息
    CompletableFuture<EmployeeDO> employeeFuture = CompletableFuture.supplyAsync(
        () -> employeeMapper.selectById(employeeId)
    );
    
    CompletableFuture<List<DeptRespDTO>> deptFuture = CompletableFuture.supplyAsync(
        () -> deptClient.listByIds(Arrays.asList(deptId)).getData()
    );
    
    CompletableFuture<UserRespDTO> userFuture = CompletableFuture.supplyAsync(
        () -> userClient.getById(userId).getData()
    );
    
    // 等待所有查询完成
    CompletableFuture.allOf(employeeFuture, deptFuture, userFuture).join();
    
    // 组装结果
    return assembleDetail(
        employeeFuture.join(),
        deptFuture.join(),
        userFuture.join()
    );
}

5.3 熔断降级配置

# application.yml
feign:
  sentinel:
    enabled: true

# Sentinel规则配置
@RestController
public class DeptRuleController {
    
    @GetMapping("/dept/rules")
    public void setDeptRules() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("DeptClient:listByIds");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setCount(100); // QPS限流
        rule.setStrategy(RuleConstant.STRATEGY_DIRECT);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

六、最佳实践总结

6.1 跨模块访问决策矩阵

是否需要实时数据?
├─ 是 → 是否涉及事务?
│      ├─ 是 → Seata分布式事务
│      └─ 否 → Feign同步调用
└─ 否 → 是否需要强一致性?
       ├─ 是 → 消息队列+最终一致性
       └─ 否 → 数据冗余+异步同步

6.2 性能优化检查清单

  • 是否使用批量调用减少网络往返
  • 是否添加Redis缓存减少数据库压力
  • 是否配置了熔断降级防止雪崩
  • 是否配置了合理的超时时间
  • 是否监控了调用量和耗时

6.3 监控指标

指标目标值说明
Feign调用成功率>99.9%服务可用性
Feign调用平均耗时<100ms性能指标
缓存命中率>90%缓存效果
消息消费延迟<1s最终一致性

更多推荐