跳转到主要内容

专题

围绕工作主线系统整理的系列专题,按主题深入展开。

这里是按主题整理的专题入口。每个专题围绕一条工作主线展开,把相关文章串成可长期查阅的知识脉络。

目录

架构专栏

客户信息系统、核心系统、分布式架构与领域驱动设计(DDD)的实战拆解。

银行系统的复杂度,往往不在单点技术,而在「如何把监管、账务、渠道与性能约束织成一张自洽的网」。本专栏拆解其中的关键决策与权衡。

下面是本专栏的文章:

聚合根与领域事件:DDD 战术建模

从一致性边界到事件驱动,拆解聚合根的设计原则、领域事件的发布与消费,以及在银行核心系统里的落地坑点。

银行核心系统里最容易被低估的概念,是「聚合」。很多人把 DDD 战术设计理解成「给实体加几个注解」,结果聚合越写越大,事务越来越长,最后把数据库和团队都拖垮。本文把聚合根和领域事件放回它们本来的位置:一致性边界的设计工具,以及领域向外界发声的窗口。

为什么需要聚合

领域模型不是一张 ER 图。ER 图关心「数据怎么存」,聚合关心「哪些数据必须在同一个事务里保持一致」。这两件事经常错位,而错位的代价在金融场景里格外昂贵——账务不平、状态错乱,往往不是因为算法错,而是因为一致性边界划错了。

一致性边界

聚合的第一性原理是:在边界之内,用强一致性保证业务规则不被破坏;在边界之外,用最终一致性传递变化。一个账户余额不能为负,这条规则必须在一个事务内被满足,所以它属于聚合内部。而「账户余额变动后通知风控」,则属于边界之外,交给领域事件。

经验法则:如果两条业务规则必须同时为真,它们大概率属于同一个聚合;如果可以接受短暂的不一致,它们就分属不同聚合。

聚合不是「大对象」

最常见的反模式,是把「客户」做成包含地址、联系人、合同、账户、画像的超级对象。它在概念上很完整,但在工程上是个灾难:每次改一个手机号,都要加载半个数据库,还要锁住一堆根本无关的数据。

聚合的边界应以事务一致性而非业务概念大小来划。概念上属于「同一个东西」的数据,未必需要在同一个事务里。

聚合根的设计原则

聚合根是聚合对外的唯一入口。外部只能持有聚合根的引用,不能直接操作聚合内部的实体或值对象。

引用靠 ID,不靠对象

跨聚合的关联,永远用标识符,而不是对象引用。聚合 A 不应直接持有聚合 B 的对象,否则两个聚合会被悄悄绑进同一事务。

public class Account {
    private final AccountId id;          // 聚合根标识
    private final CustomerId ownerId;    // 跨聚合引用:只存 ID
    private final Money balance;
    private final List<DomainEvent> pendingEvents = new ArrayList<>();

    public void withdraw(Money amount) {
        if (balance.isLessThan(amount)) {
            throw new InsufficientBalanceException(id);
        }
        balance = balance.subtract(amount);
        pendingEvents.add(new MoneyWithdrawn(id, amount, now()));
    }

    public List<DomainEvent> drainEvents() {
        var copy = new ArrayList<>(pendingEvents);
        pendingEvents.clear();
        return copy;
    }
}

工厂与仓储

复杂聚合的创建交给工厂,避免调用方掌握内部构造细节;聚合的持久化交给仓储,且仓储只按聚合根 ID 加载整个聚合,不存在「只查一半」的仓储方法。

  • 工厂负责「从无到有」并保证初始不变式成立。
  • 仓储负责「整存整取」,不暴露部分更新。
  • 应用服务协调工厂、仓储与领域事件发布,但不写业务规则。

不变式要内聚

不变式(invariant)是聚合存在的理由。余额不为负、转账双方必须同币种、合同生效前不能放款——这些规则写在聚合内部的方法里,而不是散落到 service 层去做 if 校验。把规则内聚到聚合里,测试时只需构造一个对象就能验证业务,不必搭一整套上下文。

领域事件是聚合的「对外窗口」

