Skip to content

授信系统设计 ​

定位:信贷域系统设计层第一篇。回答"一个人凭什么能借钱、能借多少、额度怎么不被并发打穿"。授信系统把风控的一次决策变成一份有有效期的合同和一个可被占用的额度池。并守住 used + frozen ≤ total 的宪法。业务概念与领域模型见信贷业务全景与领域模型,决策引擎内部构造见风控决策架构。

一、需求边界:授信与支用的职责切分 ​

授信和支用是两个系统、两次决策,切分的标准是决策成本与触发频率完全不对称:

维度授信(开池子)支用(用水)
触发频率低频(一人一次,循环额度数月复评)高频(随借随还,日千万级)
决策方风控全量决策:规则+模型+人工风控交易复检:轻量规则+黑名单
产出授信合同 + 额度账户(池)借据(池的一次占用)
时延与失败代价P99 < 1s,拒了可以再来(非时效敏感)P99 < 200ms(在支付链路里),拒绝伤交易成功率

三条职责边界纪律:

  • 授信不知道借据:授信只管"池子开多大、定价多少、何时到期",借据是支用域的事。授信系统里不存在借据表;
  • 支用不重跑授信:支用只做额度校验 + 交易级风控复检,绝不重新进件。完整风控在授信时一次做完,结果固化进合同;
  • 额度域是两者的交汇点:授信生效事件创建额度账户,支用冻结/释放额度。两边只通过额度账户交互(事件驱动 + 对账兜底)。

二、额度模型:三分额度的 DDL 级设计 ​

1)两张核心表 ​

授信合同(决策结果的固化,一次授信一条)在授信域自建;额度账户(运行态,可被并发占用)归额度域:

sql
CREATE TABLE credit_grant (            -- 授信合同: 风控决策的固化
  id            BIGINT PRIMARY KEY,
  apply_id      BIGINT NOT NULL,       -- 进件申请单
  user_id       BIGINT NOT NULL,
  product_code  VARCHAR(32)  NOT NULL,
  total_amt     DECIMAL(16,2) NOT NULL,  -- 批复总额
  day_rate      DECIMAL(10,6) NOT NULL,  -- 定价(日利率), 金额计算 BigDecimal 全链路
  term_months   INT NOT NULL,
  decision_id   VARCHAR(64)  NOT NULL,   -- 风控决策快照号(可解释性的锚)
  status        VARCHAR(16)  NOT NULL,   -- 状态机, 见第三节
  effective_at  DATETIME,
  expire_at     DATETIME NOT NULL,
  UNIQUE KEY uk_apply (apply_id)
);

额度账户表不在此重复建。表名与状态枚举以额度中心模块设计的 credit_account/SUSPENDED 为准。模块层因 frozen_amt 占用 FROZEN 一词而改用 SUSPENDED。DDL 级拆解、流水表与两阶段冻结的完整推演同见该篇。

  • available 不落库:available = total − used − frozen 是纯推导值。多存一个冗余字段就多一个不一致源——所有读路径实时计算,所有写路径走条件更新;
  • 核心不变量:used + frozen ≤ total,且三者 ≥ 0。任何绕过条件更新直接 UPDATE 余额字段的代码都是违宪。不变量的业务语义与三分模型出处见信贷业务全景与领域模型。

2)并发控制:把判断压进 WHERE ​

额度行是热点(同一用户多端支用、放款与还款并发)。与秒杀库存扣减同构(见秒杀系统设计),不用分布式锁,用条件更新:

sql
-- 阶段一: 支用申请冻结(预占)。影响行数 = 0 即额度不足/状态非法/已过期
UPDATE credit_account
   SET frozen_amt = frozen_amt + #{amt}, updated_at = NOW()
 WHERE user_id = #{uid} AND product_code = #{pcode}
   AND status = 'ACTIVE' AND expire_at > NOW()
   AND total_amt - used_amt - frozen_amt >= #{amt};

-- 阶段二: 放款确认。冻结转已用, 一个 UPDATE 原子完成两笔变更
UPDATE credit_account
   SET frozen_amt = frozen_amt - #{amt},
       used_amt   = used_amt   + #{amt}, updated_at = NOW()
 WHERE id = #{id} AND frozen_amt >= #{amt};

InnoDB 行锁保证同一行串行,"判断 + 扣减"的原子性由 WHERE 谓词与行锁共同保证。被拒绝的那次并发根本没排上锁,冲突代价只是一次行锁等待。

3)两阶段冻结的并发时序 ​

用户(支用8000)          额度域                        支用/放款
    │ ①支用申请 8000      │
    ├───────────────────▶│ CAS: frozen 0→8000 (available 12000→4000)
    │ ◀──受理中(在途)─────┤ ②并发另一端支用5000: 4000<5000 → 0行, 拒绝
    │ ③放款指令            │
    ├──────────────────────────────────────────────▶│ 渠道代付
    │                    │ ◀── 放款成功回调 ──────────┤
    │                    │ ④CAS 二段: frozen−8000∧used+8000
    │                    │   (回调重放: frozen≥amt 不成立 → 0行, 幂等)
    │ ⑤若放款失败/超时      │
    ├───────────────────▶│ 解冻: frozen−8000 (available 恢复)

解冻前必须先查渠道终态——"超时"不等于"失败",钱可能已经放出去。查不到终态的单子挂"在途"进对账,绝不盲目解冻。

三、授信状态机 ​

 DRAFT(申请) ──提交──▶ APPROVING(审批中) ──风控通过──▶ APPROVED(待签约)
     │                    │                        │         │
     │ 用户撤回            │ 风控拒绝                 │ 签约+验密 │ 有效期届满未签约
     ▼                    ▼                        ▼         ▼
 CANCELED(撤回)      REJECTED(拒绝)             ACTIVE(生效) EXPIRED(失效)
                                                   │ ▲
                                     冻结(风险事件/监管指令)   解冻审批
                                                   ▼ │
                                                FROZEN(冻结)
  • 每条边唯一触发者:审批结果由风控回调触发、生效由签约动作触发。冻结由风险事件触发、失效由到期批触发。状态迁移永远由领域事件驱动,不允许接口直接"改状态"。与借据状态机同一纪律,见信贷业务全景与领域模型;
  • 所有迁移走 CAS:UPDATE credit_grant SET status='ACTIVE' WHERE id=? AND status='APPROVED'——签约与到期批并发时由状态谓词裁决,不靠锁;
  • REJECTED 携带原因码:监管要求信贷拒绝可告知原因,原因码取自决策快照,不是枚举猜的。

四、决策接口契约 ​

授信服务与风控决策系统之间是同步 RPC,契约先于实现:

请求 CreditDecisionRequest           响应 CreditDecisionResponse
├─ request_id    (幂等键,必填)       ├─ decision: APPROVE / REJECT / MANUAL
├─ scene         CREDIT_APPLY       ├─ total_amt      (批复额度)
├─ apply_id / user_id               ├─ pricing: { day_rate, term_months }
├─ amount / term                    ├─ reason_codes[] (拒绝/人工原因码)
├─ intake_snapshot_id (进件快照)     ├─ decision_id  (决策快照号, 合同外键)
└─ ext: 渠道号/设备指纹等              └─ versions: { policy:"v12", model:"v7" }

超时契约写死在双方 SLA 里:

项约定
超时(> 1.2s)fail-close:受理失败引导稍后重试,绝不默认通过;重试至多 1 次(幂等由风控按 apply_id 兜底)
MANUAL同步返回"审核中",人工审批完成后走事件通知,不做长轮询

五、与风控决策系统的交互时序 ​

授信服务            风控编排层            规则/模型层         特征平台
  │ ①决策请求(request_id)│                │                │
  ├────────────────────▶│ 前置规则:准入/黑名单 │                │
  │                    ├───────────────▶│ 命中即短路拒绝     │
  │                    │ 取特征向量        │                │
  │                    ├─────────────────────────────────▶│ 点查 P99<50ms
  │                    │ A 卡推理 → 后置规则:额度/定价 │              │
  │ ②决策+版本号 ◀──────┤                 │                │
  ├─ ③决策快照落库(与合同同事务语义) ④CAS APPROVED+额度账户创建(同事务) │
  │ ⑤发"授信生效"事件 ──▶ 额度域/营销/账务(订阅方幂等,见业务篇事件表)   │

关键纪律:决策快照与授信合同同批落库。监管检查、拒绝原因告知、策略回溯全靠它。快照的内容与严肃性见风控决策架构。

六、异常与降级 ​

降级按"降级后剩下的东西还够不够安全"分级。风控侧通用降级矩阵——授信/开户类 fail-close、支用类降级静态规则、模型超时 fallback 评分卡、特征源缺失值策略。这些与"预案声明在决策编排 DSL、故障时自动执行"的机制,见风控决策架构,此处不重复。授信篇只补授信链路特有的两行:

故障预案
决策快照落库失败决策作废重评;禁止"无快照的批复"进合同
签约服务故障合同停 APPROVED、额度不生效;恢复后补签约,不"先给额度后补签"

授信 fail-close 与支用降级放行的不对称,是这道题最容易暴露思考深度的地方。统一答案(全放/全拒)说明没权衡过敞口与可用性。额度侧的 frozen 悬挂巡检与授信生效事件丢失的对账补偿,见额度中心模块设计的悬挂扫描。补偿场景:有 APPROVED 合同无 ACTIVE 账户 → 补偿建账。

七、追问链 ​

Q1:为什么授信和支用是两次决策,不能授信一次管到底? 风险暴露不同:授信定的是"池子"(最大敞口),支用是真实出资。池子开 5 万不代表每次取用都零风险——收入变化、多头借贷都发生在授信之后。所以授信重决策一次,支用轻复检每次,复检成本被压到毫秒级。

Q2:available 为什么不落库? 三个数已互为约束,再加第四个冗余值就要为它维护一致性——每次写多一处同步、日终多一项核对。推导值的原则:能算出来的不存。存它的唯一理由是读性能,而额度读的 QPS 远不到需要冗余的量级。

Q3:放款回调一直不来,frozen 挂着怎么办? 先查证后解冻:冻结单超时(如 30 分钟)进巡检队列 → 调渠道查单接口拿终态 → 确认失败才解冻。无终态挂"在途"进对账差错池。直接按超时解冻是经典资损:渠道实际放款成功,额度被凭空多放一次。

Q4:调额(提额/降额)和支用并发怎么保证正确? 同一行的条件更新天然串行。降额的特殊性在于只能收缩未被占用的部分。若目标额度低于 used + frozen,实际降到 used + frozen 为止(已占用不可收回)。等还款释放后额度自然收敛到目标值——多退少不补,不做强制回收。

Q5:为什么不用 Redis 抗额度并发? 一句话:额度热点是"单用户级",DB 条件更新足够。Redis 只配做预检挡量、真相之源必须在 DB。完整推演见额度中心模块设计的"为什么不用 Redis 预扣减"问答。

Q6:循环额度和一次性额度,模型差别在哪? 合同到期语义不同:一次性额度结清即终。循环额度 total 不随还款减少(还款释放 used、available 回补),到期整体失效。DDL 是同一张表,差别在"还款是否回补 available"——产品配置,不是表结构差异。

八、面试官视角小结 ​

面试官在授信设计里检查四件事:

  • 职责切分:授信与支用是不是两个决策、两条链路——混在一起说明没想过决策成本的不对称;
  • 三分模型与不变量:total/used/frozen 说不清,两阶段支用一定答不好;
  • 并发控制:能不能说出"把业务判断压进 WHERE 子句",并指出它与秒杀扣库存、支付回调 CAS 是同一思想;
  • 降级分级意识:敢不敢给"授信 fail-close、支用降级静态规则"这种不对称答案。

一句话总结:授信开池、支用用水;三分额度、条件更新;快照留证、授信宁拒勿裸。

2026-10 新增(金融信贷域 · 系统设计层)。