设计订单系统


上一节我们实现了一个资产系统,本节我们来设计并实现一个订单系统。

订单系统的目的是为了管理所有的活动订单,并给每个新订单一个递增的序列号。由于在创建订单时需要冻结用户资产,因此,我们定义的OrderService会引用AssetService

  1. public class OrderService {
  2. // 引用AssetService:
  3. final AssetService assetService;
  4. public OrderService(@Autowired AssetService assetService) {
  5. this.assetService = assetService;
  6. }
  7. }

一个订单由订单ID唯一标识,此外,订单包含以下重要字段:

  • userId:订单关联的用户ID;
  • sequenceId:定序ID,相同价格的订单根据定序ID进行排序;
  • direction:订单方向:买或卖;
  • price:订单价格;
  • quantity:订单数量;
  • unfilledQuantity:尚未成交的数量;
  • status:订单状态,包括等待成交、部分成交、完全成交、部分取消、完全取消。

一个订单被成功创建后,它后续由撮合引擎处理时,只有unfilledQuantitystatus会发生变化,其他属性均为只读,不会改变。

当订单状态变为完全成交、部分取消、完全取消时,订单就已经处理完成。处理完成的订单从订单系统中删除,并写入数据库永久变为历史订单。用户查询活动订单时,需要读取订单系统,用户查询历史订单时,只需从数据库查询,就与订单系统无关了。

我们定义OrderEntity如下:

  1. public class OrderEntity {
  2. // 订单ID / 定序ID / 用户ID:
  3. public Long id;
  4. public long sequenceId;
  5. public Long userId;
  6. // 价格 / 方向 / 状态:
  7. public BigDecimal price;
  8. public Direction direction;
  9. public OrderStatus status;
  10. // 订单数量 / 未成交数量:
  11. public BigDecimal quantity;
  12. public BigDecimal unfilledQuantity;
  13. // 创建和更新时间:
  14. public long createdAt;
  15. public long updatedAt;
  16. }

处于简化设计的缘故,该对象既作为订单系统的订单对象,也作为数据库映射实体。

根据业务需要,订单系统需要支持:

  • 根据订单ID查询到订单;
  • 根据用户ID查询到该用户的所有活动订单。