聚合根在自己身上记录了事件,但绝不能自己发消息。聚合只负责产生事件对象,发布动作由应用服务或仓储在完成事务后统一处理。这样聚合保持纯净,测试也简单。

事件如何产生

事件是对「已经发生的事实」的描述,命名用过去式:MoneyWithdrawnAccountOpenedLimitExceeded。它携带聚合根 ID、关键数值和时间戳。

public record MoneyWithdrawn(
        AccountId accountId,
        Money amount,
        Instant occurredAt
) implements DomainEvent {}

发布与订阅的边界

发布要在本地事务提交之后,否则会出现「消息发了、事务回滚」的幽灵事件。常见做法是在同一个数据库事务里写一张发件箱表(outbox),再由独立投递器搬运到消息队列。

Outbox 模式的价值不在于花哨,而在于它把「数据库一致」和「消息可靠」这两件事重新对齐:要么都成功,要么都失败。

方案一致性保证复杂度
事务内直发 MQ弱,可能幽灵事件
Outbox + 投递器强,与本地事务同生命周期
CDC 捕获 binlog强,对业务无侵入

一个银行开户的例子

假设开户需要:建立客户档案、创建主账户、初始化额度、通知风控建档。

步骤归属聚合一致性要求
创建客户档案Customer 聚合强一致,档案落库即生效
创建主账户Account 聚合强一致,账户初始余额为零
初始化额度Limit 聚合强一致,额度与账户绑定
通知风控跨聚合最终一致,事件驱动

前三项各自在自己的聚合内完成,开户服务用一次 saga 或本地事务把它们串起来;最后一项通过领域事件异步触发,风控系统晚几秒建档完全可接受。

事件流

CustomerOpened -> AccountOpened -> LimitInitialized -> (事件) -> RiskProfileRequested

订阅方消费 AccountOpened 即可并行去建额度、建风控档案,不必等开户服务逐个调用。这里体现的是聚合协作的核心思想:同步走强一致,异步走事件,两者各司其职。

常见误区

  1. 把聚合当 CRUD 载体:聚合方法名是 saveupdate,里面没有任何业务规则。这时 DDD 只是换了个地方写 SQL。
  2. 跨聚合事务:为了「保证一起成功」而在应用层开一个大事务锁住多个聚合。正确做法是 sagas/事件,接受最终一致。
  3. 聚合里注入仓储或发消息:聚合一旦依赖外部基础设施,就再也单元测试不了,领域层也被污染。
  4. 过度拆分聚合:为了「小」而把本应一致的两条规则拆到两个聚合,结果反而要处处补偿。
  • 聚合越小,并发越高,但跨聚合协作成本也越高。
  • 聚合越大,一致性越好写,但锁竞争和加载成本会反噬。
  • 平衡点来自对业务规则的诚实审视,而非拍脑袋。

结论

聚合根和领域事件不是花活,它们是把「业务的硬约束」翻译成「代码的硬边界」的手段。先把一致性边界划对,再让聚合通过事件对外低耦合地协作,银行核心系统才能真正既稳又活。下一讲我们会把这套思路接到 CQRS 与 Saga 上,看复杂长事务怎么在不牺牲一致性的前提下拆开。

CQRS 与 Saga:复杂业务的读写分离与最终一致

读写模型分家之后,复杂长事务如何用 Saga 串起来,又如何在不牺牲对账能力的前提下接受最终一致。

很多团队把 CQRS 当成「加个 MQ 同步数据」就完事,结果读模型对了、写模型乱了,长事务更无从下手。CQRS 解决「读写诉求不同」,Saga 解决「业务跨多个聚合却不能开大事务」,两者常一起出现但职责不同。

读写为什么要分家

写模型关心一致性与不变量,是一组小而强的聚合;读模型关心展示与组装,最好能直接查出前端要的 DTO。诉求冲突时强行共用一个模型,两边都不讨好。

account.withdraw(money);     // 写侧:只做业务
repository.save(account);
findByCardNo(cardNo);        // 读侧直接投影无需聚合

不要为了 CQRS 而 CQRS。单表就能满足读写、流量不大的模块,分家只会制造延迟和负担。

Saga 管理长事务

