ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

薪酬系统微服务重构实战:从单体到分布式架构的完整落地

薪酬系统微服务重构实战:从单体到分布式架构的完整落地 去年年底接手了一个传统制造企业的薪酬系统重构项目老系统是单体 Spring Boot JSP每个月工资核算报表要跑两个小时发薪高峰期接口经常超时人事、财务、IT 三拨人为数据对不上来回扯皮。我们最终用 Spring Boot Vue Spring Cloud 重新做了一套企业员工薪酬工资管理系统并且选择了微服务分布式路线。这篇博文会把当初的拆分思路、技术选型、落地过程以及踩过的坑全部写出来给正在做薪酬系统或准备微服务化的团队一个真实参考。很多人一听到“薪酬系统”第一反应是这么核心的业务也敢拆微服务我的答案是敢但必须想清楚边界。这套系统表面上只是算工资、发工资、出报表背后却牵扯组织人员、考勤、个税、社保、资金、审批、消息通知等多个业务域。如果继续在单体里堆代码每一个新需求都像在钢丝上跳舞。下面我从为什么拆、怎么拆、踩了什么坑三个层面展开。1. 为什么薪酬系统敢用微服务架构1.1 单体薪酬系统的三个痛点老系统最让人头疼的是算薪流程完全串行。每个月 15 号发薪日人事在白天导入考勤和绩效数据晚上触发批量计算第二早上财务才能看到工资单。一旦某个部门数据有误整个批处理重新跑CPU 跑满、数据库连接耗尽其他业务系统也跟着卡顿。这是典型的单体资源隔离问题一个慢任务拖垮了所有模块。第二个痛点是权限边界非常模糊。薪酬数据是敏感度极高的数据但老系统只有角色和菜单权限没有数据权限。换句话说只要有“薪资查询”菜单的人理论上能翻全公司任何人的工资条。后来我们加了部门维度过滤却只能靠 SQL 中大量if判断硬编码维护成本极高。第三个痛点是数据库单点。所有模块共用同一个 MySQL 实例月报统计、工资单查询、审批流全部打在同一张库上。大促式查询来了索引再优化也只是延缓问题。我们把用户量撑到 2 万以后一个简单的分页查询在发薪日都可能要三秒以上。说到底单体不是不能用而是当业务域之间需要独立扩缩容、独立权限控制、独立发布频率的时候它已经成了瓶颈。1.2 微服务拆分的边界与原则微服务不是拆得越碎越好尤其薪酬系统这类强一致、强审计的业务拆得过度会带来灾难。我们遵循了一个核心原则按“业务变化频率”和“数据安全边界”划分而不是按页面划分。最终拆出来的服务包括auth-service统一认证与鉴权org-service组织架构、员工档案、岗位职级attendance-service考勤结果汇总salary-engine-service薪酬规则配置与算薪引擎payroll-service工资单、发放批次、回盘数据finance-service资金冻结、代发账户管理approval-service审批流notification-service短信、站内信、邮件通知gateway-service统一入口网关这里强调一个关键点我们没有单独拆一个“报表服务”而是把报表功能放在了payroll-service里。因为报表强依赖工资单表结构拆出去只会增加跨服务聚合的复杂度。类似这种“看似独立、实际强耦合”的功能留在原服务反而更合理。每个服务独立数据库服务之间禁止直连数据库必须通过 API 调用。这一条规则在执行初期很痛但后期收益极大。比如后来考勤接口查询变慢我们只需要给attendance-service单独扩容完全不影响工资单查询。2. 核心模块拆解与数据模型设计2.1 用户与组织权限模块薪酬系统的权限设计必须同时具备“功能权限”和“数据权限”。我们在org-service里维护了部门、岗位、职级、员工状态等基础数据在auth-service里实现 RBAC 模型。用户登录后拿到的不只是菜单列表还有一个“数据可见范围”的表达式例如department_id IN (1001, 1002)。在实际项目里我们参考了若依微服务 plus 的权限模型用户、角色、菜单、部门数据权限四层。 角色不再直接关联用户而是关联“用户-部门-岗位”的组合这样人员调岗后权限自动失效不必手动重新授权。这里有个容易被忽视的细节薪酬的敏感操作查看他人工资条、调整薪资项、导出工资单必须记录操作日志。我们专门加了salary_access_log表记录操作人、操作时间、IP、请求参数摘要。审计日志表单独存放在payroll-service库里不能和业务表混在一起避免误操作被删掉。2.2 薪资核算模块设计工资项拆成三大类应发项、扣款项、专项附加项。每个员工通过salary_rule表关联自己适用的薪资规则规则里存放计算表达式和优先级。例如基本工资固定金额岗位工资固定金额绩效工资基本工资 × 绩效系数加班费小时工资 × 加班小时数 × 倍率社保公积金按基数比例计算个税累计预扣法计算我们在salary-engine-service中用 Groovy 脚本引擎解析计算规则。为什么不用 Java 硬编码因为财务每个月都可能调整扣款项和补贴项硬编码意味着每个调整都要走一次发版。Groovy 脚本存储在数据库里修改后可以即时生效同时配合规则版本号做回溯。计算精度必须用BigDecimal入库金额四舍五入保留两位小数中间计算结果保留四位小数。这里我举一个真实计算过程某员工基本工资 8000绩效系数 1.2交通补贴 500社保公积金个人缴纳部分 1680无专项附加扣除。应发金额 8000 8000 × 1.2 500 18100个税按累计预扣法计算假设累计应纳税所得额 21000对应税率 3%个税 630。实发金额 18100 - 1680 - 630 15790。 实际代码中我们绝不会用浮点数直接相乘否则 0.1 0.2 这类精度问题会让工资条对不上账。2.3 分布式事务跨服务扣款与发薪发薪场景是分布式事务的高发区。算薪完成后系统需要同时做三件事冻结/扣减资金账户余额、生成工资单、记录发放流水。这三个操作分别落在finance-service、payroll-service、notification-service上没法用本地事务一把梭。我们最终引入 Seata 的 AT 模式但并不是所有接口都用全局事务而是只保护资金变更这一条核心链路。发薪接口的执行过程大致是salary-engine-service生成工资单数据写入salary_statementpayroll-service创建发放批次状态置为PENDINGfinance-service冻结对应资金账户余额三个服务都成功后全局事务提交批次状态变成COMPLETED如果任何一步失败AT 模式自动反向补偿 SQL回滚已写入的数据这里提醒一句不要把工资单 PDF 生成、邮件通知、短信发送放入同一个全局事务。这些非核心操作占用时间长会让事务锁持有时长直线上升。正确做法是事务只管业务状态变更事务提交后再发 MQ 消息让notification-service异步处理通知。哪怕通知失败只需要定时任务补偿不影响发薪结果。2.4 关键表结构设计数据模型是整个系统最容易翻车的地方。我贴几张核心表的简化结构大家可以直接作为落地参考。salary_staff_account员工薪酬档案表CREATE TABLE salary_staff_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, staff_id BIGINT NOT NULL COMMENT 员工ID, employee_no VARCHAR(32) NOT NULL COMMENT 工号, bank_account VARCHAR(64) NOT NULL COMMENT 银行卡号, base_salary DECIMAL(12,2) NOT NULL COMMENT 基本工资, post_salary DECIMAL(12,2) DEFAULT 0 COMMENT 岗位工资, social_security_base DECIMAL(12,2) DEFAULT 0 COMMENT 社保基数, housing_fund_base DECIMAL(12,2) DEFAULT 0 COMMENT 公积金基数, account_status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效 0停用, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_staff_id (staff_id), KEY idx_employee_no (employee_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工薪酬档案表;salary_statement工资单表CREATE TABLE salary_statement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_id BIGINT NOT NULL COMMENT 批次ID, staff_id BIGINT NOT NULL, statement_month CHAR(7) NOT NULL COMMENT 账期如2026-06, payable_amount DECIMAL(12,2) NOT NULL COMMENT 应发金额, social_security DECIMAL(12,2) NOT NULL COMMENT 社保个人部分, housing_fund DECIMAL(12,2) NOT NULL COMMENT 公积金个人部分, income_tax DECIMAL(12,2) NOT NULL COMMENT 个税, actual_amount DECIMAL(12,2) NOT NULL COMMENT 实发金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未发放 1已发放 2已回盘, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_staff_month (staff_id, statement_month), KEY idx_batch_id (batch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工资单表;salary_payment_log发放流水表是幂等保障的关键CREATE TABLE salary_payment_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, payment_no VARCHAR(64) NOT NULL COMMENT 发放流水号, batch_id BIGINT NOT NULL, staff_id BIGINT NOT NULL, statement_month CHAR(7) NOT NULL, actual_amount DECIMAL(12,2) NOT NULL, payment_status TINYINT NOT NULL DEFAULT 0 COMMENT 0处理中 1成功 2失败, retry_count INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_payment_no (payment_no), UNIQUE KEY uk_batch_staff (batch_id, staff_id), KEY idx_staff_month (staff_id, statement_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT薪资发放流水表;唯一索引uk_batch_staff是最后一道防线就算代码中 Redis 锁失效、重试机制抽风数据库层面也能挡住重复发放。后面我在“高频问题”部分还会展开讲这个坑。3. 技术选型与基础设施落地3.1 注册中心、网关与配置中心技术栈选型不能只追新还要考虑团队运维能力。我们最终选了 Spring Boot 3.2 Spring Cloud 2023.0.x Spring Cloud Alibaba 2023.0.x注册中心和配置中心都用 Nacos网关用 Spring Cloud Gateway服务间调用用 OpenFeign。Nacos 对比 Eureka 最大的优势是自带配置中心可以动态修改线程池参数、开关配置不用额外部署 Config Server。比如算薪引擎在发薪高峰可能需要临时调整批量大小直接在 Nacos 上改配置应用无需重启。这个能力在薪酬系统里非常实用因为发薪日业务几乎不能停机。网关层我们做了三件事路由转发、统一鉴权、灰度发布。初始版本没有接入 Sentinel 限流结果发薪日流量一上来网关线程池被打满我们才意识到网关也需要限流降级。后来通过 Sentinel 对/payroll/pay这类关键路径配置了 QPS 限流超过阈值直接返回“系统繁忙”避免把下游服务拖垮。3.2 认证鉴权Spring Security JWT OAuth2认证这块我们单独做了auth-service没有在网关里做复杂逻辑。整体流程是用户通过用户名密码登录auth-service校验通过后签发 JWTJWT 中放入user_id、dept_ids、role_codes请求经过网关时GlobalFilter解析 JWT 并校验签名校验通过后Header 中加入X-User-Id、X-Dept-Id等内部信任头再转发到下游服务下游服务只在从网关进来的请求里读取用户信息对于直接暴露的接口必须二次校验签名JWT 的有效期我们设置得比较短默认 30 分钟配合 refresh token 刷新。为什么不设置 7 天因为员工离职、调岗后权限变化必须尽快生效JWT 一旦签发有效期太长旧 token 还能继续访问这是薪酬系统不能接受的安全隐患。 refresh token 刷新时会检查用户当前是否还有效如果员工已离职直接拒绝刷新。3.3 分布式锁与幂等控制发薪、审批、批量导入这类操作必须防止并发执行。单体时代用synchronized只能锁单机微服务部署多个实例后必须用分布式锁。我们用的 Redis 分布式锁具体实现基于 RedissonRLock lock redissonClient.getLock(salary:pay: batchId); boolean locked lock.tryLock(5, 30, TimeUnit.SECONDS); if (!locked) { throw new BizException(当前发放批次正在处理中请勿重复操作); } try { // 执行业务逻辑更新批处理状态 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }Redisson 自带看门狗机制默认会把锁持有时间自动续期避免业务执行时间过长导致锁自动过期。 这里特别强调锁的 key 必须带业务维度比如salary:pay:{batchId}不能全局一把锁。否则不同批次之间会互相阻塞严重降低发薪效率。分布式锁解决的是并发问题但不是幂等的全部。接口层还要做幂等校验比如前端在提交付款时生成一个paymentNo后端用这个唯一流水号判断是否已经处理过。Redis 里加一个SETNX paymentNo 1 EX 24h作为第一层过滤数据库唯一索引作为兜底。3.4 文件存储与消息队列工资条 PDF、社保缴纳明细、银行回盘文件等附件不适合直接放 MySQLBLOB 查询会拖垮数据库性能。最开始我图省事用 FastDFS结果配置复杂、社区维护进度慢后来直接换成了 MinIO。MinIO 部署成本低支持 S3 协议和 Spring Boot 集成非常顺。文件上传路径我们默认不走网关因为工资单批次可能一次性上传几千个 PDF网关作为同步转发点容易成为瓶颈。正确做法是后端生成预签名 URL前端直传 MinIO上传完成后回调后端登记文件元数据。这样既保证了传输速度又不会让网关长时间被连接占用。消息队列用了 RocketMQ主要场景包括发薪完成通知、工资单生成后的回调、报表导出的异步任务。RocketMQ 的事务消息在这里也很合适比如“创建发放批次”和“发送 MQ 消息”需要保持一致可以用事务消息把本地事务和消息发送绑定在一起。不过我的经验是事务消息虽好但不要滥用大多数场景用普通可靠消息 定时任务对账就够了。4. 前端Vue实现与联调实战4.1 Vue3 Vite Element Plus 工程化搭建前端我们没有用老一套 Vue2 Webpack而是选了 Vue3 Vite Element Plus Pinia TypeScript。Vite 的冷启动速度对开发体验提升非常明显改一行代码保存后页面几乎秒级刷新这在天天调整表格和表单的薪酬管理后台里能省下大量等待时间。工程目录采用按业务模块划分的方式src/ api/ # 所有接口请求 assets/ components/ layouts/ # 布局组件 router/ # 动态路由 store/ # Pinia views/ auth/ dashboard/ payroll/ report/ system/ utils/环境配置上.env.development里设置VITE_API_BASE_URL/api然后在vite.config.ts里配置代理到本地网关server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }之所以开发环境用代理而不是直接填http://localhost:8080是为了避开跨域问题同时让代码里的请求路径保持统一。生产环境由 Nginx 把/api反向代理到网关前端代码不需要区分环境。4.2 动态路由、权限指令与数据字典薪酬系统的菜单权限不是静态写死的不同角色登录后看到的侧边栏完全不同。我们通过登录接口返回菜单树然后前端动态添加路由router.addRoute({ path: item.path, name: item.name, component: () import(../views/${item.component}.vue), meta: { title: item.title, icon: item.icon } });菜单控制只是一部分按钮级权限同样重要。比如“发放工资”按钮只有财务主管角色能看到普通财务人员登录后按钮直接消失。我们用自定义指令v-permission实现app.directive(permission, { mounted(el, binding) { const requiredPerms binding.value; const userPerms useUserStore().perms; const hasPermission userPerms.some(p requiredPerms.includes(p)); if (!hasPermission) { el.parentNode?.removeChild(el); } } });注意一点前端权限只能提升交互体验不能作为安全边界。后端接口必须再做一次权限校验否则绕过前端工具直接调用接口敏感数据照样泄露。这个理念我在团队里强调了很多次。数据字典也是薪酬系统的刚需。银行列表、工资项类型、发放渠道、员工状态等都用字典管理前端写成通用Select组件不再一页页重复拉表单枚举值。我们用了简单的静态字典配置 后端接口刷新缓存没有引入很重的字典服务够用就好。4.3 前后端联调跨域、网关与Mock数据微服务架构下前端只面向网关一个入口不要直接面对面访问各服务地址。联调阶段经常出现的状况是前端说接口 404后端说服务正常最后发现是路由路径没匹配上。我们在网关里把/api/auth/**路由到auth-service把/api/payroll/**路由到payroll-service前缀必须统一不然排查起来非常痛苦。联调过程中后端接口还没写完我们用 Mock 数据并行开发。这里不推荐在真实代码里写死if (mock) return xxx而是启动一个独立的 Mock Server比如用json-server或Mock.js的独立 dev server。开发环境通过 Vite 代理切换到 Mock Server等后端接口就绪只改一行代理地址即可。 前后端约定好接口字段命名规范用camelCase避免一个接口一个命名风格。另外工资单列表页通常要做金额区间筛选可能会用到防抖和分页缓存。我们踩过一个坑Element Plus 表格在切换筛选条件时查询参数里的pageNo没有重置为 1导致用户在第一页筛选后跳到了其他页数据看着像“凭空消失”。代码里统一在筛选条件变化时重置页码这个问题就解决了。系统里的操作演示视频我们顺手接了一个支持 m3u8 的播放组件放在帮助中心给业务人员看培训录像。这不是核心功能但对新手人事专员来说比厚厚的手册有用得多。5. 分布式部署与运维监控5.1 容器化部署Docker Compose 到 K8s微服务落地后部署复杂度直线上升。我们前期没有直接上 K8s而是先用 Docker Compose 编排Nacos、MySQL、Redis、MinIO、RocketMQ 都作为基础设施容器化运行业务服务也用 Docker 镜像跑在独立容器里。每新增一个服务就写一个Dockerfile用 Maven 构建出可执行 jar再用多阶段构建精简镜像FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]为什么没用 K8s因为运维团队初期只有两个人K8s 的复杂度反而会拖慢节奏。Docker Compose 解决多服务编排已经够用后端服务挂了可以在编排层自动重启。等并发量上来、需要弹缩容时再平滑迁移到 K8s配合 HPA 自动扩缩容。5.2 日志聚合与链路追踪微服务排错最怕的是什么一个请求从网关进入调用auth-service、payroll-service、finance-service某一步慢了却不知道慢在哪里。所以我们从第一天就上了链路追踪选择了 SkyWalking它有开箱即用的 UI对 Spring Cloud 支持很好不需要侵入式改代码。每个服务在日志中必须携带traceId和userId。我们用 logback 配置在日志输出中自动带上 MDC 里的参数pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%traceId] [%userId] %logger{36} - %msg%n/pattern这样在排查问题时可以按照traceId把同一请求在网关和各服务之间的日志全部串起来。日志文件统一输出到挂载目录再用 Filebeat 采集到 Elasticsearch最终在 Kibana 里按关键字搜索。没有这套东西微服务查一次错可能要翻三个服务日志效率极低。5.3 压测与容量预估上线前我们对核心链路做了压测。压测目标很明确模拟发薪日 2 万员工的算薪批量操作以及同时在线 500 人的日常查询操作。日常查询链路我们用 JMeter 并发 100 线程循环跑工资单分页、员工查询、部门薪酬汇总。压测发现4 核 8G 实例部署一个payroll-service加 MySQL大概能支撑 300 QPS平均响应 380ms。但算薪引擎是 CPU 密集型任务涉及大量 BigDecimal 运算同样规格的实例单独跑 2 万员工批次只要 12 分钟如果和其他服务混布在同一台机器上响应时间会恶化到秒级。 所以最终把salary-engine-service独立部署并对 JVM 堆内存做了充分配置避免在批量算薪时触发频繁 Full GC。压测还发现 Nacos 注册中心在服务实例反复注册注销时如果堆内存过小CPU 会飙升。解决方法一是给 Nacos 足够的堆内存二是把服务心跳刷新时间调大比如默认 5 秒改成 10 秒减少注册中心的压力。这些细节不压测根本发现不了。6. 实战中高频问题与排查技巧6.1 一不小心重复发薪幂等设计这是开发过程中最惊险的一次事故。测试环境做发薪演练时财务点了一次“确认发放”前端按钮没做 loading 限制连点了三下。后端在没上分布式锁和唯一索引之前数据库里生成了三条发放流水银行回盘文件金额直接翻了三倍。后来我们复盘加了四层防护前端按钮在请求期间禁用防止误操作后端接口用同一个paymentNo做 Redis 幂等判断Redisson 分布式锁保护同一批次的并发处理数据库salary_payment_log表加uk_batch_staff唯一索引四层防护层层设卡最坏情况也只是第二次请求报错而不会多扣一笔钱。 这里要特别提醒业务逻辑里千万别依赖“先查询再判断状态”的简单判断因为并发请求可能同时查询到同一状态必须在写入时用唯一索引或悲观锁兜底。6.2 Excel导出撑爆内存薪酬报表导出是一个非常典型的坑。第一次导出 5 万条工资明细时直接用 Apache POI 的XSSFWorkbook程序直接 OOM服务重启后连正常登录都变慢了。原因是XSSFWorkbook会把所有格子对象都创建并常驻内存数据量大时极其消耗堆内存。后来改用SXSSFWorkbook它是流式版本可以一边写入一边滑动窗口刷新SXSSFWorkbook workbook new SXSSFWorkbook(1000); Sheet sheet workbook.createSheet(工资明细); int rowNo 0; for (SalaryStatement stmt : pageData) { Row row sheet.createRow(rowNo); row.createCell(0).setCellValue(stmt.getStaffId()); row.createCell(1).setCellValue(stmt.getActualAmount().doubleValue()); if (rowNo % 5000 0) { // 触发窗口刷新释放旧行 } }同时导出操作从同步接口改成后台异步任务前端提交导出请求后立即返回“任务已提交”后台分页查询数据库每 5000 条刷新一次临时文件最终把临时文件合并后上传到 MinIO再通过 WebSocket 通知前端下载链接。这样即使导出 20 万条数据也不会对在线查询产生影响。6.3 网关超时与文件上传限制Spring Cloud Gateway 默认对下游请求的响应时间有限制而发薪批次接口因为要执行全局事务可能超过 30 秒。最初上线时前端等不到结果直接报网关超时但后端事务还在跑财务不确定到底发没发非常尴尬。解决方案不是简单调大网关超时时间而是把长任务改为异步。发薪接口收到请求后立刻写入一个批次任务状态为PENDING同步返回“任务已提交”。后续执行结果通过 WebSocket 或轮询接口反馈。这样网关只承担短请求超时风险大大降低。文件上传也有类似问题。工资单附件如果走网关大文件传输会占用网关线程。我们改用预签名 URL 直传 MinIO 后网关连文件内容都不经手压力小了不少。再配合 Nginx 层设置client_max_body_size才真正把上传链路做稳。上传 PDF 时我们还增加了全局过滤器解析 PDF 内容并检查是否存在script等危险片段防止恶意文件被当工资条下发。6.4 分布式事务补偿表设计Seata 的 AT 模式能解决大多数字段级回滚但并不代表我们可以完全不写补偿代码。一旦全局事务分支超时或 TC 重启可能会出现悬空事务。我们专门建了一张compensation_record表CREATE TABLE compensation_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, transaction_id VARCHAR(64) NOT NULL, service_name VARCHAR(64) NOT NULL, data_type VARCHAR(32) NOT NULL, data_id BIGINT NOT NULL, action_type VARCHAR(16) NOT NULL COMMENT CREATE/UPDATE/DELETE, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待补偿 1已补偿 2失败, retry_count INT NOT NULL DEFAULT 0, last_error TEXT, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_transaction_data (transaction_id, service_name, data_type, data_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分布式事务补偿表;定时任务每两分钟扫描status 0的记录按action_type做补偿操作。补偿逻辑同样要具备幂等性例如补偿“冻结资金失败”会先查当前资金流水是否已存在存在则直接改成成功状态不作重复扣减。这张表上线后我们才真正敢说分布式事务是可控的。最后再说一个个人习惯薪酬系统上线前不管时间多紧一定要做一次完整的“发薪演练”拿整月真实数据跑一遍算薪、审批、资金冻结、生成回盘文件的流程。演练中发现的每一处数据差异都要追溯到根因并记录在案。我在这个项目里最大的体会是薪酬系统的技术难点不在某个框架的 API而在你面对数字不一致时能否快速定位是精度问题、事务问题、还是幂等问题。这套排查能力比会写多少行代码都重要。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表