Как реализовать простое конкурентное key-value хранилище в Go, аналог Redis?

Ответ

Для создания простого in-memory key-value хранилища с поддержкой конкурентного доступа в Go чаще всего используют map в сочетании с примитивами синхронизации.

Вариант 1: sync.RWMutex (классический подход)

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

import "sync"

type ConcurrentMap struct {
    data  map[string]string
    mutex sync.RWMutex
}

func NewConcurrentMap() *ConcurrentMap {
    return &ConcurrentMap{
        data: make(map[string]string),
    }
}

func (cm *ConcurrentMap) Set(key, value string) {
    cm.mutex.Lock() // Блокировка на запись
    defer cm.mutex.Unlock()
    cm.data[key] = value
}

func (cm *ConcurrentMap) Get(key string) (string, bool) {
    cm.mutex.RLock() // Блокировка на чтение
    defer cm.mutex.RUnlock()
    val, ok := cm.data[key]
    return val, ok
}

Вариант 2: sync.Map (оптимизированный для определенных случаев)

sync.Map — это специализированная структура, оптимизированная для двух сценариев:

  1. Когда ключ записывается один раз, а затем читается много раз.
  2. Когда разные горутины работают с разными, непересекающимися наборами ключей.

В этих случаях sync.Map может быть производительнее, так как избегает глобальных блокировок.

Что еще можно добавить для полноценного аналога:

  • Сетевой уровень: Реализовать TCP-сервер с помощью пакета net, который будет принимать и обрабатывать команды (SET, GET, DEL).
  • Поддержка TTL (Time-To-Live): Для каждого ключа хранить время его "смерти" и периодически запускать горутину-чистильщика или проверять TTL при доступе.
  • Персистентность: Реализовать сохранение данных на диск при выключении и загрузку при старте.

Выбор между sync.RWMutex и sync.Map зависит от предполагаемого паттерна нагрузки на ваше хранилище.