Добро пожаловать на официальный аккаунт "Keegan Xiaogang" для получения дополнительных статей.
Разработка движка соответствия: начало
Разработка соответствующего движка: версия MVP
Разработка механизма сопоставления: проектирование структуры данных
Разработка Matching Engine: стыковка черного ящика
Разработка механизма сопоставления: процесс расшифровки «черного ящика»
Разработка механизма сопоставления: кодовая реализация процесса
промежуточное ПО
Давайте сначала рассмотрим структуру каталогов промежуточного программного обеспечения в нашем проекте соответствующей программы:
├── middleware # 中间件的包
│ ├── cache # 缓存包
│ │ └── cache.go # 缓存操作
│ ├── mq # 消息队列包
│ │ └── mq.go # MQ操作
│ └── redis.go # 主要做Redis初始化操作
Хотя в настоящее время используется только одно промежуточное программное обеспечение Redis, разработка пакета промежуточного программного обеспечения облегчит будущее расширение и добавление другого промежуточного программного обеспечения, такого как Kafka или RocketMQ.
Затем передайте кеш и очередь сообщений, обязанности очень ясны, и приложение также очень ясно.
redis.goПросто сделайте начальное подключение, давайте посмотрим на код:
package middleware
import (
"matching/log"
"github.com/go-redis/redis"
"github.com/spf13/viper"
)
var RedisClient *redis.Client
func Init() {
addr := viper.GetString("redis.addr")
RedisClient = redis.NewClient(&redis.Options{
Addr: addr,
Password: "", // no password set
DB: 0, // use default DB
})
_, err := RedisClient.Ping().Result()
if err != nil {
panic(err)
} else {
log.Printf("Connected to redis: %s", addr)
}
}
Среди них viper — упомянутая выше сторонняя библиотека конфигурации, черезviper.GetString("redis.addr")Прочитайте адрес Redis, который нужно подключить, из файла конфигурации, а затем создайте новый клиент Redis и подключитесь к серверу Redis.
дизайн кэша
Говоря о дизайне структуры данных, мы уже говорили, что есть две основные цели использования кешей:
- дедупликация запроса, чтобы избежать повторной отправки одного и того же заказа;
- Восстановление данных, то есть все данные можно восстановить после перезапуска программы.
Помните, когда в прошлой статье говорилось о реализации Dispatch, там есть логика для определения того, существует ли заказ? Он должен прочитать, существует ли уже заказ в кэше, чтобы определить, является ли это повторным запросом или недействительным запросом. И помните инициализацию пакета процессов? Это процесс восстановления данных из кеша.
Давайте сначала разберемся, какие данные мы кэшируем в целом:
-
Откройте символ совпадающей цели транзакции;
-
последние цены базовых транзакций;
-
Все действительные запросы на заказы, включая запросы на размещение и отмену заказов.
1. Символы кэша
Символов цели транзакции будет несколько для сопоставления, и они не могут повторяться, их можно сохранить как тип набора коллекций. Я разрабатываю ключ набора какmatching:symbols, после этого каждый раз при совпадении символа вы можете использовать RedissaddКоманда добавляет символ в набор. При закрытии сопоставления необходимо использоватьsremКоманда удаляет закрытый совпадающий символ из коллекции. Доступен для чтения всех символовsmembersКомандное действие.
Работа с символами в программе предусматривает три функции, которые используются для сохранения символов, удаления символов и получения всех символов.Следующий код реализован:
func SaveSymbol(symbol string) {
key := "matching:symbols"
RedisClient.SAdd(key, symbol)
}
func RemoveSymbol(symbol string) {
key := "matching:symbols"
RedisClient.SRem(key, symbol)
}
func GetSymbols() []string {
key := "matching:symbols"
return RedisClient.SMembers(key).Val()
}
2. Цены на кэш
Последняя цена цели транзакции заключается в том, что у каждого символа будет цена, и нет необходимости кэшировать историческую цену, тогда я напрямую использую строковый тип для сохранения цены, а ключ каждой цены содержит свой собственный символ и дизайн ключевого формата дляmatching:price:{symbol}, если символ для сохранения = "BTCUSD", соответствующее значение ключа равноmatching:price:BTCUSD, сохраненное значение — это последняя цена BTCUSD.
Мы также предоставляем три функции для сохранения цены, получения цены и удаления цены.Коды следующие:
func SavePrice(symbol string, price decimal.Decimal) {
key := "matching:price:" + symbol
RedisClient.Set(key, price.String(), 0)
}
func GetPrice(symbol string) decimal.Decimal {
key := "matching:price:" + symbol
priceStr := RedisClient.Get(key).Val()
result, err := decimal.NewFromString(priceStr)
if err != nil {
result = decimal.Zero
}
return result
}
func RemovePrice(symbol string) {
key := "matching:price:" + symbol
RedisClient.Del(key)
}
3. Кешировать заказы
Дизайн тайника для заказов не так прост, и нужно выполнить два требования:
- Он может кэшировать как запрос на размещение заказа, так и запрос на отмену заказа;
- Заказы подчиняются требованиям последовательности.
Давайте сначала поговорим о первом пункте, зачем вам кешировать ордера? И почему запросы на размещение и отмену заказа должны кэшироваться?
Давайте сначала ответим на первый вопрос. Мы сопоставляем в памяти. Каждая целевая система транзакций поддерживает реестр доверенных транзакций. Когда программа запускается, эти реестры сохраняются непосредственно в памяти программы. Затем, если программа завершает работу, эти книги очищаются. При отсутствии кеша данные реестра не могут быть восстановлены после перезапуска программы. Чтобы выполнить это требование, все заказы в леджере должны быть кэшированы.
По второму вопросу рассмотрим сценарий: если в канале заказа стоит запрос на отмену заказа, а программа не кэширует запрос на отмену, то программа перезапускается, то все заказы в канале заказов еще не ушли. очищается до того, как он будет получен и обработан механизмом, и запрос на отмену не может быть восстановлен.
Следовательно, программе необходимо кэшировать заказ, а также заказ и отмену.
Давайте посмотрим на второе требование, почему оно должно соответствовать последовательности? Мы знаем, что ордера в канале ордеров упорядочены, а ордера той же цены в стакане транзакций также упорядочены по времени, если ордер не упорядочивается во время кэширования, трудно обеспечить восстановление ордера. в исходном порядке после перезапуска программы.
Итак, как спроектировать кеш этого порядка? Мое решение состоит в том, чтобы разделить кэши на два типа: первый тип сохраняет каждый отдельный запрос ордера, включая размещение и отмену ордера, второй тип объекта подтранзакции сохраняет идентификатор ордера и действие всех запросов ордеров, соответствующих символу.
Первая категория, ключевой формат, который я разработал, этоmatching:order:{symbol}:{orderId}:{action}, symbol, orderId и action — это три значения переменных, соответствующие заказу. Например, символ ордера = "BTCUSD", orderId = "12345", action = "cancel", тогда ключевое значение ордера, сохраненного в Redis, равноmatching:order:BTCUSD:12345:cancel. Значение, соответствующее ключу, предназначено для сохранения всего объекта заказа, который можно использовать сhashтип хранения.
Вторая категория, ключевой формат, который я разработал:matching:orderids:{symbol}, Значение содержитsorted setВведите данные, сохраните все запросы ордеров, соответствующие символу, значение, сохраненное в каждой записи,{orderId}:{action},а такжеscoreЗначение устанавливается для соответствующего ордера{timestamp}. Упорядочивание можно гарантировать, используя время заказа в качестве оценки. Помните, в предыдущей статье мы установили единицу времени заказа на 100 наносекунд, чтобы длина метки времени была ровно 16 бит? Это связано с тем, что если он превышает 16 бит, оценка будет преобразована в экспоненциальное представление, что приведет к цифровым искажениям.
Согласно этому дизайну логика реализации при сохранении заказа показана в следующем коде:
func SaveOrder(order map[string]interface{}) {
symbol := order["symbol"].(string)
orderId := order["orderId"].(string)
timestamp := order["timestamp"].(float64)
action := order["action"].(string)
key := "matching:order:" + symbol + ":" + orderId + ":" + action
RedisClient.HMSet(key, order)
key = "matching:orderids:" + symbol
z := &redis.Z{
Score: timestamp,
Member: orderId + ":" + action,
}
RedisClient.ZAdd(key, z)
}
Кроме того, предусмотрены такие функции, как GetOrder(), UpdateOrder(), RemoveOrder(), OrderExist(), GetOrderIdsWithAction(). Давайте покажем вам реализацию функции GetOrderIdsWithAction():
func GetOrderIdsWithAction(symbol string) []string {
key := "matching:orderids:" + symbol
return RedisClient.ZRange(key, 0, -1).Val()
}
Результаты, полученные этой функцией, сортируются в соответствии со значением оценки, что нам и нужно. Поняв этот дизайн, вернитесь и посмотрите на инициализацию пакета процесса, и вы поймете логику этих кодов.
Дизайн MQ
Мы решили использовать структуру данных Redis Stream в качестве вывода MQ. Структура данных Stream использует дизайн, аналогичный Kafka, который очень удобен в применении. Но поскольку Redis работает в памяти, он намного быстрее, чем Kafka, и это основная причина, по которой я выбираю его в качестве выходного MQ соответствующей программы.
У нас есть только два типа MQ, результаты отмены заказа и записи транзакций.Реализация отправки сообщений выглядит следующим образом:
func SendCancelResult(symbol, orderId string, ok bool) {
values := map[string]interface{}{"orderId": orderId, "ok": ok}
a := &redis.XAddArgs{
Stream: "matching:cancelresults:" + symbol,
MaxLenApprox: 1000,
Values: values,
}
RedisClient.XAdd(a)
}
func SendTrade(symbol string, trade map[string]interface{}) {
a := &redis.XAddArgs{
Stream: "matching:trades:" + symbol,
MaxLenApprox: 1000,
Values: trade,
}
RedisClient.XAdd(a)
}
в,matching:cancelresults:{symbol}Это ключ, которому принадлежит MQ результата отмены.matching:trades:{symbol}Это ключ, которому принадлежит MQ записи транзакции. Можно видеть, что мы также разделяем разные MQ в соответствии с разными символами, что также облегчает нижестоящим службам реализацию распределенных подписок на MQ с разными символами по мере необходимости.
резюме
В этом разделе объясняется конструкция и реализация кеша и MQ.После понимания конструкции этой части вы можете в основном понять основную конструкцию всего механизма сопоставления.
Наконец, есть еще несколько вопросов для размышления: Можно ли не использовать кеш? Как решить проблему дедупликации и восстановления данных без кэширования?
Отсканируйте следующий QR-код, чтобы подписаться на официальную учетную запись (имя общедоступной учетной записи: Киган Сяоган)