转账、跨境汇款天然跨账户跨系统,不可能用本地事务锁住。Saga 把大事务拆成一串本地事务,每步都有补偿。

模式思路适用
编排中心协调器逐步调用并补偿需强管控
协同各服务靠事件触发去中心低耦合

补偿与幂等

Saga 最怕「补偿自己也失败」,所以每步都要幂等:用业务流水号去重,重试不重复扣款。每步记录状态机 PENDING -> DONE / COMPENSATED,补偿顺序与正向相反,对账做兜底而非第一道防线。

最终一致不是「不管了」,而是把一致性从「即时」换成「可验证」:允许短暂中间态,但必须能收敛到正确态。

落地上,CQRS 让读写各取所需,Saga 让长流程可回退,再加独立对账,复杂业务才算立得住。

六边形架构在银行核心系统的落地

用端口与适配器把核心业务从数据库、渠道和三方系统里解放出来,让银行核心真正可测、可替换、可演进。

银行核心最痛的不是业务复杂,而是业务逻辑和周边技术死死绑在一起:换连接池要改核心,接新渠道要动账务,单测要起一整套中间件。六边形架构(Ports & Adapters)给出干净解法。

核心、端口与适配器

  • 领域核心:纯业务规则,不依赖任何框架、数据库或 HTTP 客户端。
  • 端口:核心暴露(入站)和需要(出站)的接口,用 Java 接口表达意图。
  • 适配器:把端口接到具体技术——REST 控制器是入站适配器,JDBC 仓储是出站适配器。
public interface AccountRepository {   // 端口:只定义要什么
    Account load(AccountId id);
    void save(Account account);
}
public class JdbcAccountRepository implements AccountRepository { /* 技术留在外围 */ }

判断架构是否干净的一条硬标准:删掉数据库、MQ、Web 框架,核心业务代码还能原封不动地跑并被测。

在银行核心里怎么切

核心账务、计息、限额放在中心;大小额网关、CBS 接口、短信、风控回调全做成外围适配器。监管规则变了只动核心,接新清算通道只加一个适配器。

层次内容依赖框架
领域核心账务、计息、限额
应用层用例编排
适配层REST/JDBC/MQ

收益与代价

收益是可单测、可独立部署、渠道可替换;代价是初期抽象成本与团队纪律。在银行这种长生命周期系统里,核心被技术债锁死的代价远高于前期投入。

架构不是墙上的图,而是写在依赖箭头里的纪律。六边形的价值,是让「技术变了业务不动」成为默认结果。

在银行客户信息系统(ECIF)里落地 DDD 聚合

客户是「一个实体」还是「一组上下文」?用聚合根与界限上下文重新切分 ECIF 的客户模型。

ECIF 里「客户」的概念极其庞大:个人、对公、同业,各自的属性、关系、生命周期都不同。如果用一个巨大的 Customer 实体硬扛,代码会迅速腐化。

用界限上下文切分

把客户拆成几个界限上下文:

  • 客户主数据(Party):统一的自然人/机构标识与基础属性。
  • 客户画像(Profile):风险偏好、营销标签,读写频率高、变化快。
  • 客户关系(Relationship):持股、担保、集团关系。

聚合根怎么定

每个上下文内部再定聚合根。例如 Party 上下文里,Party 是聚合根,AddressContact 是其值对象,保证一致性边界内不跨聚合调用。

经验:聚合的边界应以「事务一致性」而非「业务概念大小」来划。ECIF 里最容易犯的错,就是把所有客户信息塞进一个聚合。

这样设计后,主数据服务稳定,画像服务可以独立迭代,互不影响。

银行业务专栏

支付清算、计息与限额、账户/卡、反洗钱(AML)/KYC/CRS 等银行核心业务梳理。

银行的「业务规则」才是系统真正的复杂度来源。本专栏把核心业务从底层机制讲起,让技术决策有业务依据。

下面是本专栏的文章:

支付清算全流程拆解

从一笔转账发起,到资金在金融机构间真正落账,拆解支付、清分、结算三阶段与大小额、银联、网联各自的角色,以及日终对账为何是生命线。

