Skip to content

Quota/Balance模块

本模块用于在内存中维护用户的配额和余额信息,提供查询和更新接口。

注意!请务必谨慎调整本地数据库轮转时间间隔

轮转间隔的调整会影响数据库文件命名,因此未经测试的轮转间隔调整可能会导致数据丢失且难以恢复,且会导致不可预知的严重后果

如需调整轮转间隔,请务必在测试环境中充分验证后再应用到生产环境。强烈建议停服更新、等待所有余额刷回数据库后执行操作,以避免数据丢失产生错误。

注意!请务必谨慎从数据库中修改配额和余额信息

从数据库中直接修改配额和余额信息将会导致不可预知的严重后果

如需余额订正,请遵循文档指示

特性

  • 提供基于TCC的分布式事务接口。
  • 提供Must(必须成功)的接口。
  • 提供基于自定义Key的原子内存锁:不同用户、不同资产的配额和余额可以并发更新,没有锁等待。
  • 本地TCC事务存储,解决了TCC分布式事务的幂等性问题。
  • 子事务屏障,解决了TCC分布式事务的空补偿、悬挂等问题 https://dtm.pub/practice/barrier.html
  • 本地WAL(Write Ahead Log)结合数据版本跟踪,程序异常退出重启后,仍能将内存和数据库中的数据恢复到崩溃前,并同步到一致状态。
  • 定期将内存中的被修改的配额和余额信息持久化到数据库中,避免数据丢失。
  • 内存中不存在的余额,可以从数据库(或自定义途径)加载到内存中,提供快速查询和更改。

数据存储

由Walock模块控制,提供TCC+WAL的分布式事务支持。本地WAL文件会定期轮转,避免单个文件过大。

TCC事务文件、WAL文件都基于LevelDB实现。

  1. 生成与业务相关的LockKey,例如LockKey = accountId + "#" + assetId
  2. 根据LockKey找到miniLock(ensureUserMiniLock),对该miniLock加锁。如不存在,执行按需加载逻辑(ensure)。
  3. 根据请求头,生成TCCKey(BuildTccBarrierReceiver)
    • TCCBarrierKey = fmt.Sprintf("%s-%s-%s-%s", transactionType, globalId, branchId, branchType),,例如B-GlobalId-BranchId-T
    • 检查TCCBarrierKey是否存在于TccBarrierLevelDb,如果存在,则是一个重复的TCC请求。(CheckBarrierTry)
  4. 根据请求,生成WAL (GenerateWalTry)及对应的WalKey=fmt.Sprintf("%s-%d", lockKey, version)
  5. 更新本地WAL数据库
    • 在本地Dirty数据库中标记Value为Dirty,用于在重启时识别已经被修改过的记录。
    • 在本地WAL数据库中建立TccBarrier->WalKey的映射,当Confirm或Cancel时,可以通过TCCBarrier找到对应的WAL记录进行回滚。
    • 在本地WAL数据库中建立WalKey->Wal的映射,用于重启恢复时追上所有丢失的version的WAL记录。
    • 事务更新本地WAL数据库
  6. 更新内存
    • 在内存中标记Value为Dirty,用于在定时任务触发时识别已经被修改过的记录。
    • 在内存中更新配额和余额信息。
  7. 解锁miniLock
  8. 返回TCCKey,执行结果给调用方

文件Rotating

  • TCC事务文件和WAL文件会定期轮转,避免单个文件过大。
  • TCC事务文件和WAL文件的轮转时间间隔可以通过配置文件设置。
  • 轮转时将会按照时间戳命名文件,避免文件名冲突。
  • 程序在运行时会同时检查两个文件中的文件,确保不会遗漏任何TCC事务或WAL记录。
  • 请务必谨慎调整轮转时间间隔,轮转间隔的调整会影响数据库文件命名,因此未经测试的轮转间隔调整可能会导致数据丢失且难以恢复。
  • 如需调整轮转间隔,请务必在测试环境中充分验证后再应用到生产环境。强烈建议停服更新,以避免数据丢失。
  • 停服更新时,请务必确保所有标记为Dirty的记录都已经被持久化到数据库中,且没有新增更新,以避免数据丢失。