因此,OrderService需要用两个Map存储活动订单:

  1. public class OrderService {
  2. // 跟踪所有活动订单: Order ID => OrderEntity
  3. final ConcurrentMap<Long, OrderEntity> activeOrders = new ConcurrentHashMap<>();
  4. // 跟踪用户活动订单: User ID => Map(Order ID => OrderEntity)
  5. final ConcurrentMap<Long, ConcurrentMap<Long, OrderEntity>> userOrders = new ConcurrentHashMap<>();

添加一个新的Order时,需要同时更新activeOrdersuserOrders。同理,删除一个Order时,需要同时从activeOrdersuserOrders中删除。

我们先编写创建订单的方法:

  1. /**
  2. * 创建订单,失败返回null:
  3. */
  4. public OrderEntity createOrder(long sequenceId, long ts, Long orderId, Long userId, Direction direction, BigDecimal price, BigDecimal quantity) {
  5. switch (direction) {
  6. case BUY -> {
  7. // 买入,需冻结USD:
  8. if (!assetService.tryFreeze(userId, AssetEnum.USD, price.multiply(quantity))) {
  9. return null;
  10. }
  11. }
  12. case SELL -> {
  13. // 卖出,需冻结BTC:
  14. if (!assetService.tryFreeze(userId, AssetEnum.BTC, quantity)) {
  15. return null;
  16. }
  17. }
  18. default -> throw new IllegalArgumentException("Invalid direction.");
  19. }
  20. // 实例化Order:
  21. OrderEntity order = new OrderEntity();
  22. order.id = orderId;
  23. order.sequenceId = sequenceId;
  24. order.userId = userId;
  25. order.direction = direction;
  26. order.price = price;
  27. order.quantity = quantity;
  28. order.unfilledQuantity = quantity;
  29. order.createdAt = order.updatedAt = ts;
  30. // 添加到ActiveOrders:
  31. this.activeOrders.put(order.id, order);
  32. // 添加到UserOrders:
  33. ConcurrentMap<Long, OrderEntity> uOrders = this.userOrders.get(userId);
  34. if (uOrders == null) {
  35. uOrders = new ConcurrentHashMap<>();
  36. this.userOrders.put(userId, uOrders);
  37. }
  38. uOrders.put(order.id, order);
  39. return order;
  40. }

后续在清算过程中,如果发现一个Order已经完成或取消后,需要调用删除方法将活动订单从OrderService中删除:

  1. public void removeOrder(Long orderId) {
  2. // 从ActiveOrders中删除:
  3. OrderEntity removed = this.activeOrders.remove(orderId);
  4. if (removed == null) {
  5. throw new IllegalArgumentException("Order not found by orderId in active orders: " + orderId);
  6. }
  7. // 从UserOrders中删除:
  8. ConcurrentMap<Long, OrderEntity> uOrders = userOrders.get(removed.userId);
  9. if (uOrders == null) {
  10. throw new IllegalArgumentException("User orders not found by userId: " + removed.userId);
  11. }
  12. if (uOrders.remove(orderId) == null) {
  13. throw new IllegalArgumentException("Order not found by orderId in user orders: " + orderId);
  14. }
  15. }

删除订单时,必须从activeOrdersuserOrders中全部成功删除,否则会造成OrderService内部状态混乱。

最后,根据业务需求,我们加上根据订单ID查询、根据用户ID查询的方法:

  1. // 根据订单ID查询Order,不存在返回null:
  2. public OrderEntity getOrder(Long orderId) {
  3. return this.activeOrders.get(orderId);
  4. }
  5. // 根据用户ID查询用户所有活动Order,不存在返回null:
  6. public ConcurrentMap<Long, OrderEntity> getUserOrders(Long userId) {
  7. return this.userOrders.get(userId);
  8. }

整个订单子系统的实现就是这么简单。

下面是问题解答。

Order的id和sequenceId为何不合并使用一个ID?

订单ID是Order.id,是用户看到的订单标识,而Order.sequenceId是系统内部给订单的定序序列号,用于后续撮合时进入订单簿的排序,两者功能不同。

可以使用一个简单的算法来根据Sequence ID计算Order ID:

  1. OrderID = SequenceID * 10000 + today("YYmm")

因为SequenceID是全局唯一的,我们给SequenceID添加创建日期的”YYmm”部分,可轻松实现按月分库保存和查询。

参考源码

可以从GitHubGitee下载源码。

GitHubmichaelliaowarpexchange/

▸ build)

▸ sql)

▤ schema.sql)

▤ docker-compose.yml)

▤ pom.xml)

▸ common)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ enums)

▤ AssetEnum.java)

▤ Direction.java)

▤ OrderStatus.java)

▸ model)

▸ support)

▤ EntitySupport.java)

▸ trade)

▤ OrderEntity.java)

▸ support)

▤ LoggerSupport.java)

▸ resources)

▤ logback-spring.xml)

▤ pom.xml)

▸ config)

▸ src/main)

▸ java/com/itranswarp/exchange/config)

▤ ConfigApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ config-repo)

▤ application-default.yml)

▤ application-test.yml)

▤ application.yml)

▤ push.yml)

▤ quotation.yml)

▤ trading-api.yml)

▤ trading-engine.yml)

▤ trading-sequencer.yml)

▤ ui-default.yml)

▤ ui.yml)

▸ parent)

▤ pom.xml)

▸ push)

▸ src/main)

▸ java/com/itranswarp/exchange/push)

▤ PushApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ quotation)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ QuotationApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-api)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ TradingApiApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-engine)

▸ src)

▸ main)

▸ java/com/itranswarp/exchange)

▸ assets)

▤ Asset.java)

▤ AssetService.java)

▤ Transfer.java)

▸ order)

▤ OrderService.java)

▤ TradingEngineApplication.java)

▸ resources)

▤ application.yml)

▸ test/java/com/itranswarp/exchange/assets)

▤ AssetServiceTest.java)

▤ pom.xml)

▸ trading-sequencer)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ TradingSequencerApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ ui)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ UIApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▤ .gitignore)

▤ LICENSE)

▤ README.md)

小结

一个订单系统在内存中维护所有用户的活动订单,并提供删除和查询方法。

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

设计订单系统 - 图1