1. 信贷系统模型表整体设计思路
1.1 这件事的核心难点在哪
信贷系统的数据模型设计,说难不难,说简单也不简单。我见过太多团队,业务刚起步的时候拍脑袋建了三四张表,等业务规模上来之后,发现扩展一个产品类型要改一堆代码,加一个风控字段要动五六张表的结构,最后只能靠一堆临时表和中间表硬撑。这种痛苦,经历过的人应该都能体会。
信贷系统模型表的核心难点不在于“建表”,而在于三个字:前瞻性。所谓前瞻性,就是你要在业务只有一种产品、一天几百笔进件的时候,预见到未来可能有三五条产品线、每天几十万笔进件,你的表结构是否还能撑得住。不能撑得住,后面就是持续两三年的重构痛苦;能撑得住,后面的事情都会顺很多。
第二个难点在于状态机的设计。信贷业务是一条长链路:进件、审批、授信、签约、放款、还款、结清、逾期、催收、核销、呆账。每个环节都有状态,状态之间还有流转关系。状态设计得不好,后续所有统计报表都会乱掉。比如一个“逾期”状态,在不同产品里含义可能不同——消费贷逾期1天和房贷逾期1天,严重程度完全不一样,但如果你设计表的时候不把“逾期天数段”拆出来,后续就容易出问题。
第三个难点在于金额字段的精度处理。钱这个东西,一分钱都不能出错。但是信贷业务日息、罚息、复利、提前结清、部分还款,各种金融计算混杂在一起,数据库浮点数精度不够,整数存储又不够直观,到底怎么存才稳妥,这是每个做信贷系统的开发都必须想清楚的问题。
第四个难点在于多租户隔离与数据视角。如果是做助贷平台、多资金方接入、联合贷,那么你的模型表还需要考虑资金方维度、渠道方维度的隔离。同一个客户,在多个资金方各有额度和借据,怎么区分、怎么汇总,这也是一个隐藏的深坑。
1.2 模型表的核心实体关系梳理
在信贷系统里,核心实体其实就那么几个。我们先不看具体字段,先把主干捞出来。
客户、申请单(进件)、授信额度、合同、借据/放款记录、还款计划、还款流水、逾期记录、催收记录。这是信贷系统最底层的九张表,其他所有表,都是围绕它们延展的。
客户表管“谁在借钱”,申请单表管“每次申请的情况”,授信额度表管“给客户批了多少钱、还能借多少”,合同表管“双方约定”,借据表管“实际放了多少出去”,还款计划表管“每一期该还多少”,还款流水表管“客户实际还了多少”,逾期记录表管“哪些款项到期没还”,催收记录表管“为了追回欠款做了什么”。
这九个实体之间的关系,我通常这么描述:
一个客户可以发起多笔申请,一笔申请经过审批通过后,会生成一个或多个授信额度。额度之下,客户可以发起提款,每次提款生成一笔合同,合同项下会生成借据。借据会展开为多期还款计划,客户每期还款时会产生还款流水。如果某期计划到期未还,系统会生成逾期记录,逾期记录会驱动催收工单。
这就是信贷系统的主干数据流。你在做模型设计的时候,第一步不是去想字段怎么设计、索引怎么设计,而是把这九张表之间的关系理顺,确保业务流转过程中产生的每一份数据,都能在这套骨架中找到合理的位置。任何游离在骨架之外的“特殊需求”,基本都说明骨架设计有问题。
1.3 为什么强调“宽表不如窄表”
在设计信贷模型表的时候,很多刚入行的同学倾向于设计宽表——把所有相关字段堆在一张表里,省去关联查询,看起来“性能好”,实际后面会哭。
举个真实例子。我们之前接过一个系统,客户信息表里有三十多个字段,包括职业、单位名称、单位电话、单位地址、身份证有效期、婚姻状况、学历、车辆信息、房产信息、联系人A、联系人B等等,加上客户的基础身份信息,一张表七十多个字段。当时设计的人说“这样查询快,不用join”,结果后面业务方提需求说“联系人信息需要支持多联系人录入,每天户最多支持五个紧急联系人”,改表结构就改了一天,还影响到了业务主流程的性能。
信贷系统的模型表设计,语义边界比物理存储更重要。客户基本信息是一层,客户扩展信息(职业、资产、联系人)是一层,客户风险标签是一层,客户关系(如关联企业)又是一层。永远不要嫌表多,你要做的语义隔离,是为了将来任何一层发生变化时,其他层不受牵连。
我建议所有做信贷系统的同学,在设计模型表时把“核心表”和“扩展表”分开,把“高频查询字段”和“低频异常字段”分开,把“强一致数据”和“弱一致数据”分开,这样后面无论是做数据迁移还是做性能优化,都轻松得多。
2. 核心表结构与字段设计的实操拆解
2.1 客户信息表:唯一客户标识怎么设计
客户信息表是整个信贷系统的基石。这个表设计得好不好,直接决定了后续所有业务流程的顺畅程度。
先说说唯一客户标识。很多系统直接拿“身份证号”作为客户的唯一标识,看起来好像没问题——每个人身份证号是唯一的。但实际业务里并非如此:同一个客户,可能在线上渠道注册了一个账号,又在线下门店提交过资料,两个渠道录入信息时身份证号写法有差异(一个带X一个带x,一个15位一个18位),或者客户的手机号变了,就可能被当成两个客户。如果你用身份证号做唯一标识,后面做额度合并、客户去重,会非常痛苦。
我建议的做法是:客户表有自己的主键ID,同时单独加一个“客户唯一标识”字段,用于关联身份信息,主键ID自增即可,客户唯一标识则是业务上用来做客户归并的聚合键。在唯一标识的生成层面,身份证号可以参与拼装,但不要直接把它当唯一键。
还有一个字段值得专门提出:客户状态。这个状态至少要有“正常、冻结、注销、黑名单”四种。黑名单客户如果后续被解禁,需要保留历史标记,所以“是否黑名单”建议不要简单用一个布尔值,而是用“黑名单状态 + 黑名单原因 + 最近拉黑时间”,这样给后续风控策略留了余地。
客户表的索引设计也要用点心思。身份证号是必查字段,手机号是高频查询字段,建议都以身份证号作为前缀索引建唯一索引(通常是身份证号加姓名防冲突),手机号建普通索引。如果做过分库分表,这类的索引规则还要再重新设计,这里就不展开了。
2.2 进件申请单表:多产品复用的关键
进件申请单表(也叫申请件/申请单/借款申请)是信贷系统里交易量最大的一张表之一。每天进入的贷款申请,都会在申请单表里产生一条记录。
很多系统把申请单表设计成了“一个产品一张表”:消费贷申请一张表,经营贷申请一张表,房贷申请一张表。这样做虽然直观,但后续的问题很多:第一,业务方提一个“查询客户所有申请记录”的需求,你要跨三张表去union;第二,新产品上线时,你得再建一张几乎一模一样的表;第三,风控策略要读取所有申请做风险分析时,数据来源分散,做特征加工的成本很高。
我在实际项目里的做法是:一张主申请单表 + 多张产品扩展表。主申请单表只放所有产品通用的字段:申请单号、客户ID、申请时间、产品编码、申请金额、申请期限、审批状态、审批人、审批时间、进件渠道、归属机构ID等。而各个产品特有的字段——比如房贷的房屋估值、首付比例、按揭年限,或者车贷的车辆VIN码、车辆评估价、车龄等——则放到对应的产品扩展表中,用申请单ID关联。
这样做的好处非常明显。主表的查询性能不会被产品特有字段拖累,产品特有数据在各自的扩展表里独立管理。你后续要加一个新产品的特有字段,只需要新增一张扩展表,不动主表结构,影响面被缩到最小。
关于申请单状态,我在这里多强调一句。申请单一定要有完整的审批状态流转,至少包含:草稿、待初审、初审通过、初审拒绝、待终审、终审通过、终审拒绝、已撤销、已失效这几个核心状态。如果一个申请单被驳回后重新提交,建议不要修改原申请单状态为“重新提交”,而是生成一个“申请版本号”,同一个申请单号下维护多条申请版本,这样追溯日志就不会乱。
2.3 审批决策表:把人工经验和规则模型分开
审批环节是信贷业务的核心风险控制点,审批数据值得单独建表——审批决策表。
审批决策表记录每一次审批动作的详细结果,包括:审批单ID、审批节点、审批人、审批结果(通过/拒绝/人工复核)、审批意见、审批额度、审批期限、审批利率、决策使用的策略集编码、命中规则明细JSON、评分卡得分等等。
这里的一个关键点是:人工审批数据和自动决策数据不能混在一张表里。人工审批关注的是审批人、审批时间、审批意见、审批照片等操作信息;自动决策关注的是规则命中情况、模型评分、策略版本等算法信息。如果把两者混在一张表里,后续做审批质量分析、策略回测、模型迭代评估时,很难拆开使用。我的实操做法是:审批主表 + 审批规则命中明细表。审批主表记录决策结果和审批人的信息;审批规则命中明细表专门记录命中了哪条规则、规则版本、输出评分等。这样既保留了结果数据,也保留了过程数据,对后续模型迭代特别有价值。
还有一个容易被忽略的字段:审批时长。从申请单进入审批队列到审批完成之间的时间差,是系统运营的重要考核指标,也能反映审批效率变化趋势。别小看这个字段,后面做客服投诉分析、审批流程优化,都要用到它。
2.4 授信额度表:额度是信贷的“水龙头”
授信额度表管理的是客户在系统中的总借款能力,信贷额度相关设计细节比较多,值得单独展开。
额度表常见字段包括:额度ID、客户ID、产品编码、额度类型(单笔额度/循环额度)、总额度、已用额度、剩余额度、生效日期、失效日期、额度状态。在多产品场景下,一个客户可能在多个产品下各有一个额度,所以客户对产品非唯一,设计时要有“编码”概念。
额度与授信审批高度的关系是:审批通过后生成额度记录,额度记录关联审批决策表。同时额度需要锁定机制——客户发起借款时,先锁定额度,放款成功后再扣除额度并释放锁定。这套并发控制逻辑,对防止超额放款特别重要。我见过不少系统在额度扣除环节,只是简单地在应用层查一下剩余额度,然后做减法,结果就是并发场景下额度超发,实际放款总额超过了审批额度。正确的做法是,在数据库层面通过对额度记录进行行级锁更新(如使用select ... for update,或通过update语句中的条件校验),配合乐观锁版本号,确保额度扣减的原子性。
额度还必须有“恢复机制”。比如一笔借款提前结清后,如果产品支持循环额度,那么已用额度要能自动恢复。这个恢复操作一般通过还款流水事件触发,而不是通过定时任务批量扫描。事件驱动的方式实时性好,实现上也不复杂——只需要在结清回调里调用额度恢复服务即可。
2.5 合同表和借据表:签了字和出了钱,是两回事
合同表和借据表很容易被混为一谈,但在信贷系统里,这两者的业务含义完全不同。
合同是双方权利义务的约定,代表借贷关系的法律基础,签约动作发生之后,借贷关系即被确认。借据是放款凭证,代表资金实际已经划拨出去。一次签约合同可以在额度内分多次放款,所以一笔合同下可以有多张借据。
合同表字段包括:合同ID、合同编号、客户ID、产品编码、授信额度ID、合同金额、期限、利率模式(固定/浮动)、还款方式(等额本息/等额本金/先息后本/一次性还本付息)、合同签署时间、合同生效时间、合同状态(生效中/已结清/已作废/已终止)等。
借据表的字段包括:借据ID、借据编号、合同ID、客户ID、放款金额、放款时间、起息日、到期日、还款方式、期数、放款账户信息、资金方ID等。注意,借据才是还款计划表的上游来源。每一张借据会展开成多期还款计划。
这里有一个实操细节需要提醒:扣款账户信息建议不要直接存银行卡号明文。可以存脱敏卡号加支付通道的令牌号,这样即使数据库被拖库,也不会造成大面积卡号泄露,安全合规上能少很多麻烦。
3. 还款计划、还款流水与资金清算明细
3.1 还款计划表:把钱怎么还的时间表说清楚
还款计划表是按期生成的。每一张借据放款成功后,系统会根据还款方式计算出每一期的应还日期、应还本金、应还利息、应还总额。
这里以最常见的等额本息为例说一下计算公式:
每月还款额为:( M = P \times r \times (1+r)^n / ((1+r)^n - 1) )
其中,P是贷款本金,r是月利率,n是还款期数。
比如借款12000元,期限12个月,月利率0.8%。代入计算,每月月供约1052.76元。第一期利息为12000×0.8%=96元,本金部分为1052.76-96=956.76元;剩余本金变为12000-956.76=11043.24元。第二期利息就变成11043.24×0.8%=88.35元,本金部分为1052.76-88.35=964.41元。这样逐期递推,最后一期本金刚好归零。
每一期还款计划都应是一个独立记录,字段包括:还款计划ID、借据ID、期次、应还日期,应还本金、应还利息、应还总额、逾期状态、实际还款日期、实还总额、结清状态。
计算利息和本金分摊时,务必用“分”为单位做整数计算,或者用高精度的Decimal类型(在关系数据库里可用numeric/decimal字段定义足够的精度),避免浮点数误差累积。别小看这一条,等额本息120期之后,误差可能已经有好几块钱了。
3.2 还款流水表:每一分钱都从哪来到哪去
还款流水表是粒度最细的账务数据表,记录了每一笔资金划转动作。它比还款计划表更底层,一件还款计划可以有多笔还款流水(比如客户先还了一部分,过两天又还了一部分)。
还款流水表核心字段包括:流水ID、借据ID、还款计划ID(可空)、客户ID、交易时间、交易金额、本金部分、利息部分、罚息部分、违约金部分、还款方式(主动还款/系统代扣/线下补录)、支付渠道、支付流水号、交易状态。
设计这张表的时候,最难的不是字段,而是如何保证对账的一致性问题。支付回调、渠道通知、手动补录,多通道写入,很容易出现同一笔还款被记两次或者漏记的情况。
实操中的建议是:还款流水表必须建立“业务唯一索引”,通常用支付渠道流水号加还款计划ID作为联合唯一键。所有还款流水写入之前,先按这个唯一键insert,如果冲突就说明重复,直接拒掉。这是最朴素也最有效的幂等控制手段。
还有一点容易踩坑:还款流水到账后,业务系统需要更新还款计划的状态、更新借据的剩余本息、更新额度的已用金额。这三个更新要么全成功,要么全失败,建议用本地事务,确保数据强一致。不要用定时任务去扫流水反推,那样会造成对账延迟和状态回写的错乱。
3.3 逾期记录与催收工单表:风险处理链路
逾期记录表和催收工单表,严格来说属于贷后管理域。很多设计者在最初的模型规划中不重视这块,直到坏账来了才开始补救,结果非常被动。
逾期记录的生成逻辑一般有两种:一种是还款计划到期当天未还清,系统跑批生成逾期记录;另一种是还款日当天只还了一部分,生成部分逾期记录。部分逾期是特别容易被忽略的,尤其是按揭类的还款,客户这个月只还了一半,另一半逾期了,逾期金额怎么算、罚息怎么计,都要在逾期记录表里体现清楚。
逾期记录表字段包括:逾期ID、借据ID、还款计划ID、逾期本金、逾期利息、罚息、违约金、逾期开始日期、逾期天数、逾期等级(M1/M2/M3等)、结清时间。逾期等级不同公司有不同定义,常见的是M1代表逾期1-30天,M2代表31-60天,M3代表61-90天,M4+代表更严重阶段。
催收工单表是逾期记录的后续驱动表,一般有工单ID、逾期记录ID、催收员ID、催收状态、催收计划时间、最近联系结果、承诺还款日期,实还金额等字段。催收工单要与提醒记录分离,一次工单会伴随多次电话提醒、短信提醒等动作,这些提醒记录同样值得单独成表。催收话术、催收依据、客户承诺等文本信息可以放在扩展表里,保持工单主表的简洁。
4. 系统扩展域与附件管理
4.1 影像资料表和合同附件表:别让文件成为孤岛
信贷系统里除了结构化数据,还有大量的非结构化数据——身份证照片、人脸识别照片、收入证明、银行流水、合同扫描件、签章影像、电子签名等。这些文件如果管理不当,很容易成为数据孤岛,后续审计、合规、法律纠纷处理都很麻烦。
统一的存储方案是:影像资料表 + 物理文件存储(文件服务器或云对象存储)。影像资料表字段包括:影像ID、业务类型(进件/合同/贷后)、关联业务ID、文件类型、文件名称、存储路径、OSS-Key、文件大小、上传人、上传时间。
这样做的好处有以下几点。第一,文件本身和业务数据解耦,文件存储扩容不影响数据库性能。第二,多业务可以共用同一套影像管理服务,录入、查询、权限控制都有统一的逻辑。第三,后续做档案电子化、合规检查时,能够按业务ID和文件类型快速检索到全部文件。
这里要补充一个教训:关联业务ID不要存多值。我看到有些系统,因为一张进件单有多张身份证影像,就在业务表里存了一个“影像编号列表”,用逗号分隔,然后查询时又要用FIND_IN_SET之类的函数去拆。这其实违背了关系数据库的基本设计原则,而且后续统计、归档、权限判断都非常痛苦。正确做法是,影像表和业务表之间是多对一的关系,影像表里存关联业务ID,不要反过来在业务表里存影像ID列表。
4.2 数据字典表和通用配置表:让模型表“活”起来
信贷系统涉及的枚举值非常多。产品类型、还款方式、客户风险等级、申请渠道、合同状态、借据状态、逾期等级、催收状态……这些枚举值,不能硬编码在应用代码里,而要放在数据字典表中做统一管理。
数据字典表字段一般包括:字典类型、字典编码、字典名称、状态、排序号、扩展信息。比如字典类型为“loan_product_type”,字典编码为“housing_loan”,字典名称为“住房贷款”。所有业务表里使用编码值,展示时通过字典翻译成可读文本。这样做的最大价值在于,新增一种产品类型或状态,只需要在字典表里加一条数据,不需要改代码、发版。
通用配置表则用于维护业务参数,比如最低还款金额、罚息利率、合同模板编号、代扣批次时间等。配置表字段包括:配置键、配置值、配置描述、生效时间、失效时间。把业务参数和代码逻辑分开,之后业务方自行调整参数而不需要开发介入,对运营效率的提升不是一星半点。
有些团队会把数据字典表设计成“一张大表,所有类型都往里塞”,比如:dict_typeVARCHAR(100),dict_codeVARCHAR(100),dict_nameVARCHAR(200)这种结构。这样设计没有问题,但要特别注意查询性能:所有业务对字典表的访问量都很高,字典表本身的数量也不小(可能有几千条),因此一定要建好联合索引(dict_type+dict_code),并考虑如何做缓存——在应用层,可以把字典表整体加载到本地缓存或Redis里,过期策略十分钟足够,这样查询字典时根本不走数据库,大大缓解数据库压力。
5. 模型表的落地实施要点与常见问题
5.1 SQL建表示例与通用字段约定
为了让大家能形象地感知模型表的落地实施效果,我以九张核心链路中的“借据表”和“还款计划表”为例,写两段极简DDL,展示常见的字段与注释约定。
CREATE TABLE `loan_receipt` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `receipt_no` VARCHAR(64) NOT NULL COMMENT '借据编号', `contract_no` VARCHAR(64) NOT NULL COMMENT '关联合同编号', `customer_id` BIGINT NOT NULL COMMENT '客户ID', `product_code` VARCHAR(32) NOT NULL COMMENT '产品编码', `loan_amount` DECIMAL(14,2) NOT NULL COMMENT '放款金额(单位:元,注意精度)', `loan_date` DATETIME NOT NULL COMMENT '放款日期', `value_date` DATE NOT NULL COMMENT '起息日', `maturity_date` DATE NOT NULL COMMENT '到期日', `repay_method` VARCHAR(20) NOT NULL COMMENT '还款方式', `status` TINYINT NOT NULL COMMENT '借据状态:0-正常 1-逾期 2-结清 3-核销', PRIMARY KEY (`id`), UNIQUE KEY `uniq_receipt_no` (`receipt_no`), KEY `idx_customer_id` (`customer_id`), KEY `idx_contract_no` (`contract_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借据表';CREATE TABLE `loan_repay_plan` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `receipt_id` BIGINT NOT NULL COMMENT '借据ID', `period_no` INT NOT NULL COMMENT '期次', `due_date` DATE NOT NULL COMMENT '应还日期', `due_principal` DECIMAL(14,2) NOT NULL COMMENT '应还本金', `due_interest` DECIMAL(14,2) NOT NULL COMMENT '应还利息', `due_total` DECIMAL(14,2) NOT NULL COMMENT '应还总额', `remain_principal` DECIMAL(14,2) NOT NULL COMMENT '剩余本金', `repay_status` TINYINT NOT NULL COMMENT '0-未还 1-部分还款 2-结清', PRIMARY KEY (`id`), UNIQUE KEY `uniq_receipt_period` (`receipt_id`, `period_no`), KEY `idx_due_date` (`due_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='还款计划表';关于列类型,我做几点补充说明。
金额字段一律用DECIMAL,不用FLOAT和DOUBLE,避免精度误差。货币单位建议统一为“元”,在代码里全部以元为单位进行计算,但特别注意小数精度控制,必要时考虑在展示层做四舍五入加工。
日期字段一律用DATE(日期)或DATETIME(日期时间),不做区分时不要混用。合理选择这两个类型关系到后续跟资金方系统进行对账的兼容性。
主键统一用BIGINT自增。如果后续要分库分表,全局ID生成策略再说,但在单体阶段自增主键足够用。
每一个表都要有“创建时间”和“更新时间”字段。这不仅是规矩,而且后面排数据问题的时候,没有这两个字段简直寸步难行。
5.2 状态机流转是模型表的“灵魂”
前面我多次提到了状态,这里集中展开一下状态机设计。信贷系统的核心域中,每个核心对象都有状态机,状态机设计得好不好直接决定了业务流程的可靠性与可追溯性。这里以“借据状态机”举个例子。
借据初始状态为“正常”。到期未还清,系统跑批将状态更新为“逾期”,同时生成逾期记录。客户还清逾期本息后,状态恢复为“正常”,对应的逾期记录也要关联上“结清时间”。当借据所有期次全部结清后,状态更新为“结清”。如果出现了极端坏账,通过核销流程,将状态更新为“核销”。
状态机设计要遵循两个原则:每个状态的变更都要有触发事件,每个状态变更都要记录操作日志。触发事件就是业务上的实际动作(还款、跑批、人工操作等);操作日志则记录变更前状态、变更后状态、操作人、操作时间、触发事件类型。没有日志的状态变更不可追溯,后续处理客诉、审计都会是灾难。
另外还有一点:状态变更不一定要实时同步。比如借据从“正常”变为“逾期”,一般是通过每日批处理任务来完成,而不是每笔还款失败就立即改状态。一方面跑批处理效率高,另一方面也避免小额短时波动对系统造成不必要的压力。但是像“还款成功”导致借据状态变为“结清”这类关键事件,则要求实时同步,因为客户端需要立即看到结果。你需要区分哪些状态变更可以异步批量处理、哪些必须同步实时更新。这个分寸把握,是靠经验拿捏出来的。
5.3 常见问题与排查技巧实录
信贷系统上线跑了一段时间后,常见问题逐渐暴露出来,这里分享几个我实际遇到过、排查过的问题。
问题一:对账不平,还款流水比账实金额多
排查思路:先比对还款流水表中的渠道流水号是否存在重复;然后检查“支付回调”与“人工补录”是否走了同一个幂等校验逻辑。最常见的原因是人工补录时没有走系统唯一键校验,补录了和自动还款同一渠道同一时间的流水。解决办法:人工补录入口强制走同一幂等逻辑,并把“业务来源”字段设为必填。
问题二:额度扣减偶尔超发
排查思路:检查扣减额度是否是在事务内完成,是否对额度记录执行了行级锁。很多超发的原因是应用层先查额度、再更新额度,两步之间发生了并发。解决办法:改为单条UPDATE loan_limit SET used_amount = used_amount + #{amount} WHERE id = #{id} AND used_amount + #{amount} <= total_amount,利用行锁和条件约束来保证并发安全。
问题三:逾期记录的重复生成
排查思路:检查逾期跑批任务是否做了幂等。有时是调度系统重复触发了跑批,而跑批的逻辑里只判断了“当前日期等于还款计划应还日期”,没有判断“逾期记录是否已存在”。解决办法:在还款计划ID和逾期状态上建联合唯一索引,从数据库层面拦截重复生成。
问题四:还清逾期款项后,额度没有恢复
排查思路:检查还款流水事件是否驱动了“逾期结清”和“额度恢复”两个操作。常见原因是事件发送成功但消费端处理失败,而且没有重试机制。解决办法:还款清分时把“额度恢复”作为并发任务执行,设置MQ消费失败重试,重型重试不行走本地定时任务扫描补偿。
这些问题的共性规律是:信贷系统的异常大多不是发生在大流量并发下,而是发生在补偿逻辑、边界条件和幂等控制缺失时。所以做模型表设计、接口开发时,提前理清补偿路径,比追求极致的性能更能省心。
5.4 模型表设计中的字段冗余与性能平衡
最后我们再回到设计层面,谈一个几乎每个做信贷模型设计的同学都会纠结的问题:字段冗余到底允许不允许?
比如客户表里存了一个“客户总授信额度”的汇总字段,业务方查询时直接读取,不用实时计算。这是字段冗余,但它带来的好处是极大的查询性能提升。而如果客户表里存了“最近一次借款产品”这样的字段,它也是冗余字段,但这个冗余字段伴随高频率更新,每个客户借款后都要改一次,单看写入代价不大,但客户量上来了之后代价就很明显。
我个人总结的经验是三点。第一,高频查询字段可以冗余,高频更新字段不要冗余。第二,冗余字段必须设计同步更新机制,而且这个机制要尽量简单,能在事务内完成的就不做异步。第三,统计汇总类字段尽量放到宽表或数仓层,不要塞进在线交易的核心表里。交易核心表越轻越稳;统计分析的需求尽量通过数仓去满足。
信贷模型表的“轻重分离”是一个长期工程。初期你怎么省事怎么来,但一旦规模上了,就不太可能回头了。所以模型表设计这个事,我始终建议宁可前期多想一点点,也别等上线之后再做“性能优化式重构”,那时候的成本是前期的好几倍,而且风险极高。
这个内容后续还可以这样扩展:细到具体场景去设计反欺诈特征表、人行征信报文解析与落库模型、额度策略引擎的决策表,这些都是信贷系统模型表之上很自然的延展方向。真有兴趣的同行,可以从这几类表开始深入研究,一步步把信贷系统中数据底座的完整版图铺开。