点狮HRM基于Spring Cloud的HRM微服务架构设计与跨模块数据访问实践
·
一、微服务架构演进背景
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/JFinalOA | Gitee代码仓库 |
| Gitcode | 点狮HRM模块 https://gitcode.com/Glory_Lion/pointlion-HRM | HRM独立模块 |
| 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 | 最终一致性 |
更多推荐


所有评论(0)