Время от времени у меня возникает ощущение, что я хочу чему-то научиться. Я считаю, что настоящий процесс обучения кроется в решении проблем, когда вы пытаетесь достичь какой-то цели. Учебники и курсы по Pluralsight/Udemy никогда не работали для меня. Это не предложение прекратить использование этих платформ, потому что каждому нужно найти свой способ обучения.
На этот раз я решил создать клон Redis на C#. Реальной потребности в этом инструменте в моей повседневной работе не было, и он не предназначен для дальнейшего развития, поэтому на надежность я особо не обращал внимания. Но если вы хотите поиграть с ним и показать мне, как/когда он сломается, это будет здорово.
mes1234/cachy: игровая площадка для Redis, похожая на магазин KV (github.com)
Цель достижения
Я хотел создать хранилище Key-Value с выделенным клиентом для C#. Вы можете добавить элемент для хранения/удаления/получения. Вы можете получить последнюю версию, а также вернуться к более старой версии. Он также должен позволять устанавливать Time To Live, по истечении которого система автоматически удалит его.
Я решил заняться параллельным программированием, хотя у меня редко есть возможность бороться с какими-либо проблемами параллелизма (живя в мире микросервисов).
Общая архитектура
Основой системы является Очередь, которая использует
using System.Collections.Concurrent; ConcurrentQueue<IEntity> _queue;
Это стандартная параллельная коллекция .NET, которая позволяет безопасно использовать одну и ту же очередь несколькими потоками.
Существует четыре размещенных службы:
- Получатель — связь с внешним миром
- Оркестратор — посредник для обработки различных типов действий.
- Снимок — репозиторий элементов без поддержки версий.
- Долгосрочное хранение — добавление только реестра элементов, что позволяет поддерживать версии
Получатель
В службе Receiver размещаются три службы gRPC:
- Инсертитемсервис
- GetItemService
- УдалитьItemService
Каждый из них имеет доступ к Queue. Интересна только служба GetItemService, которая добавляет запрос в Queue и ожидает, пока любая другая часть Cachy предоставит запрошенный элемент.
Первый шаг — запустить таймер, чтобы в случае слишком долгой обработки он мог замкнуть его накоротко:
var start = DateTime.Now; var timeout = TimeSpan.FromSeconds(2000);
Затем он создает Waiter, который на самом деле представляет собой простую Action Task:
item.Waiter = Task.Run(async () =>{ while (item.Result == null && ((DateTime.Now - start) < timeout)) { await Task.Delay(10);}});
Единственное, что мы можем сделать, это подождать, пока Официант закончит:
- что-то установило результаты, чтобы они больше не были null
- Время истекло.
_queue.Enqueue(item); await item.Waiter;
Наконец, обработчик должен проверить, что на самом деле произошло, и предоставить клиенту соответствующий ответ:
if (item.Result == null) return FailedResponse(); else return HandleResponse(item.Result);
Оркестратор
Цель оркестратора — отделить части Cachy друг от друга. Его главная достопримечательность:
ConcurrentBag<IHandler> handlers
и
ConcurrentQueue<IEntity> _queue;
В начале каждого срока службы он регистрируется в обработчиках, поэтому Orchestrator может их использовать. например, служба Snapshot:
public SnapshotStorage(ConcurrentBag<IHandler> handlers,....) { handlers.Add(this); ... }
Теперь Orchestrator имеет доступ ко всем обработчикам из службы Snapshot.
Работа с оркестратором довольно проста:
while (stoppingToken.IsCancellationRequested != true) { IEntity item; if (_queue.TryDequeue(out item)) { await Schedule(item); } else { await Task.Delay(10); } }
Он пытается получить один элемент из Queue, а затем пытается использовать один из обработчиков:
public async Task Schedule<T>(T item) where T : IEntity { foreach (var handler in _handlers) { try { await handler.Handle(item); } catch (Exception ex) { System.Console.WriteLine(ex); } } }
Greedy catch позволяет сервису выжить даже в случае ошибки одного из обработчиков.
Моментальные снимки и долгосрочное хранение
Обе эти службы служат контейнером для данных. Они поддерживают следующие операции:
public void Add(T item); public void Remove(string name); public T Get(string name, int revison); public void CheckTtl();
Единственное отличие состоит в операции Get, которая в случае снимка не поддерживает ревизии, в то время как снимок отслеживает только последнюю ревизию.
CheckTtl используется в основном цикле обслуживания. Его цель — удалить элементы, у которых Time To Live Expired. Эта операция выполняется один раз. Элемент выбирается случайным образом и проверяется, следует ли его удалить или в случае долговременного хранения он должен изменить свое состояние на
Active = false
Заключение и что я узнал
параллелизм
Я попытался сделать каждую службу независимой и общаться с помощью контейнеров Concurrent:
ConcurrentBag<IHandler> _handlers; ConcurrentQueue<IEntity> _queue;
Я считаю, что этот подход в порядке, но имейте в виду, что при параллельной обработке могут возникать условия гонки и все неприятные параллельные проблемы. Это не способ «если», а скорее «когда» это происходит.
Каждый объект, который является общим, должен обрабатываться безопасным способом:
public void Add(T item) { lock (_registry) { if (_registry.ContainsKey(item.Name)) _registry[item.Name].Add(item); else _registry[item.Name] = new Events<T> { item }; } }
Я думаю, что потратил больше времени на проверку того, что не удалось, чем если бы я сначала добавил блокировки по всему коду. Я бы предложил использовать этот подход, чтобы сэкономить время.
Записи C#9
Записи отличные. Они неизменны, просты в использовании и хорошо подготовлены. Синтаксис для создания глубокой копии с измененным значением:
var newObj = obj with { Active = false };
Но есть еще что улучшать. Вы не можете указать, что обобщение является записью. Поэтому, как только вы решили использовать универсальный формат, вы в основном теряете все полезные функции записей.
Есть еще возможность взломать это, например. добавьте в общий интерфейс этот метод:
interface: public IStoredEntity CopyAndDeactivate(); implementation: public IStoredEntity CopyAndDeactivate() => this with { Active = false };
но это все еще хакерский способ, и я бы не советовал его использовать.
IDE
Мой повседневный набор инструментов — Visual Studio + Reshaper. В этом проекте я использовал расширение VS Code + Microsoft C#.
Это хорошо? Да.
Он работает все время и никогда не было проблем. Очевидно, что в VS&Reshaper гораздо больше автоматизации, но в VS Code нет ничего невозможного.
Пожалуйста, проверьте код на:
mes1234/cachy: игровая площадка для Redis, похожая на магазин KV (github.com)
Существует простой демонстрационный проект, демонстрирующий все функции. Пожалуйста, дайте мне знать, что вы думаете, что бы вы сделали по-другому?