日切(Cutoff)

本系统支持使用一表双余额模式进行日切(Cutoff),即,用户在更新余额时需要带上余额变动的动作时间点。

系统在执行更新时,会检查当前余额的上一次Cutoff时间点是否在动作时间点之前,如果是,则直接更新,否则,执行Cutoff搬运。

Cutoff搬运时,系统将把当前的余额复制到Cutoff列,并更新Cutoff时间点为当前Cutoff的时间(一般被round到日级别,或8小时级别)。

因此Cutoff列永远保留了上一次Cutoff时间点的余额信息。而balance列则是当前的余额信息。

有定时任务会定期将Cutoff列的余额信息持久化到数据库的快照表中,形成整点快照。

注意:由于时钟回拨,可能导致Cutoff搬运后仍有发生在日切点之前的余额变动请求抵达,此时会同时更新余额列和Cutoff列。

对于在一个Cutoff周期内没有更新过任何余额的用户,Cutoff列将会保留上一次Cutoff的余额信息。

快照

系统支持将Cutoff列的余额信息定期持久化到数据库的快照表中,形成整点快照。

执行每一次Snapshot时,系统会生成TimeKey,对于同一个TimeKey的快照任务,系统会检查是否已经存在快照记录,如果存在,则跳过该快照任务。

执行时,系统会先删干净TimeKey对应的余额快照条目(防止先前因为意外导致的不完整快照),然后将Cutoff列的余额信息批量插入到快照表中。批量插入时根据ID进行分片,避免单次插入过多数据导致性能问题。

手动修复余额

注意:手动修复余额是极其危险的动作,请务必谨慎操作!!!

手动修复余额可能导致如下严重问题:

  • 错误修复后,若锁定的余额小于应锁定余额,将导致清算模块的Must余额变更操作无法进行,从而引发现货清算无法继续进行,导致与之相关的币对的清算流程持续崩溃;
  • 用户余额被错误修改,导致用户无法正常交易或提现,或导致用户资产损失或双花风险;

如需手动修复余额,请务必遵循以下步骤:

  • 关闭所有相关交易对的下单/撤单功能
  • 关闭转入转出现货交易所功能(即,停止Order服务)
  • 等待所有相关交易对的订单被清算完毕, 可以通过查询数据库,确认所有订单已被清算完毕。
  • 等待Quota/Balance模块的回写数据库动作完成(10秒后,有日志例如:07-01 09:56:26.444 INF flushing back mapSize=246 refreshCount=0),refreshCount必须连续为0,表示没有增量未刷新的数据。
  • 停止Quota/Balance模块
  • 此时才可以手动修改数据库中的余额信息
  • 启动Quota/Balance模块,观察健康检查,如必要,可以手动查询该用户的余额信息,确认余额已被正确修复。
  • 恢复所有服务。

如需在线订正Balance余额,也可以通过Cron服务进行:

通过在数据库exchange_spot_clearing表中插入一条记录,指定用户ID、币种ID、订正金额和订正时间点,Cron服务会自动处理余额订正。

sql
INSERT INTO `4000_exchange_spot_clearing`.balance_fixes
(id, text_id, account_id, asset_id, order_id, amount_delta, locked_delta, free_delta, `time`, reason, confirmed, done)
VALUES(0, 'FIX001', 'USER111', 'BTC', 'ORDER001', '-100', '0', '-100', now(), 'Your reason here', 0, 0);

为确保订正操作的正确性,请务必遵循以下步骤:

  • 插入SQL时,确保confirmed字段为0,表示未确认。
  • 插入SQL后,如数据正确,可以将confirmed字段更新为1,表示已确认。
  • Cron服务会自动处理confirmed字段为1的订正操作,并将记录的done更新为1到balance_fixes表中。

🚀 构建现代化数字资产交易平台