普通人眼里的「转账」是一个瞬间动作:输金额、点确认、余额变了。但背后的资金流动要经过支付、清分、结算三个阶段,跨过多个系统,才可能真正「钱到了」。理解这套链路,是做银行支付系统的前提,也是排查「钱怎么少了」「为什么还没到账」的地图。

一笔转账背后发生了什么

当你在 App 上给朋友转 1000 元,这笔指令从你的银行出发,要穿越自己的核心系统、跨行清算通道、对方银行的核心系统,最后落到对方账户。中间任何一环的状态错位,都会让这笔钱「卡在半路」。所以支付系统设计的第一个原则,是把每个阶段的状态显式建模,而不是用一个「已转账」含糊带过。

支付系统最危险的 bug,不是算错金额,而是状态机说不清一笔钱现在到底在哪:是已扣、已发、已清算、已结算,还是已退回。

三个阶段:支付、清分、结算

这是整条链路的主干,三者的边界必须划清。

支付:机构内部的记账

付款方银行收到指令后,先校验账户状态、余额、限额、风控,然后在自己的核心账本上记一笔「待清算」的借贷。这一步完全发生在单个机构内部,还没有任何人把钱真正挪动。

清分:轧差与净额

清分是交易双方机构把彼此的借贷指令汇总、计算净额的过程。它有两种模式:

  • 全额清分:每一笔借贷都单独计算,资金流与业务流一一对应,安全但占用流动性。
  • 净额清分(轧差):把一段时间内的多笔借贷相互抵扣,只交差额。比如 A 欠 B 100 万、B 欠 A 80 万,净额就是 A 再付 B 20 万。

结算:资金的真实位移

结算是资金在央行或清算机构的账户上真正转移,借贷最终平衡。只有结算完成,这笔钱才算「落地」。在此之前,它只是账面上的债权。

阶段发生位置是否动真实资金关键产物
支付付款方银行内部否(仅记账)支付流水、待清算挂账
清分清算机构/双方对账否(算差额)净额头寸、清分文件
结算央行/清算账户准备金账户余额变动

清分算的是「谁欠谁多少」,结算做的是「把钱真正挪过去」。只清不分或只分不结,都会让账面悬空。

大小额与网银清算系统

中国人民银行的大小额支付系统,是跨行资金流动的主动脉。不同系统对应不同的金额、时效与成本取舍。

系统处理方式金额与时效适用场景
大额支付系统(HVPS)逐笔实时全额大额、实时、营业时间对公大额、同业头寸调拨
小额支付系统(BEPS)批量净额轧差小额、定时撮合日常零售、代收付
网上支付跨行清算逐笔/批量7x24、准实时网银、手机银行跨行

大额走全额是为了安全和实时,小额走净额是为了降低流动性占用和通道成本。设计路由时,按金额和时效要求分流,而不是一刀切全走大额。

卡组织与三方支付清算

银行卡跨行交易走银联(银行卡)或网联/银联(网络支付)。资金路径是:

持卡人 -> 发卡行 -> 卡组织(银联/网联) -> 收单行 -> 商户
           支付指令   清分轧差          结算资金

商户侧的收单机构先记账,再通过对账文件与卡组织清分,最后在准备金账户结算。三方支付(钱包)则多了「支付机构备付金账户」这一层,资金在用户钱包余额、支付机构存管账户、银行之间多跳流转。

清分净额怎么算

净额不是拍脑袋,而是可复现的计算。一个简化的轧差脚本:

def net_position(debits, credits):
    # debits: 本机构应付他行; credits: 他行应付本机构
    net = sum(credits) - sum(debits)
    if net >= 0:
        return f"他行应付本机构 {net} 元"
    return f"本机构应付他行 {-net} 元"

print(net_position([100, 200], [350]))   # 他行应付本机构 50 元

结算时,正头寸等待收款,负头寸从准备金账户划出,借贷两方最终在央行账上平衡。

对账:日终的生命线

日终,各方用对账文件(通常固定格式、带汇总校验和笔数)核对借贷。不平的这笔叫「差错」,进入差错处理流程,而不是默默掩盖。

  • 对账文件必须可追溯、可重跑,最好带哈希校验。
  • 长款(我方多收)和短款(我方少收)处理规则不同,不能混为一谈。
  • 自动勾对 + 人工差错处理是标配,勾对规则要可解释。
  • 对账滞后会让资金敞口放大:T+1 才发现 T 日差错,补救成本指数上升。

