金融撮合系统后端实践:借款与投资业务的状态机与并发控制

发布时间:2026/10/14 11:49:59
金融撮合系统后端实践:借款与投资业务的状态机与并发控制 写金融系统最绕不开的模块就是“撮合”两个字。我最近在做一个模拟金融撮合平台的后端重构项目推进到第三阶段时核心任务变成了发布借款业务和投资业务。说白了就是把借款人的需求包装成可交易的标的再让投资人在另一端看到、投出去整个过程涉及大量的状态机设计、资金流水处理和并发控制。这篇博文记录的就是这个阶段的完整实践。我先梳理一下项目背景整个系统不是真实的金融机构而是一个教学与实际开发结合的练兵项目目标是还原一个线上信用借贷撮合平台。所谓“发布借款业务和投资业务”拆开看其实是两条产品线一条服务借款人一条服务投资人。但底层要处理的数据关系和资金逻辑是同一套。这套业务值得分享的地方在于它几乎覆盖了金融交易系统里最典型的三类问题——状态流转、并发限额、资金对账。你把这套逻辑吃透了后面做积分系统、订单系统、账户系统都会觉得顺很多。这个阶段适合谁来参考如果你是刚转后端不久、想了解资金撮合类系统怎么设计或者你已经写过常规CRUD想看看金融场景下事务和锁到底怎么用又或者你只是想找一个项目实战的样板这篇文章都能给你一些可以直接抄走的思路。1. 业务线拆解借款端与投资端的关系模型1.1 闭环第一步标的是怎么来的要理解借款和投资的关系你先得抓住一个核心概念标的就是一段被标准化了的借款需求。借款人不会直接把“我缺钱”这件事展示给投资人系统要做的是把借款金额、年化利率、还款期限、还款方式这些散落的信息组装成一个投资人看得懂、算得清、敢下手的产品。我举个具体例子。一个借款人提交申请金额10万元、期限12个月、年化利率9.6%、还款方式等额本息并且打算每个月15日还款。系统拿到这些信息后并不会立刻把它展示出来而是先进入审核和风控状态。只有通过了审核这条借款申请才会被打上“可投”标签变成标的发布到投资端列表。所以你在做数据建模时借款申请和标的是两个独立实体而不是一个实体。我在项目里把这两者分开落库理由很直接申请信息在审核前后会多次修改标的一旦发布其利率、金额、期限都应该被冻结不能跟着借款人的后续操作变来变去。这个分离设计在后续做对账和审计时帮了大忙。1.2 标的状态机全流程只有六种状态状态机是这类系统的第一个设计难点。我梳理过市面上一批同类系统的文档发现不管怎么命名核心状态基本逃不出这六个DRAFT草稿、PUBLISHED投资中、FULL满标、REPAYING还款中、CLEARED已结清、VOID流标。我建议所有团队都先用这六个状态把主干串起来再考虑加“部分流标”“提前结清”这类分支状态。一个容易踩坑的地方是状态的流转方向比状态本身更重要。比如只有满标的标才能流标打回草稿是禁止的只有投资中的标才能被提前终止。如果你在代码里不做方向校验很容易出现“草稿标直接变成已结清”这种逻辑漏洞。我在项目里专门写了一个StateMachine辅助类把合法的迁移路径全部显式声明出来任何不在路径上的迁移直接抛异常。1.3 投资端看到的东西其实是统计结果投资端的产品列表页看起来很简单无非是几列数据借款编号、年化利率、期限、可投金额。但这些列里面真正需要查表的其实只有借款编号和年化利率“可投金额”是一个动态计算字段它等于标的总额减去所有有效投资订单的金额总和。这个点看起来不起眼却是后来并发问题的源头。因为可投金额不是静态存在的列如果你每次查询都现算一旦多个投资人同时投标就会产生“看到的余额明明够提交时却提示额度不足”的现象。解决思路上我会在后文详细讲但先记住一个原则所有涉及钱和剩余额度的更新永远不要基于“查询后再更新”的朴素逻辑一定要走数据库层面的原子操作或行锁。2. 借款业务发布从申请到上架的完整路径2.1 借款申请表的核心字段设计我先把借款申请表的字段列出来这张表是借款人进入系统的第一站。实际项目中字段还可以拆得更细但核心骨架大概是字段名类型说明apply_idbigint申请主键user_idbigint借款人用户IDamountdecimal(18,2)申请借款金额ratedecimal(5,2)年化利率百分比termint借款期限月repay_typetinyint还款方式1等额本息 2先息后本 3一次性还本付息statustinyint申请状态audit_timedatetime审核时间create_timedatetime创建时间这里面有两个我特别想提醒的字段。第一个是amount坚决不能用double或者float去存金融场景必须用decimal(18,2)或者BigDecimal。这个坑我在后文会专门说。第二个是repay_type它是生成还款计划的依据不同的还款方式计算逻辑完全不同你在初始化还款计划时一定要先把它读出来做分支处理。2.2 借款审核的规则落库审核过程在演示项目里可以简化成三步资料齐全性校验、黑名单校验、额度规则校验。每一步我都是用一个可插拔的校验器实现的这样后续风控部门要加新规则只要在规则链上追加一个类就行不会动到主流程。审核结果最终会体现在申请状态上通过PASS、拒绝REJECT、人工复审MANUAL。我这里的处理是PASS之后才允许创建标的REJECT直接结束流程MANUAL则进入后台任务队列等待人工处理。需要提醒的是审核通过不等于标的一定能发出去。我在项目里还加了一道“标的池”限制逻辑——同一时间段允许在投的标的数量不能超过阈值。这个限制不是为了防并发而是为了控制资金端和资产端的节奏避免出现一堆小额标把系统打满真正的大额资产反而排不上队的问题。2.3 发布借款标的的实操步骤发布借款业务在代码层面可以分为两步创建标的、标记发布。创建标的就是把申请信息拷贝过来生成一个唯一的subject_no然后初始化金额相关的字段total_amount、remain_amount两个值此时相等status DRAFT。标记发布则是把状态从DRAFT改成PUBLISHED并写入发布时间。我画一条精确的SQL更新语句给你看这是后面所有更新操作的模板UPDATE loan_subject SET status PUBLISHED, publish_time NOW(), version version 1 WHERE subject_id ? AND status DRAFT注意WHERE里把当前状态带上了。这是一个非常关键的技巧它保证了状态的更新是条件化的如果这条SQL执行后影响行数为0说明标的已经不是草稿状态此时必须抛异常回滚让上层重新查询最新状态。我后来处理所有涉及钱的状态变更都用了这个模式它比先查再判的写法节省了无数并发隐患。3. 投资业务发布标的上架后的那些事3.1 标的上架后的展示与筛选当标的进入PUBLISHED状态投资端的列表查询就开始命中它了。这里有一个性能细节投资列表页需要展示“可投金额”如果每次查询都对每个标做一次SUM聚合列表超过20条时接口耗时就会明显上升。我的处理方案是双写loan_subject表保存remain_amount字段同时维护一张invest_order表。每次投标成功事务内同时更新这两处。查询列表时直接读remain_amount极端情况下即使这个字段和实际订单总和对不上也可以用定时对账任务去修正。这种用空间换时间的做法在金融系统里的名字叫冗余可查字段很常见。筛选逻辑方面我按投资人最常用的筛选项做了索引状态必填、利率区间、期限、发布时间。前两个字段建了联合索引发布时间的排序用二级索引解决。我这个阶段的列表页压测单表100万条数据的情况下响应延迟能稳定在80毫秒以内算是比较合格的水平。3.2 投标并发控制必须讲清楚的两把锁投标这个动作是整个系统并发压力最集中的地方。同一个标的可能同时有成百上千个投资人在操作如果处理不好“打款成功但标已经满”的尴尬场景后面对账会非常痛苦。我先讲第一种方案乐观锁。在loan_subject表上加一个version字段投标时执行这样的SQLUPDATE loan_subject SET remain_amount remain_amount - ? , version version 1 WHERE subject_id ? AND remain_amount ?如果影响行数为1说明更新成功如果为0说明剩余额度不足或者version冲突此时应该抛“额度不足或已满标”的错误。这个方案实现简单但有一个问题在高并发下如果同一个标的被大量命中大量请求会做无谓的失败重试浪费数据库连接。第二种方案是行级锁。投标事务开始时先执行一条SELECT ... FOR UPDATE把标的行为锁住后续步骤都基于锁定的最新值操作事务提交后再释放锁。这种方案更稳但因为锁的粒度大同一标的的并发能力会被限制在很低的水平。我在这个项目里最终选了第二种方案理由很务实金融标的的数量天然受限一个平台同时在投的标的也就几十个到几百个每个标的的投标并发一般不会超过几十笔/秒这个量级下行锁完全应付得过来而且逻辑最不容易出错。如果你做的是抢购、秒杀这种超高并发的场景再考虑乐观锁消息队列削峰。3.3 还款计划的生成逻辑还款计划是投资业务能不能落地的关键因为它直接决定了投资人能看到多少收益。我以最常见的等额本息为例把计算的过程完整讲清楚。等额本息的月还款额公式是[ M P \times \frac{r(1r)^n}{(1r)^n - 1} ]其中P是本金r是月利率年化利率除以12n是还款期数。每月还款额相同但利息和本金的比例逐月变化。我在代码里没用浮点数直接算而是每一步都用BigDecimal的divide(mc, RoundingMode.HALF_UP)方法精度设置在8位小数最后一期用“总额减已还金额”的方式反推确保每一分钱都对得上。顺带提一句还款计划的明细表一定要包含current_period、due_date、principal、interest、total_due、status这几个核心字段。后面做还款提醒和逾期罚息的时候这些都是最基础的引用数据。4. 撮合引擎与资金流水核心代码怎么落4.1 两张必建的表投资订单与资金流水这一节我直接给你看建表SQL这两张表是投资业务的核心。CREATE TABLE invest_order ( order_id bigint PRIMARY KEY AUTO_INCREMENT, subject_id bigint NOT NULL, user_id bigint NOT NULL, invest_amount decimal(18,2) NOT NULL, status tinyint NOT NULL DEFAULT 0, create_time datetime NOT NULL, KEY idx_subject_status (subject_id, status) ); CREATE TABLE capital_flow ( flow_id bigint PRIMARY KEY AUTO_INCREMENT, user_id bigint NOT NULL, account_type tinyint NOT NULL COMMENT 1可用余额 2冻结余额 3待收本息, change_amount decimal(18,2) NOT NULL, balance_after decimal(18,2) NOT NULL, biz_type varchar(32) NOT NULL, biz_id varchar(64) NOT NULL, create_time datetime NOT NULL, KEY idx_user_time (user_id, create_time) );资金流水表的细节值得展开说一下。balance_after字段非常关键它存储的是这笔流水发生后的账户余额。有了它你可以随时根据流水表做全量对账而不用依赖账户表的当前值。这在对账出现差异时能快速定位是哪笔交易出了问题我在项目里就用这个字段排查出过一次重复写入的Bug。4.2 投标核心事务的代码逻辑投标动作我建议放在一个事务方法里完成大致代码如下以Java和Spring为例Transactional(rollbackFor Exception.class) public void invest(InvestRequest request) { // 1. 行锁锁定标的 LoanSubject subject loanSubjectMapper.selectByIdForUpdate(request.getSubjectId()); if (subject null || !PUBLISHED.equals(subject.getStatus())) { throw new BizException(标的不可投); } if (subject.getRemainAmount().compareTo(request.getAmount()) 0) { throw new BizException(剩余额度不足); } // 2. 扣减剩余额度 loanSubjectMapper.deductRemainAmount(request.getSubjectId(), request.getAmount()); // 3. 创建投资订单 InvestOrder order new InvestOrder(); order.setSubjectId(request.getSubjectId()); order.setUserId(request.getUserId()); order.setInvestAmount(request.getAmount()); order.setStatus(OrderStatus.SUCCESS); investOrderMapper.insert(order); // 4. 生成资金流水冻结投资人可用余额 capitalFlowMapper.insert(request.getUserId(), ACCOUNT_FREEZE, request.getAmount().negate(), accountService.queryAvailable(request.getUserId()), INVEST_FREEZE, order.getOrderId()); // 5. 判断是否满标满标则进入下一步处理 if (subject.getRemainAmount().subtract(request.getAmount()).compareTo(BigDecimal.ZERO) 0) { loanSubjectMapper.updateStatus(request.getSubjectId(), FULL); } }这里每一步我都只做一件事事务回滚时才能明确。尤其第4步生成资金流水这步如果漏了后面还本付息时账根本对不上。我建议你每一步的流水与业务操作放在同一个事务里不要采用“先更新业务表再异步记账”的方案异步记账适合百万级日单量的大型系统我们的体量用同步事务更稳。4.3 满标后的放款事务满标之后并不是直接给借款人打钱通常还有一道放款确认确认前用户可以发起“流标”。流标意味着所有投资人的冻结资金要解冻退回。放款事务我同样是同步完成的先更新标的为REPAYING再给借款人账户增加可用余额同时生成放款流水。这里我额外强调一点放款时不建议直接调用外部转账接口而是先做内部记账记完账后进入待支付队列由支付模块异步处理。这样做的原因是外部渠道可能失败如果同步阻塞整个事务容易把数据库事务拉得特别长极大影响系统吞吐。我踩过一次坑是放款回调重复通知导致账户余额加了两次。后来在流水的biz_id上建了唯一索引重复回调直接插入失败问题就消失了。这个经验可以类推到所有回调、通知类的业务处理上幂等处理的核心就是唯一索引加状态检查二者缺一不可。5. 故障复盘真实踩过的坑与排查方法5.1 并发投标导致的超额问题这是这类系统最经典的故障。我最早写完投标接口后做了50并发压测结果发现一个总额10万元的标最终被投出了12万。原因很直接多个请求同时查到了remain_amount50000都认为自己能投40000然后在更新时都没有限制条件最后一共扣了12万。后来修复方案就是前面说的更新语句把remain_amount ?作为WHERE条件带上这样错误的更新直接影响行数为0事务自然失败。这件事也让我养成了一个习惯凡是涉及“扣减”的SQL必须带上“扣减后不能为负”的条件。5.2 金额精度丢失我接手时发现代码里有人用Double保存利率然后用它做了一堆乘法账面上看起来差别不大但累计下来有几块钱的误差。这类问题在金融系统里的严重性在于投资人随时可能拿计算器算收益对不上就是投诉。排查方法很简单把所有涉及金额、利率的字段改成BigDecimal并且统一RoundingMode.HALF_UP。这里我建议在数据库层用decimal(18,4)存储展示层再四舍五入到2位可以最大限度减少累计误差。5.3 状态机错乱草稿标直接跳到还款中有一次测试时发现一笔借款申请还没审核通过后台却已经生成了放款记录。最后定位是代码里有个定时任务直接根据申请表违规把状态改成了REPAYING跳过了中间的发布和满标环节。修复办法是引入状态机迁移校验所有状态更新都经过统一入口入口内查一次合法迁移表。这个校验逻辑不复杂但我认为每个金融项目都必须有否则一次错误的状态更新就可能造成资金流水错乱而复盘时要花好几倍时间才能把账理清。5.4 我的排查工具与方法我平时排查这类型问题第一步永远是拉流水。流水表记录了每一次余额变化你可以从某笔异常交易开始把同一用户、同一时段的所有余额变化按时间顺序串起来几乎一眼就能看到问题出在哪里。第二步是开慢查询日志。在MySQL里把慢查询阈值设成1秒压测后去看哪些SQL耗时长十有八九就是没走索引或者是锁等待。第三步才是查应用日志不要一开始就钻进日志里那样效率太低。6. 上线前的压测要点与我的实操体会6.1 压测清单我把这类系统上线前必做的压测项列成一个清单方便你直接照抄检查项预期指标说明投标接口并发压测不出现超卖、不出现金额错误至少压到正常值3倍并发满标触发成功率100%满标后应立刻触发状态变更放款回调幂等重复5次不重复入账唯一索引必须生效列表页响应延迟100毫秒内超过则检查索引除此之外建议再做一个“恢复测试”人为制造数据库连接池耗尽观察系统是否能自动恢复。这个测试很多团队不做但线上出问题时你大概率是先体验到连接池异常才会发现问题根源。6.2 一点实际体会写到这里我大致给出了这套借款和投资业务从模型设计到发布上线的主要环节。整个过程中我最大的感受是所谓金融项目实战真正难的不是某个算法或者某个高大上的组件而是那些看起来很朴素的工程细节状态有没有被正确约束、流水有没有被完整记录、并发更新有没有被精确限制。把这些细节做扎实了比引入再多的中间件都管用。如果你也在做类似的资金撮合类系统我建议你把这篇文章里提到的状态机校验、条件更新SQL、资金流水三张表先建齐再从借款发布跑通到投标满标中间不要图快跳步骤。你亲自动手搭一遍比看十篇讲解都更有收获。这个项目下一步我会继续做还款提醒和逾期罚息相关模块整套账务逻辑还会继续复杂下去。到那个阶段如果有新的体会我再单独写一篇分享出来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询