设计资产系统


用户在买入BTC时,需要花费USD,而卖出BTC后,获得USD。当用户下单买入时,系统会先冻结对应的USD金额;当用户下单卖出时,系统会先冻结对应的BTC。之所以需要有冻结这一操作,是因为判断能否下单成功,是根据用户的可用资产判断。每下一个新的订单,就会有一部分可用资产被冻结,因此,用户资产本质上是一个由用户ID和资产ID标识的二维表:

上述二维表有一个缺陷,就是对账很困难,因为缺少了一个关键的负债账户。对任何一个资产管理系统来说,要时刻保证整个系统的资产负债表为零。

对交易所来说,用户拥有的USD和BTC就是交易所的系统负债,只需引入一个负债账户,记录所有用户权益,就可以保证整个系统的资产负债表为零。假设负债账户以ID为1的系统用户表示,则用户资产表如下:

用户ID资产ID可用冻结
1USD-22900.30
1BTC-5500
101USD8900.31200
101BTC5000
102USD128000
103BTC050

引入了负债账户后,我们就可以定义资产的数据结构了。

在数据库中,上述表结构就是资产表的结构,将用户ID和资产ID标记为联合主键即可。

但是在内存中,我们怎么定义资产结构呢?

可以使用一个两层的定义如下:

第一层Map的Key是用户ID,第二层Map的Key是资产ID,这样就可以用Asset结构表示资产:

  1. public class Asset {
  2. // 可用余额:
  3. BigDecimal available;
  4. // 冻结余额:
  5. BigDecimal frozen;
  6. public Assets() {
  7. this(BigDecimal.ZERO, BigDecimal.ZERO);
  8. }
  9. public Assets(BigDecimal available, BigDecimal frozen) {
  10. this.available = available;
  11. this.frozen = frozen;
  12. }
  13. }

下一步,我们在AssetService上定义对用户资产的操作。实际上,所有资产操作只有一种操作,即转账。转账类型可用Transfer定义为枚举类:

转账操作只需要一个tryTransfer()方法,实现如下:

  1. public boolean tryTransfer(Transfer type, Long fromUser, Long toUser, AssetEnum assetId, BigDecimal amount, boolean checkBalance) {
  2. // 转账金额不能为负:
  3. if (amount.signum() < 0) {
  4. throw new IllegalArgumentException("Negative amount");
  5. }
  6. // 获取源用户资产:
  7. if (fromAsset == null) {
  8. // 资产不存在时初始化用户资产:
  9. fromAsset = initAssets(fromUser, assetId);
  10. }
  11. Asset toAsset = getAsset(toUser, assetId);
  12. if (toAsset == null) {
  13. // 资产不存在时初始化用户资产:
  14. toAsset = initAssets(toUser, assetId);
  15. }
  16. return switch (type) {
  17. case AVAILABLE_TO_AVAILABLE -> {
  18. // 需要检查余额且余额不足:
  19. if (checkBalance && fromAsset.available.compareTo(amount) < 0) {
  20. // 转账失败:
  21. yield false;
  22. }
  23. // 源用户的可用资产减少:
  24. fromAsset.available = fromAsset.available.subtract(amount);
  25. // 目标用户的可用资产增加:
  26. toAsset.available = toAsset.available.add(amount);
  27. // 返回成功:
  28. yield true;
  29. }
  30. // 从可用转至冻结:
  31. case AVAILABLE_TO_FROZEN -> {
  32. if (checkBalance && fromAsset.available.compareTo(amount) < 0) {
  33. }
  34. fromAsset.available = fromAsset.available.subtract(amount);
  35. toAsset.frozen = toAsset.frozen.add(amount);
  36. yield true;
  37. }
  38. // 从冻结转至可用:
  39. case FROZEN_TO_AVAILABLE -> {
  40. if (checkBalance && fromAsset.frozen.compareTo(amount) < 0) {
  41. yield false;
  42. fromAsset.frozen = fromAsset.frozen.subtract(amount);
  43. toAsset.available = toAsset.available.add(amount);
  44. yield true;
  45. }
  46. default -> {
  47. throw new IllegalArgumentException("invalid type: " + type);
  48. }
  49. };
  50. }

除了用户存入资产时,需要调用tryTransfer()并且不检查余额,因为此操作是从系统负债账户向用户转账,其他常规转账操作均需要检查余额:

冻结操作可在tryTransfer()基础上封装一个方法:

  1. public boolean tryFreeze(Long userId, AssetEnum assetId, BigDecimal amount) {
  2. return tryTransfer(Transfer.AVAILABLE_TO_FROZEN, userId, userId, assetId, amount, true);
  3. }

解冻操作实际上也是在tryTransfer()基础上封装:

可以编写一个AssetServiceTest,测试各种转账操作:

  1. public class AssetServiceTest {
  2. @Test
  3. void tryTransfer() {
  4. // TODO...
  5. }

并验证在任意操作后,所有用户资产的各余额总和为0

最后是问题解答:

因为我们要实现的交易引擎是100%全内存交易引擎,因此所有用户资产均存放在内存中,无需访问数据库。

为什么要使用ConcurrentMap?

使用ConcurrentMap并不是为了让多线程并发写入,因为AssetService中并没有任何同步锁。对AssetService进行写操作必须是单线程,不支持多线程调用tryTransfer()

但是读取Asset支持多线程并发读取,这也是使用ConcurrentMap的原因。如果改成HashMap,根据不同JDK版本的实现不同,多线程读取HashMap可能造成死循环(注意这不是HashMap的bug),必须引入同步机制。

我们在AssetEnum中以枚举方式定义了USD和BTC两种资产,如果要扩展到更多资产类型,可以以整型ID作为资产ID,同时需要管理一个资产ID到资产名称的映射,这样可以在业务需要的时候更改资产名称。

参考源码

可以从GitHub或下载源码。

GitHub ▸ ▸ warpexchange

▸ build)

)

▤ schema.sql)

)

▤ pom.xml)

)

▸ src/main)

)

▸ enums)

)

▸ support)

)

▸ resources)

)

▸ config)

)

▸ java/com/itranswarp/exchange/config)

)

▸ resources)

)

▤ pom.xml)

)

▤ application-default.yml)

)

▤ application.yml)

)

▤ quotation.yml)

)

▤ trading-engine.yml)

)

▤ ui-default.yml)

)

▸ parent)

)

▸ push)

)

▸ java/com/itranswarp/exchange/push)

)

▸ resources)

)

▤ pom.xml)

)

▸ src/main)

)

▤ QuotationApplication.java)

)

▤ application.yml)

)

▸ trading-api)

)

▤ TradingApiApplication.java)

)

▤ application.yml)

)

▸ trading-engine)

)

▸ main)

)

▸ assets)

)

▤ AssetService.java)

)

▤ TradingEngineApplication.java)

)

▤ application.yml)

)

▤ AssetServiceTest.java)

)

▸ trading-sequencer)

)

▸ java/com/itranswarp/exchange)

)

▸ resources)

)

▤ pom.xml)

)

▸ src/main)

)

▤ UIApplication.java)

)

▤ application.yml)

)

▤ .gitignore)

)

▤ README.md)

本节我们讨论并实现了一个基于内存的高性能的用户资产系统,其核心只有一个转账方法,业务逻辑非常简单。

读后有收获可以支付宝请作者喝咖啡: