技术博客

可用/冻结/在途:资金与持仓的六个余额

第 14 / 141 篇 · 2026-06-14 · secfin · 余额模型并发控制冻结

打开任何一个券商 App 的资金页,你会看到"总资产、可用、可取、冻结"好几个数。用户以为这是展示设计,其实这是账务系统的核心状态模型:账户上的钱按"处于什么阶段"分成了互斥的几份,每一份有严格的增减规则。这篇把资金和持仓两边的余额模型讲透,重点是冻结解冻的并发语义——这是核心系统被踩得最多的一类 bug。

六个余额的语义

资金侧三个基本量:

字段 含义 增减时机
当前余额(balance) 账户里真实存在的钱 清算交收、存取款、结息时变
冻结金额(frozen) 已被委托/业务占用的钱 买入委托冻结、撤单/废单解冻
可用金额(available) 还能拿来下单的钱 = 当前 − 冻结(或独立维护)

持仓侧对称的三个:

字段 含义 增减时机
当前持仓(qty) 中登登记的股份数 交收过户、红股到账时变
冻结数量(frozen_qty) 被卖出委托占用的股数 卖出委托冻结、撤单解冻
可用持仓(avail_qty) 还能卖的股数 = 当前 − 冻结

还有两个"在途"量,是 T+1 制度的产物:

  • 资金在途(on_the_way / T+1 可取):今天卖出股票回笼的钱,当前余额里已经有,但今天不能取也不能买部分品种,明天清算完成后才转可用可取。
  • 持仓在途(pending_qty):今天买入的股票,当前持仓里已经有(柜台实时加),但 T+0 不能卖,明天才进可用持仓。

恒等关系(以资金为例):

$$ \text{当前余额} = \text{可用} + \text{冻结} + \text{在途} $$

这个等式是日终校验的抓手之一:三个分量加起来不等于当前余额,就是账出错了。

资金余额的三个分量与典型流转可用 available可下单、可取(T+1后)冻结 frozen被活委托占用扣款(冻结转实扣)成交清算时在途 on_the_way卖出回笼,T+1 转可用买入委托成交卖出成交回款(当日)T+1 清算完成撤单/废单:冻结 → 可用(图中省略,是冻结的唯一合法回路)

冻结的并发语义:一条 SQL 定生死

买入下单时的资金冻结,要求"校验余额充足"和"扣减可用"是原子的。并发下分开做就会双花:两笔并发委托都读到可用 10 万,各自买 8 万,都通过校验,账户透支 6 万。

标准实现是条件更新:

UPDATE fund_account
   SET available = available - :amt,
       frozen    = frozen + :amt
 WHERE fund_acct = :acct
   AND available >= :amt;

WHERE available >= :amt 把校验和扣减合成一条原子语句,影响行数为 0 即余额不足,拒绝委托。不需要先 SELECT 再 UPDATE,也不需要显式加锁——单行更新的行锁语义天然串行化了同一账户的并发冻结。这就是"条件更新即乐观锁"的用法:乐观体现在不加锁等待,安全体现在数据库的行级原子性。

卖出侧的持仓冻结完全对称,avail_qty >= :qty 条件更新。

解冻为什么比冻结难

冻结只有校验失败一种异常,解冻的坑多得多:

解冻金额必须精确对应原冻结。 撤单解冻的是"未成交部分对应的冻结",部成部撤时冻结要拆成两段处理:成交部分走"冻结转扣款",未成交部分走"冻结转可用"。实现上冻结记录要有独立的冻结流水号,解冻按流水号定位金额,不能拍脑袋按"委托金额 − 成交金额"重算——费率精度和零股规则会让重算值和原冻结差出分位钱。

解冻必须幂等。 撤单回报会重复(回报幂等那篇展开),同一个撤单确认来两次,第二次解冻就是凭空造钱。防御是状态驱动:只有"撤单中"状态的委托才执行解冻,执行时同事务把委托迁移到终态;重复回报发现委托已是终态,直接丢弃并告警。幂等不靠"记住处理过的消息号",靠"状态机拒绝非法迁移"——前者在多实例部署时根本不可靠。

对账不平时的强制解冻要走审批。 日终发现"委托已终结但冻结残留"(脏数据),需要批量解冻修复。这个修复通道必须独立于正常交易通道,逐笔留痕、双人复核,因为它是绕过状态机直接动钱的口子,被滥用就是挪用资金的通道。

在途:T+1 制度在余额模型里的投影

新人常问:卖出成交的钱,为什么柜台的当前余额立刻加了,客户却说明天才能取?因为 A 股 T+1 交收:成交只是撮合成功,钱的正式划付要等今晚清算、明天交收。柜台"实时加余额"是为了让客户当天能再用这笔钱买入(卖出回款当日可买,这是 A 股规则允许的),但这笔钱没有完成交收,不能转出到银行。

所以在途余额的本质是信用额度:券商在交收完成前,基于"中登保证交收"的信用,提前让客户使用这笔钱。在途转可用发生在日终清算任务里,按交易日历推进,不是按自然时间——周五卖出的在途,周一才转可用(交易日历那篇讲日切)。

持仓在途同理但方向相反:今天买入的股票持仓已加,可用不加,明天交收完成后转入可用。这里有个容易踩的细节:红股/送转股到账日当天的在途处理——送转股份上市日才可用,清算文件里有明确日期,不能简单按"T+1 自动转"的通用逻辑处理,公司行动的在途要按文件日期精确控制。

实现清单

把上面所有语义压实成实现纪律,是这几条:

  1. 六个字段全用 DECIMAL,任何分量不允许为负(数据库 CHECK 约束兜底);
  2. 可用不做"查询时现算",要物化为列——下单校验是纳秒级路径,读一行不能变成一次聚合;
  3. 所有分量变动必须同事务写变动流水(哪个委托/哪笔清算导致、变动前后值),流水是排查"钱去哪了"的唯一现场;
  4. 恒等式 $\text{当前} = \text{可用} + \text{冻结} + \text{在途}$ 进日终校验,不平即告警拦流程;
  5. 强制修复通道(批量解冻/调账)独立于交易通道,审批 + 留痕。

余额模型是账户系统的"物理定律",后面讲的 EOD、对账、错账处理,全是在维护这六个数的正确性。把这层的语义和纪律定清楚,上层业务才敢往复杂了做。