对账不是事后补丁,而是支付系统的「最后一道真理」。它用独立的数据源交叉验证主流程,发现那些主流程自己发现不了的错。

常见坑

  1. 把清分当结算:以为指令发出就到账,实际资金还在途,引发透支或重复支付。
  2. 忽略时区与营业日:大额系统非营业时间不运行,跨日交易要排队,别用本地时间当清算日。
  3. 对账滞后:T+1 才跑 T 日对账,资金敞口已经放大一整天。
  4. 状态机缺失:把「已转账」当一个终点,出问题时无法定位卡在支付、清分还是结算。

结论

支付清算的本质,是把「信任」从机构内部延伸到机构之间:支付建立债权,清分计算净额,结算完成资金转移。把这三者的边界、状态机和对账机制刻进系统设计,跨行交易才不会在某个环节 quietly 出错。下一讲我们看计息与限额,那是另一道「算得清才能管得住」的闸门。

计息与限额:算得清才能管得住

计息口径和限额控制是银行账务的两道闸门,算不清就管不住,管不住就出风险。拆开来看它们各自的关键点。

银行系统里最容易「看起来很简单、做起来全是坑」的两件事:计息和限额。它们一个决定「钱怎么生钱」,一个决定「钱不能出什么事」。两者共同的特点是:规则必须可被验证,结果必须可被复核

计息:口径比公式重要

公式 本金 × 利率 × 天数 / 基数 人人会写,难的是口径统一。

  • 计息基数:是每日余额、还是日均余额?贷款多用实际天数,存款各有约定。
  • 闰年与节假日:一年按 365 还是 360?结息日遇节假日顺延还是提前?
  • 利率变动:固定利率、浮动利率重定价日如何衔接,分段计息怎么切。
BigDecimal interest = dailyBalance
        .multiply(rate)
        .multiply(BigDecimal.valueOf(days))
        .divide(BigDecimal.valueOf(360), 6, RoundingMode.HALF_UP);

计息 bug 最可怕的结局,不是算错一笔,而是「每天错一点点,半年后对不上,却找不到从哪天开始错」。所以计息必须有可复现的逐日明细。

限额:控制点要前置

限额分好几类:单笔、日累计、月累计、余额上限、渠道限额。关键不在「存一个上限数字」,而在控制点必须前置到交易校验环节,而不是事后统计。

限额类型控制点典型场景
单笔限额交易发起前防单笔大额盗刷
日累计交易前累计判断渠道风控
余额上限入账后校验产品合规约束
  • 限额规则要可配置,且变更留痕、可追溯生效时间。
  • 累计类限额必须考虑并发:用原子计数或数据库约束,别靠「先查后扣」。
  • 超限处理要明确:拒绝、转人工、还是降级,不能悄悄放行。
if (dailyUsed.add(amount).compareTo(dayLimit) > 0) {
    throw new LimitExceededException(cardNo, dailyUsed, dayLimit);
}

限额系统宁可「误杀一笔正常交易」,也不能「放过一笔该拦的」。前者是体验问题,后者是风险事件。

把计息的逐日明细和限额的前置校验都做扎实,核心账务才谈得上「算得清、管得住」。

AML/KYC:合规不是绊脚石

反洗钱与客户身份识别常被当成业务阻力,其实它们是对系统工程的一部分。从身份、交易到名单监控,讲清落地要点。

一提 AML/KYC,业务侧往往皱眉:开户变慢、交易被拦。但合规不是外挂的「麻烦」,而是银行必须内建的能力。把它当系统一等公民来设计,风控和体验才能双赢。

KYC:先搞清楚你是谁

KYC 是起点,不是收集证件复印件,而是建立持续可验证的客户画像

  • 身份核验:证件真实性、活体、工商比对。
  • 风险分级:按行业、地域、业务性质定级。
  • 受益所有人:穿透到最终自然人,防借壳。
