接下来要做撮合服务,我们要先让 counter 提供订单查询服务。
不让撮合系统直接从数据库里获取,是出于设计上的考虑。这样撮合服务不需要知道数据如何存储,从本地的模拟测试 Actor ,到单节点连接数据库的简单结构,再到分布式数据和集群节点,无论下单系统如何演进,撮合对接时不需要修改。
这里需要写一些比较复杂的查询,例如给定一个order id,找出它的下一条,也就是比它更大的id中最小的一个。虽然理想状态下这个序列应该是连续的,但是实践中我们不能期待分布式系统永远可靠,每一个节点都应该考虑其它节点不可靠时的应对。同样指定id返回对应记录的查询,也要考虑没有匹配记录时的处理。
这种情况,无论 clojure.java.jdbc 还是 java 的 hibernate 体系,能提供的帮助都比较有限。我们还是要手工写一点儿查询。
我们应该接受一个事实,就是每一个软件开发人员,应该尽量保持自己的知识范围比工作中常用的要“大一圈”。尽管现在数据库访问越来越方便,作应用开发的程序员,特别是后端开发人员,还是应该了解一些 SQL 编写的知识,特别是不能局限于 MySQL 体系。同样道理,仅仅 MSSQL 或 Oracle 也是不行的,哪怕仅仅从实用角度考虑,掌握一种服务器型的关系数据库的 SQL ,和一种内存嵌入式关系型数据库(例如 SQLite )的 SQL ,也能够有效提升工作效率。
再例如,我并不是 JVM 专家,我的工作中也不会直接面对 JVM 的精细量化调节,但是十六年前我第一次接触 Java 项目,就是从逆向乙方的 java 版 sdk ,重新实现 c sharp 版本开始。我们不应该指望学习一次技术,就可以吃一辈子,也不应该寄希望于阻止团队前进来为自己保值。市场在竞争,在优胜劣汰,我亲眼见过安心睡觉五六年后,惊愕的发现自己因为错过了技术升级的机会,只能被淘汰的企业。企业可以破产,个人如何选择未来?机会最终还是会归于有准备的人。希望大家能够理解一件事,团队和个人的学习,都是非常重要的事情,我们都知道坚持锻炼是保持健康的根本措施,那么坚持学习也一样。
这里我们要在 order.clj 中加入查找“比给定 id 大的最小 id”所对应的记录,这个查询是:
with last as (select min(id) from order_flow)
select order_flow.id, account_id, price, content
from order_flow join last where order_flow.id=last.id;
对应的 Clojure 代码,用 jaskell.sql 工具写出来是:
(def find-last-query
(-> (with [:last as
(select (f :min :id) :as :id
from :order_flow
where :id :> (p 0))]
select [:order_flow.id :content :price]
from :order_flow
join :last on :order_flow.id := :last.id)
(.cache)))
现在我们可以写出 place-order 的逆操作 load-order :
(defmulti load-order (fn [data] (get-in data [:content "category"])))
(defmethod load-order "limit-ask" [data]
(doto (LimitAsk.)
(.setId (:id data))
(.setPrice (:price data))
(.setSymbol (get-in data [:content "symbol"]))
(.setQuantity (get-in data [:content "quantity"]))
(.setCompleted (get-in data [:content "completed"]))
(.setAccountId (get-in data [:content "account-id"]))))
(defmethod load-order "limit-bid" [data]
(doto (LimitBid.)
(.setId (:id data))
(.setPrice (:price data))
(.setSymbol (get-in data [:content "symbol"]))
(.setQuantity (get-in data [:content "quantity"]))
(.setCompleted (get-in data [:content "completed"]))
(.setAccountId (get-in data [:content "account-id"]))))
(defmethod load-order "market-ask" [data]
(doto (MarketAsk.)
(.setId (:id data))
(.setSymbol (get-in data [:content "symbol"]))
(.setQuantity (get-in data [:content "quantity"]))
(.setCompleted (get-in data [:content "completed"]))
(.setAccountId (get-in data [:content "account-id"]))))
(defmethod load-order "market-bid" [data]
(doto (MarketBid.)
(.setId (:id data))
(.setSymbol (get-in data [:content "symbol"]))
(.setQuantity (get-in data [:content "quantity"]))
(.setCompleted (get-in data [:content "completed"]))
(.setAccountId (get-in data [:content "account-id"]))))
(defmethod load-order "cancel" [data]
(doto (Cancel.)
(.setId (:id data))
(.setSymbol (get-in data [:content "symbol"]))
(.setAccountId (get-in data [:content "account-id"]))
(.setOrderId (get-in data [:content "order-id"]))))
在保存数据的时候,我们是先 place-order 生成一致的 json 后执行 save 入库。读取数据则相反,根据 find-by 和 find-next 操作查询到的数据,通过 load-order 或 NoteMore/NotFound 的构造返回查询结果,那么 find-by 和 find-next 代码是:
(defn find-by
[order-id]
(if-some [data (j/get-by-id @db :order_flow order-id)]
(load-order data)
(doto (OrderNotFound.)
(.setId order-id))))
(defn find-next
[from-id]
(let [data (j/query @db [(.script find-last-query) from-id])]
(if (not-empty data)
(load-order (first data))
(doto (OrderNoMore.)
(.setPositionId from-id)))))
有了这两个函数,我们就可以……且慢还是写写个测试吧,首先我们准备一些测试数据:
(ns liu.mars.market.test-data)
(def sym "btcusdt")
(def note-paper
{
:limit-ask [{
:id 1 :symbol sym :price 34522M :quantity 1 :account-id 3223421}
{
:id 2 :symbol sym :price 34512M :quantity 10001 :account-id 3223421}
{
:id 3 :symbol sym :price 34525M :quantity 10020 :account-id 34223421}
{
:id 4 :symbol sym :price 34562M :quantity 1000 :account-id 3422341}
{
:id 5 :symbol sym :price 44522M :quantity 10000 :account-id 34223421}]
:limit-bid [{
:id 6 :symbol sym :price 24522M :quantity 1 :account-id 34223421}
{
:id 7 :symbol sym :price 3412M :quantity 10001 :account-id 34223421}
{
:id 8 :symbol sym :price 32525M :quantity 10020 :account-id 34223421}
{
:id 9 :symbol sym :price 31562M :quantity 1000 :account-id 34223421}
{
:id 10 :symbol sym :price 1522M :quantity 9999 :account-id 34223421}]
:market-ask [{
:id 11 :symbol sym :quantity 1 :account-id 34223421}
{
:id 12 :symbol sym :quantity 10001 :account-id 3422342}
{
:id 13 :symbol sym :quantity 10020 :account-id 34223421}
{
:id 14 :symbol sym :quantity 1000 :account-id 3423421}
{
:id 15 :symbol sym :quantity 10000 :account-id 34223421}]
:market-bid [{
:id 16 :symbol sym :quantity 12433 :account-id 34223421}
{
:id 17 :symbol sym :quantity 10001 :account-id 34223421}
{
:id 18 :symbol sym :quantity 10020 :account-id 34223421}
{
:id 19 :symbol sym :quantity 1000 :account-id 34223421}
{
:id 20 :symbol sym :quantity 9999 :account-id 3422421}]
:cancel [{
:id 21 :symbol sym :account-id 4223421 :order-id 23341}
{
:id 22 :symbol sym :account-id 3423421 :order-id 23342}
{
:id 23 :symbol sym :account-id 3422321 :order-id 2341}
{
:id 24 :symbol sym :account-id 3423421 :order-id 23}
{
:id 25 :symbol sym :account-id 3423421 :order-id 9}]})
后面的测试会反复使用这组数据,接下来是测试代码:
(ns liu.mars.market.inner-find-test
(:require [clojure.java.jdbc :as j])
(:require [liu.mars.market.order :refer :all])
(:require [clojure.test :refer :all])
(:require [liu.mars.market.test-data :as data]
[liu.mars.market.config :as cfg])
(:import (liu.mars.market.messages LimitAsk LimitBid MarketAsk MarketBid)))
(testing "inner testing for order module"
(j/delete! @cfg/db :order_flow ["id < ?" 26])
(testing "tests for limit ask orders save and reload"
(doseq [item (:limit-ask data/note-paper)]
(let [data (assoc item :completed 0 :category "limit-ask")]
(save data)
(let [order (find-by (:id data))]
(is (instance? LimitAsk order))
(is (= (:id data) (.getId order)))
(is (= 0 (:completed data) (.getCompleted order)))
(is (= (:quantity data) (.getQuantity order)))
(is (= (:account-id data) (.getAccountId order)))
(is (= (:symbol data)) (.getSymbol order))
(is (= (:price data) (.getPrice order)))))))
(testing "tests for limit bid orders save and reload"
(doseq [item (:limit-bid data/note-paper)]
(let [data (assoc item :completed 0 :category "limit-bid")]
(save data)