Разработка соответствующего движка: кэш и MQ

Архитектура

Добро пожаловать на официальный аккаунт "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.

дизайн кэша

Говоря о дизайне структуры данных, мы уже говорили, что есть две основные цели использования кешей:

  1. дедупликация запроса, чтобы избежать повторной отправки одного и того же заказа;
  2. Восстановление данных, то есть все данные можно восстановить после перезапуска программы.

Помните, когда в прошлой статье говорилось о реализации 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. Кешировать заказы

Дизайн тайника для заказов не так прост, и нужно выполнить два требования:

  1. Он может кэшировать как запрос на размещение заказа, так и запрос на отмену заказа;
  2. Заказы подчиняются требованиям последовательности.

Давайте сначала поговорим о первом пункте, зачем вам кешировать ордера? И почему запросы на размещение и отмену заказа должны кэшироваться?

Давайте сначала ответим на первый вопрос. Мы сопоставляем в памяти. Каждая целевая система транзакций поддерживает реестр доверенных транзакций. Когда программа запускается, эти реестры сохраняются непосредственно в памяти программы. Затем, если программа завершает работу, эти книги очищаются. При отсутствии кеша данные реестра не могут быть восстановлены после перезапуска программы. Чтобы выполнить это требование, все заказы в леджере должны быть кэшированы.

По второму вопросу рассмотрим сценарий: если в канале заказа стоит запрос на отмену заказа, а программа не кэширует запрос на отмену, то программа перезапускается, то все заказы в канале заказов еще не ушли. очищается до того, как он будет получен и обработан механизмом, и запрос на отмену не может быть восстановлен.

Следовательно, программе необходимо кэшировать заказ, а также заказ и отмену.

Давайте посмотрим на второе требование, почему оно должно соответствовать последовательности? Мы знаем, что ордера в канале ордеров упорядочены, а ордера той же цены в стакане транзакций также упорядочены по времени, если ордер не упорядочивается во время кэширования, трудно обеспечить восстановление ордера. в исходном порядке после перезапуска программы.

Итак, как спроектировать кеш этого порядка? Мое решение состоит в том, чтобы разделить кэши на два типа: первый тип сохраняет каждый отдельный запрос ордера, включая размещение и отмену ордера, второй тип объекта подтранзакции сохраняет идентификатор ордера и действие всех запросов ордеров, соответствующих символу.

Первая категория, ключевой формат, который я разработал, это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-код, чтобы подписаться на официальную учетную запись (имя общедоступной учетной записи: Киган Сяоган)