customer_risk:
  level: high
  factors: [virtual_asset, high_risk_region, pep: true]

AML:盯着钱往哪走

反洗钱看交易行为与资金路径,而非单次金额大小:名单监控、交易监测(规则+模型打分)、可疑报告(STR)走调查上报流程。

维度信号处置
频率短期多笔等额进出触发复核
对手方命中制裁名单阻断上报
金额拆分规避限额合并评估

真正的反洗钱是「识别不合理模式」:规整发工资的小额账户突然集中收款,比一笔大额更可疑。

工程落地要点

名单更新要准实时且对存量重跑;规则先求可解释、可回测;误报率持续度量;告警工单化、可审计。合规做扎实,业务国际化反而少踩坑。

一文理清 ECIF:客户信息为什么要「集中」

从多头开户、信息不一致到统一客户视图,ECIF 解决的是银行最基础的「客户是谁」问题。

在没有 ECIF 的年代,网点、网银、信用卡中心各自维护一份客户信息。同一个客户,在不同系统里姓名、证件、联系方式都不一样,营销和风控都无从谈起。

ECIF 解决什么

ECIF(企业客户信息整合)把分散在各业务系统的客户主数据收敛到一处,对外提供唯一客户视图

  • 唯一标识:用客户号(Party ID)统一自然人/机构,而非证件号。
  • 主次关系:支持一人多户、一户多卡,但主数据唯一。
  • 服务化:其他系统通过接口查询,不再各自落库。

技术上的关键取舍

  • 读写分离:主数据写入强一致,查询可走缓存/只读副本。
  • 变更可溯源:客户信息变更需要留痕,满足监管审计。

ECIF 不是「又一个数据库」,而是银行数字化的最底层地基。

数据专栏

数据治理、加密与安全、分库分表、消息与流处理相关的技术与方法论。

数据是现代银行的资产,也是风险。本专栏关注「让数据可用、可信、可控」的工程实践。

下面是本专栏的文章:

数据治理:从资产目录到口径统一

从资产目录、元数据血缘到指标口径统一,讲清银行数据治理怎么从运动式填表,走向内建式、可验证的治理工程。

很多银行的数据治理项目,最后都沦为「补元数据、填责任表、交汇报 PPT」。运动一过,元数据过期、口径继续打架、下游照样不敢用这份数据。治理之所以失效,是因为它被当成了一份额外的「填表工作」,而不是数据生产流水线本身的属性。真正有效的治理,是让目录、血缘、口径和质量规则内建到系统里,而不是靠人肉维护。

治理为什么总做成运动

根本原因是把「治理」和「生产」割裂了。业务系统吐数据,治理团队在下游追着补标签、对口径。一旦人员变动或项目结项,治理成果立刻失真。

治理的目标不是「漂亮的报告」,而是让下游系统敢用这份数据。如果一份数据没人敢用来做决策,它就算被打了满分标签,也还是负债。

要扭转这个局面,得从三件最实在的事入手:盘清家底(资产目录)、追清来路(元数据血缘)、对齐说法(口径统一)。

资产目录:先盘清家底

资产目录回答的是「我们到底有哪些数据、归谁管、能不能用」。它不是 Excel 清单,而是带权属和分级的结构化注册表。

资产类型例子治理关注点
贴源表核心系统流水来源系统、更新频率
派生表客户宽表加工逻辑、依赖
指标月活、不良率口径定义、负责人
文件/接口监管报送文件敏感度、共享范围

目录必须和真实的元数据打通,否则就会出现「目录里说有、库里早就删了」的尴尬。一个可行的做法是:目录条目由采集任务自动生成,人工只补充业务语义,而不是反过来手工录入。

元数据与血缘:字段从哪来、到哪去

元数据解决「这个字段是什么」,血缘解决「它怎么变来的、又被谁用了」。两者合起来,才能做影响分析和溯源。

血缘怎么采

血缘最好自动采集,而不是靠文档口述。主流方式有两种:

  • 静态解析:扫描 SQL、ETL 脚本、Spark/DAG 定义,提取表与字段级的输入输出关系。
  • 运行时采集:在任务执行时记录实际读写的上下游,准确率最高但侵入性较强。

影响分析

有了血缘,一次核心系统字段口径变更,能立刻列出受影响的下游报表和指标。没有血缘,这类变更只能靠「老员工记忆」,风险极高。

血缘的价值不在炫技,而在于把「改一个字段要通知谁」从玄学变成可查询的图。它是治理能规模化的前提。

指标口径统一:最难的最后一公里

银行里「不良率」「活跃客户」「存款日均」这类指标,不同部门算出来常常不一样。根因不是谁算错,而是口径没有单一可信来源

口径冲突的例子

「月活客户」可能被定义为:

  • 渠道侧:当月登录 App 即算活跃;
  • 零售侧:当月有动账交易才算活跃;
  • 监管侧:按监管文件口径,且需去重。

三个数字放在一起对比,结论自然矛盾。更糟的是,没人说得清哪个是「官方版本」。

指标字典与单一来源

解决思路是建立指标字典:每个指标一条定义,包含口径 SQL、维度、过滤条件、负责人和生效时间。下游统一从字典取数,禁止各自重写逻辑。

-- 指标字典中「存款日均」的口径定义(单一来源)
SELECT cust_id,
       SUM(balance) / COUNT(DISTINCT cal_date) AS avg_daily_balance
FROM   dwd_account_daily
WHERE  cal_date BETWEEN :start AND :end
  AND  balance_type = 'SAVING'
GROUP  BY cust_id;

口径变更要走版本管理,旧口径保留可追溯,新口径标注生效日,避免历史报表被悄悄改义。

数据质量内建到流水线

质量不是事后抽查,而是 ETL/湖仓任务里的「门禁」。规则不过,数据不入库。

def quality_gate(df):
    checks = {
        "not_null": df["cust_id"].notna().all(),
        "unique":  df["cust_id"].is_unique,
        "balance_nonneg": (df["balance"] >= 0).all(),
    }
    failed = [k for k, ok in checks.items() if not ok]
    if failed:
        raise DataQualityError(f"质量门禁未过: {failed}")
    return df   # 通过才落库
  • 规则要可配置、可观测,失败要告警而非静默跳过。
  • 关键字段(证件号、金额)非空、唯一、非负,是底线规则。
  • 质量趋势要可视化,恶化要能回溯到哪天哪个任务开始。

质量内建的核心,是把「信任」从事后审计提前到数据落库之前。一条坏数据一旦进了湖,下游十张报表一起错。

安全与合规:治理的硬约束

治理绕不开安全。银行对敏感字段的处理有硬要求:

  • 静态加密:证件号、卡号、手机号落盘即加密,密钥与数据分离管理。
  • 动态脱敏:查询侧按角色脱敏,开发人员查生产只能看到掩码。
  • 分级分类:数据按敏感度分级,决定谁能看、能传到哪、能留多久。

这些不是治理的附加项,而是治理成立的边界条件。

落地节奏建议

治理切忌「全面铺开、一步到位」,那样必成运动。更稳的节奏是:

  1. 先选一个高价值域(如客户、账户),把目录、血缘、核心指标跑通。
  2. 把质量门禁接进现有流水线,不新建独立系统,降低阻力。
  3. 指标字典从冲突最严重的一批指标开始统一,尽快产出可见收益。
  4. 用自动化替代手工填表,让目录和血缘随任务自动更新。

小步快跑、用真实收益说话,比一份宏伟蓝图更能让治理活下来。

结论

数据治理不是运动,也不是额外的填表负担,而是把「可发现、可追溯、可信任」变成数据流水线的默认属性。从资产目录盘清家底,用元数据血缘追清来路,靠指标字典统一说法,再把质量门禁内建进去——四件事做实,数据才真正从负债变成资产。下一讲我们聊消息队列选型,那是让这些数据可靠流动的另一块基石。

消息队列选型:Kafka vs Pulsar vs RabbitMQ

从模型、吞吐、顺序性和运维复杂度对比三款主流消息中间件,给出银行场景下的选型思路。

消息队列是分布式系统的神经系统,但 Kafka、Pulsar、RabbitMQ 三者的设计哲学差异极大。选错不是「换个客户端」的事,而是架构重做。下面从几个工程最关心的维度拆开看。

模型差异是根本

  • RabbitMQ:经典消息代理,面向「消息」和「队列」,支持丰富路由(直连、主题、头部、扇出)。适合任务分发、低延迟小消息。
  • Kafka:日志型、分区有序、拉取消费,面向「流」和「事件溯源」。适合高吞吐、可重放。
  • Pulsar:计算存储分离,分层架构,原生多租户和跨地域复制。适合既要 Kafka 的流、又要 RabbitMQ 的队列语义的统一平台。
# Kafka 顺序消费依赖分区
bin/kafka-console-consumer.sh \
  --topic orders --bootstrap-server b1:9092 \
  --partition 0 --offset earliest

关键维度对比

维度KafkaPulsarRabbitMQ
吞吐极高极高中等
消息模型流/日志流+队列队列
顺序性分区内有序分区/Key 有序队列有序
运维复杂度高(含 BookKeeper)
消费模型拉取、可重放拉取、可重放推送

选型思路

  1. 事件流、日志、可重放 → Kafka 稳。审计、CDC、行为埋点这类场景它的重放能力几乎是刚需。
  2. 统一消息平台、多租户、跨地域 → Pulsar 更合适,但团队要扛得住运维。
  3. 任务队列、复杂路由、低延迟 → RabbitMQ 更直接。
// 用消息头做复杂路由(RabbitMQ 风格)
channel.basicPublish("orders.topic",
    "order.created.vip",  // routingKey
    props, body);

别用 RabbitMQ 扛日均百亿事件流,也别用 Kafka 做需要复杂路由的任务分发——用对的工具,比用最强的工具重要。

银行场景里,核心事件总线多用 Kafka 保证可重放与审计;内部任务编排可用 RabbitMQ。Pulsar 适合已经长成「平台」的团队。选型时还要把「团队能不能运维」算进成本,而不是只看基准测试数字。

分库分表策略与热点治理

从分片键选择到扩容与热点,讲清分库分表必须提前想清楚的几件事,以及银行海量账户下的真实取舍。

当单表涨到几亿行、单库连接打满,分库分表就从「可选项」变成「生存项」。但分片一旦定错键,后期重构的代价接近重写。所以策略要在一开始就想清楚。

分片键是第一步,也是最重要的一步

分片键决定数据怎么散、请求怎么走。选错键,要么数据倾斜,要么跨分片查询泛滥。

  • 选高频等值查询字段:比如账户号、客户号,保证大多数请求落单分片。
  • 避免低基数或单调递增:性别这种分片键会制造大分片;自增 ID 做键会集中在最新分片,形成写入热点。
  • 兼顾关联查询:同一客户的账户、交易最好同片,减少跨片 JOIN。
-- 按客户号哈希分 1024 片
SELECT * FROM trans_${hash(cust_id)%1024}
WHERE cust_id = ? AND trans_date >= ?;

扩容:预分片优于临时拆

一开始就按「未来三年规模」定足够多的逻辑分片(如 1024、4096),物理上先少后多。扩容只是把逻辑分片映射到新物理库,数据迁移量远小于重新分片

策略优点缺点
预分片扩容平滑初期略冗余
范围分片易理解易热点
哈希分片均衡范围查询跨片
// 逻辑分片到物理库的映射可配置
String logic = "trans_" + (hash(custId) % 1024);
String phys  = routeTable.lookup(logic); // 映射到具体物理库

热点治理

即使哈希均匀,业务上仍会有「明星账户」「大商户」成为单点热点。

  • 二级分片:对超大 key 再按时间或子维度拆。
  • 读写分离 + 缓存:把热点读打到只读副本或缓存。
  • 限流与隔离:热点账户单独队列,避免拖垮整片。

分库分表解决的是「装得下」,热点治理解决的是「扛得住」。两者都做,系统才既大又稳。

最后提醒:分片后事务、全局唯一 ID、跨片聚合统计都要重新设计,别等上线才发现漏了。分布式 IDs 建议用号段或雪花算法,跨片统计要么预聚合、要么上 OLAP 旁路,不要把在线事务库当报表库用。