sync.Map, Mutex или RWMutex: 5100 замеров на Go 1.27

• 16 мин чтения

Я сравнил map + Mutex, map + RWMutex и sync.Map на Go 1.27.3. Сделал 5100 замеров на таблицах от одного до 10 млн ключей. Ниже 32 графика скорости и памяти, код тестов и выводы по разным нагрузкам.

Код и результаты можно скачать одним архивом. Скорость самого сервиса нужно измерять отдельно.

Что получилось

При чтении 4096 ключей с 12 рабочими горутинами быстрее был SyncMap. Он показал 5,521 нс/оп. У RWMutex получилось 37,42, у Mutex 168,8 нс/оп.

При смеси 50/50 на одном ключе с 12 горутинами быстрее был RWMutex: 84,36 нс/оп. У Mutex получилось 171,2, у SyncMap 265,9 нс/оп.

Поэтому я бы проверял все три варианта. Для выбора нужны размер map, число горутин и доля записей. Ещё важно, как часто горутины обращаются к одним ключам.

Что сравниваем

Первый вариант: обычная map с RWMutex. Читатели берут RLock, писатели Lock. Под блокировкой выполняется только поиск или запись в map:

type LockedMap struct {
    mu sync.RWMutex
    data map[uint64]*Value
}

type Value struct {
    Number uint64
}

func (m *LockedMap) Load(key uint64) (*Value, bool) {
    m.mu.RLock()
    v, ok := m.data[key]
    m.mu.RUnlock()
    return v, ok
}

func (m *LockedMap) Store(key uint64, v *Value) {
    m.mu.Lock()
    m.data[key] = v
    m.mu.Unlock()
}

Второй вариант использует Mutex. Здесь и чтение, и запись берут Lock:

type MutexMap struct {
    mu sync.Mutex
    data map[uint64]*Value
}

func (m *MutexMap) Load(key uint64) (*Value, bool) {
    m.mu.Lock()
    v, ok := m.data[key]
    m.mu.Unlock()
    return v, ok
}

func (m *MutexMap) Store(key uint64, v *Value) {
    m.mu.Lock()
    m.data[key] = v
    m.mu.Unlock()
}

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

В обоих примерах поле data нужно создать через make. Полный код есть в архиве. Основы разобрал в статье о map в Go.

Третий вариант использует sync.Map. Он принимает ключи через any. При чтении код приводит результат к *value. Эти шаги входят в замер. Обычные map хранят ключи и значения с заданными типами.

Значения в тесте хранятся в двух объектах. Каждый Store кладёт в map указатель на один из них. Поля объектов не меняются. Новый объект приложения на каждой записи создавать не нужно. Внутренние аллокации map входят в замер.

Как читать графики

В тестах число рабочих горутин равно GOMAXPROCS. Дальше я обозначаю его буквой P. Например, P=12 означает 12 рабочих горутин и GOMAXPROCS=12.

Порядок в легендах и группах столбцов один: RWMutex, Mutex, SyncMap. Оба варианта с обычной map серые. RWMutex показан сплошной линией, Mutex короткими штрихами. SyncMap показан синим, с длинными штрихами. На столбчатых графиках у Mutex есть штриховка.

Каждая точка показывает медиану пяти повторов. Если упорядочить пять чисел, медиана будет третьим. Линия соединяет точки, которые я измерил.

На линейных графиках меньше нс/оп значит больше операций за секунду. Число взято из RunParallel: всё время замера делится на число итераций всех горутин. Например, 20 нс/оп дают около 50 млн итераций в секунду суммарно. Время ответа одного запроса по этому числу узнать нельзя. Подробности есть в документации RunParallel.

Пять коротких повторов дают лишь первый ориентир. Если линии близки, я бы увеличил время теста. Разницу в несколько процентов здесь легко спутать с шумом. Минимум и максимум каждого набора повторов лежат в summary.csv.

Чтение существующих ключей

В map заранее лежат 4096 ключей. Горутины только читают их. Все поиски успешны.

У RWMutex читатели обновляют общий счётчик блокировки. Поэтому даже чтение без писателей имеет общие расходы.

Чтение без записей, 4096 ключей; медиана пяти повторов, меньше лучше

При P=1 у Mutex получилось 17,69 нс/оп, у RWMutex 20,2, у SyncMap 45,52. При P=12 порядок сменился: SyncMap 5,521, RWMutex 37,42, Mutex 168,8 нс/оп.

Один процент записей

На каждом шаге вероятность Store равна 1%. Запись меняет значение у готового ключа. Таблица не растёт. Ключи и значения уже есть в памяти.

99% Load и 1% Store, 4096 ключей; медиана пяти повторов, меньше лучше

Десять процентов записей

Теперь вероятность записи равна 10%. Остальные обращения читают map. Общая блокировка нужна даже тогда, когда горутины выбрали разные ключи.

90% Load и 10% Store, 4096 ключей; медиана пяти повторов, меньше лучше

При P=1 у Mutex и RWMutex получились близкие числа: 22,29 и 22,68 нс/оп. У SyncMap 53,09. При P=12 у Mutex уже 209,4, у RWMutex 113,5, у SyncMap 11,56 нс/оп.

Половина операций записывает

Чтение и запись выбираются с равной вероятностью. Под блокировкой по-прежнему выполняется только обращение к map. Походов в сеть или базу здесь нет.

50% Load и 50% Store, 4096 ключей; медиана пяти повторов, меньше лучше

Только перезапись

Каждый шаг вызывает Store. Ключ уже есть в map, меняется только указатель на значение. RWMutex работает через Lock, как и обычный Mutex.

100% Store существующих ключей, 4096 ключей; медиана пяти повторов, меньше лучше

При P=12 Mutex показал 321 нс/оп, RWMutex 347,5, SyncMap 47,76. Ниже я проверяю перезапись и на других размерах map.

Чтение отсутствующих ключей

В map лежат 4096 ключей. Горутины ищут ключи из другого диапазона и всегда получают промах. После промаха тест ничего не загружает и не добавляет в map.

100% Load отсутствующих ключей; медиана пяти повторов, меньше лучше

Удаление и повторная вставка

Горутина удаляет случайный ключ и вставляет его обратно. В одном шаге два вызова: Delete и Store. Между ними может сработать другая горутина. К моменту удаления ключ уже может отсутствовать.

У Mutex и RWMutex это два отдельных захвата блокировки. Поэтому сравнивать число с тестом одного Store напрямую нельзя. Число ключей здесь колеблется, но не растёт без конца.

Одна итерация Delete + Store, 4096 возможных ключей; медиана пяти повторов, меньше лучше

Каждой горутине свой набор ключей

У каждой горутины свой набор ключей. Map и блокировка остаются общими. И Mutex, и RWMutex по-прежнему защищают всю таблицу.

На горутину приходится 4096 ключей. При P=1 их всего 4096, при P=12 уже 49152. Значит, вместе с числом горутин растёт и таблица. Этот график показывает оба эффекта сразу.

50% Store, непересекающиеся наборы по 4096 ключей; медиана пяти повторов, меньше лучше

LoadOrStore: ключ уже есть

Все ключи уже лежат в map. Метод LoadOrStore всегда возвращает готовое значение.

У обычных map поиск идёт под Lock. У RWMutex можно сделать иначе: сначала проверить ключ под RLock. Если его нет, взять Lock и проверить ещё раз. Такой вариант здесь не измерялся.

LoadOrStore: ключ всегда существует; медиана пяти повторов, меньше лучше

Как влияет доля записей

Оставляем 4096 ключей и P=12. Меняем только шанс записи: от 1% до 100%. По графику можно выбрать долю записей и сравнить все три варианта в одной точке.

Доля Store при P=12 и 4096 ключах; медиана пяти повторов, меньше лучше

Размер map: только чтение

Теперь меняем число ключей: от одного до 10 млн. На каждой линии 22 точки, P=12. Горутины читают заранее заполненную map.

Ось X логарифмическая. Это помогает уместить на одном графике и единицы, и миллионы ключей. Время заполнения таблицы в замер не входит.

Размер map, 100% Load, P=12; медиана пяти повторов, меньше лучше

На миллионе ключей RWMutex показал 44,87 нс/оп, SyncMap 53,68, Mutex 386,5. На 10 млн: RWMutex 46,04, SyncMap 71,67, Mutex 506,4 нс/оп. В этом тесте большой размер изменил выбор по сравнению с чтением 4096 ключей.

Размер map: десять процентов записей

Здесь тоже P=12 и до 10 млн ключей. Шанс записи равен 10%. Ключи числовые, значения в map хранятся как указатели. Стоимость копирования больших объектов нужно проверять отдельно.

Размер map, 10% Store, P=12; медиана пяти повторов, меньше лучше

Размер map: половина записей

При P=12 чтение и запись имеют равный шанс. Вместе со скоростью стоит смотреть на аллокации: они добавляют работу сборщику мусора.

Размер map, 50% Store, P=12; медиана пяти повторов, меньше лучше

При 16 ключах RWMutex показал 99,62 нс/оп, SyncMap 181,8, Mutex 202. При миллионе быстрее стал SyncMap: 65,68 нс/оп. У RWMutex получилось 294,4, у Mutex 457,4. Число горутин и доля записей не менялись.

От миллиона до десяти миллионов ключей

Ниже те же большие точки крупнее. Ось X теперь линейная. На всех трёх графиках числовые ключи и P=12.

1–10 млн ключей, 0% записей, P=12; нс/оп, меньше лучше
1–10 млн ключей, 10% записей, P=12; нс/оп, меньше лучше
1–10 млн ключей, 50% записей, P=12; нс/оп, меньше лучше

Размер map: только запись

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

Размер map, 100% Store, P=12; медиана пяти повторов, меньше лучше

Размер map: промахи

Размер заполненной map растёт от одного ключа до миллиона. Горутины ищут ключи из другого диапазона. Каждый поиск даёт промах.

Размер map, Load отсутствующего ключа, P=12; медиана пяти повторов, меньше лучше

Размер map: удаление и вставка

На каждом размере один шаг вызывает Delete, затем Store. Размеры те же: от одного ключа до миллиона. Пару вызовов нельзя считать одной операцией с map.

Размер map, Delete + Store, P=12; медиана пяти повторов, меньше лучше

Размер map при одной горутине

При P=1 рабочие горутины не спорят за блокировку. Здесь лучше видны расходы на поиск и сами методы. Верхняя точка этих графиков составляет миллион ключей.

Только чтение

Числовые ключи, размер map, только чтение, P=1; медиана пяти повторов, меньше лучше

Десять процентов записей

Числовые ключи, размер map, десять процентов записей, P=1; медиана пяти повторов, меньше лучше

Половина записей

Числовые ключи, размер map, половина записей, P=1; медиана пяти повторов, меньше лучше

Размер map со строковыми ключами

Теперь ключи строковые. В каждой строке 20 байт. Они созданы до теста. На линиях по 19 размеров от одного ключа до миллиона, P=12.

Только чтение

Строки по 20 байт, размер map, только чтение, P=12; медиана пяти повторов, меньше лучше

Десять процентов записей

Строки по 20 байт, размер map, десять процентов записей, P=12; медиана пяти повторов, меньше лучше

Половина записей

Строки по 20 байт, размер map, половина записей, P=12; медиана пяти повторов, меньше лучше

Обращения к горячим ключам

В map лежат 4096 ключей. Горутины обращаются только к первым K ключам. Меняем K от 1 до 4096, шанс записи всегда равен 50%.

При K=1 все делят один ключ. При K=4096 обращения идут по всей map. Внутри выбранного набора ключи равновероятны. Остальные вообще не используются. Это отдельная модель нагрузки, без распределения Zipf или правила 80/20.

Горячий набор в map из 4096 элементов, 50% Store, P=12; медиана пяти повторов, меньше лучше

На одном горячем ключе RWMutex показал 95,34 нс/оп, Mutex 203,6, SyncMap 254,8. На восьми у SyncMap уже 72,37 нс/оп, у RWMutex 100,8, у Mutex 211,9. При K=4096 числа составили 30,96, 160,1 и 310,3 нс/оп в том же порядке. Разброс повторов есть в CSV.

Строковые ключи: чтение

Здесь размер фиксирован: 4096 строк. Меняется число горутин. Тест считает время поиска и сравнения строк. Создание строк через fmt.Sprintf вынесено за таймер.

Строки по 20 байт, 100% Load, 4096 ключей; медиана пяти повторов, меньше лучше

Строковые ключи: десять процентов записей

Обычные map имеют тип map[string]*value. В sync.Map строка передаётся через any. Затраты на это тоже входят в замер. Их след можно увидеть в B/op.

Строки по 20 байт, 10% Store, 4096 ключей; медиана пяти повторов, меньше лучше

Строковые ключи: половина записей

Последний тест скорости даёт строкам 50% записей. У всех строк одна длина и общий префикс. Для длинных URL или строк разной длины нужны свои замеры.

Строки по 20 байт, 50% Store, 4096 ключей; медиана пяти повторов, меньше лучше

Сколько ключей помещается в RAM

На машине около 30,6 ГиБ RAM. Перед проверкой было доступно около 22,5 ГиБ. Лимита памяти у cgroup процесса не было. Swap в расчёт не включал: работа с ним меняет скорость теста.

Память измерил отдельно на 1 и 5 млн ключей. Каждый случай запускал в новом процессе. После GC снял живой heap: память, которая осталась занятой. Пик RSS взял из VmHWM в /proc/self/status. Это максимум RAM, которую занимал процесс. Поле описано в документации Linux.

В heap входят map, массив ключей и один общий объект значения. Каждая запись ссылается на этот объект. В вашем приложении значения могут занимать гораздо больше места.

Живой heap после GC

  • RWMutex
  • Mutex
  • SyncMap
1 млн, uint64
RWMutex: 43,7 МиБ
Mutex: 43,7 МиБ
SyncMap: 116,0 МиБ
1 млн, string
RWMutex: 91,5 МиБ
Mutex: 91,5 МиБ
SyncMap: 154,1 МиБ
5 млн, uint64
RWMutex: 182,5 МиБ
Mutex: 182,5 МиБ
SyncMap: 568,9 МиБ
5 млн, string
RWMutex: 404,1 МиБ
Mutex: 404,1 МиБ
SyncMap: 759,8 МиБ
Живой heap заполненной таблицы и массива ключей; МиБ, меньше лучше

Пик RSS процесса

  • RWMutex
  • Mutex
  • SyncMap
1 млн, uint64
RWMutex: 48,2 МиБ
Mutex: 48,3 МиБ
SyncMap: 128,0 МиБ
1 млн, string
RWMutex: 101,8 МиБ
Mutex: 101,7 МиБ
SyncMap: 168,4 МиБ
5 млн, uint64
RWMutex: 187,5 МиБ
Mutex: 187,4 МиБ
SyncMap: 611,4 МиБ
5 млн, string
RWMutex: 437,9 МиБ
Mutex: 437,7 МиБ
SyncMap: 813,0 МиБ
Пик RSS при подготовке таблицы; МиБ, меньше лучше

На 5 млн строковых ключей SyncMap занял около 159 байт heap на ключ. В эту цену входит и массив ключей. Если умножить эту цену на 50 млн, получится около 7,4 ГиБ.

Это оценка по измеренной точке. Сам предел в 50 млн я не проверял. Для обновлений и GC нужен запас RAM. У обычной map расход на ключ тоже может меняться с размером таблицы.

На этой машине можно отдельно проверить память для 30–50 млн маленьких записей. В тестах скорости я дошёл до 10 млн. Если каждое значение хранит свою строку или буфер, ключей поместится меньше.

Проверить память можно так:

$ go build -o /tmp/sync-map-memory ./cmd/memory
$ /tmp/sync-map-memory -n 5000000 -kind string -impl SyncMap

Числа всех 12 случаев есть в results/memory.json. Объём живой таблицы и аллокации на одну операцию измеряют разные вещи.

Сколько памяти выделяют операции

B/op показывает, сколько байт в среднем выделено за итерацию. allocs/op показывает число аллокаций. Заполнение map до начала теста в эти числа не входит.

  • RWMutex
  • Mutex
  • SyncMap
Чтение, 4096 ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 0 Б/оп
10% Store, 4096 ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 5 Б/оп
100% Store, 4096 ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 55 Б/оп
Delete + Store, 4096 ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 55 Б/оп
Строки, 50% Store, 4096 ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 32 Б/оп
50% Store, 1 млн ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 27 Б/оп
50% Store, 10 млн ключей
RWMutex: 0 Б/оп
Mutex: 0 Б/оп
SyncMap: 27 Б/оп
Аллокации во время операций, P=12; медиана пяти повторов, Б/оп, меньше лучше

У Mutex и RWMutex на этих нагрузках получилось 0 B/op. У SyncMap записи выделяют память. Например, при 100% Store тест показал 55 B/op. Это ещё одна цена, которую стоит проверить в сервисе.

В выводе go test число allocs/op целое. Если аллокация есть лишь на части шагов, вывод может показать ноль. Ненулевой B/op при таком нуле всё ещё означает, что память выделялась.

Что защищает блокировка

Все три варианта защищают обращения к map. После Load вы получаете указатель на объект. Чтобы менять его поля из разных горутин, нужна своя защита. В тесте поля объектов не меняются. Про защиту отдельных значений писал в статье о sync/atomic.

Два безопасных вызова тоже могут дать ошибку. Здесь обе горутины могут прочитать одно число. Каждая прибавит единицу и запишет тот же результат:

// Фрагмент с ошибкой: несколько горутин могут прочитать одно значение.
v, _ := m.Load("counter")
m.Store("counter", v.(int)+1)

С обычной map можно держать Lock от чтения до конца записи. Для sync.Map подойдёт CAS-цикл или отдельный атомарный счётчик. Выбор зависит от задачи. Эти способы в тест не входят.

LoadOrStore тоже не обещает один вызов функции загрузки. В m.LoadOrStore(key, expensive()) сначала вызывается expensive(). Только потом сам метод. Несколько горутин могут выполнить одну и ту же дорогую работу.

Метод Range не даёт снимок map на один момент времени. Во время обхода другие горутины могут менять значения. Это описано в документации Range. Если нужно согласованно менять несколько ключей, обычная map под общим Lock часто проще.

Как устроены тесты

Все замеры сделаны 9 ноября 2026 года. Машина: AMD Ryzen 5 PRO 4650U, 6 ядер, 12 аппаратных потоков. Система Linux, amd64. Версия Go 1.27.3, GOEXPERIMENT пустой. Архив Go скачан с go.dev, его контрольная сумма совпала с указанной на сайте.

Нагрузку запускает testing.B.RunParallel. Рабочие значения P: 1, 2, 3, 4, 6, 8 и 12. Вместе с P меняется число горутин. Случай со ста горутинами на одном P здесь не проверялся.

ТестЧто меняетсяОстальные условия
Число горутин7 значений P, 9 нагрузокОбычно 4096 ключей
Размер, числовые ключи19 размеров, 6 нагрузокP=12; чтение, 10/50/100% записей, промахи, Delete+Store
Большие таблицыЕщё 3, 5 и 10 млн ключейP=12; 0/10/50% записей
Размер при одной горутине19 размеровP=1; 0/10/50% записей
Размер, строки19 размеровP=12; 0/10/50% записей
Доля записей11 значений от 1% до 100%P=12, 4096 ключей
Горячие ключи8 наборов от 1 до 4096 ключейP=12, 50% записей
Строки и число горутин7 значений P4096 ключей; 0/10/50% записей

Основные размеры: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 4096, 16384, 65536, 100000, 250000, 500000, 750000 и 1000000 ключей. В трёх числовых нагрузках добавлены 3000000, 5000000 и 10000000. В них по 22 точки на линию. В остальных тестах размера по 19.

Всего 340 наборов условий. Умножаем на три реализации и пять повторов. Получаем 1020 конфигураций и 5100 измерений. Все три варианта прошли один полный прогон.

Ключи, строки и значения создаются до таймера. Заполнение map тоже вынесено за него. Для обычного теста готовится N ключей. Для промахов 2N, для отдельных наборов N ключей на горутину. Лишних ключей в массиве нет.

Все варианты вызываются через один интерфейс: Load, Store, Delete, LoadOrStore. Расходы на этот вызов входят в результат. Как и выбор операции, хеширование и работа блокировки.

На всех тестах порядок вариантов фиксирован. Частоты CPU и работа планировщика тоже могут влиять на числа. Пять повторов по 150 мс не доказывают устойчивый выигрыш в пару процентов.

Как тест выбирает ключ

У каждой горутины свой генератор xorshift. Для всех обычных тестов индекс считается так:

func keyIndex(r uint64, n int) int {
    return int((uint64(uint32(r)) * uint64(n)) >> 32)
}

Формула берёт младшие 32 бита и выбирает индекс от 0 до N−1. Она работает и для миллиона ключей. При наших размерах каждый индекс достижим. Число исходных значений на один индекс может отличаться на единицу. Поэтому выбор почти равномерный.

Маска r & (N-1) годится только для размеров, равных степени двойки. Здесь она осталась лишь у горячего набора. Старшие биты генератора выбирают чтение или запись.

Шанс записи проверяется на каждом шаге. При 10% записей короткий отрезок может дать и другую долю. Жёсткого порядка «девять чтений, одна запись» в коде нет.

Что проверяет сводка

Скрипт ждёт все 1020 конфигураций, по пять повторов каждой. Замеры с числом итераций меньше 10000 он отклоняет. В CSV остаются медиана, минимум, максимум и число итераций.

После теста таймер выключается до возврата прежнего GOMAXPROCS. Смена P может ждать GC. Это ожидание в цену map не входит.

Суффикс -12 в сыром выводе относится к внешней настройке testing. Число P для нагрузки задано внутри теста. Например, parallel/read/4/RWMutex работает с P=4.

Версия sync.Map

В Go 1.24 sync.Map получил другую реализацию. В Go 1.27.3 внутри него используется HashTrieMap. Старые разборы через read и dirty к этим тестам не подходят. Изменение описано в примечаниях Go 1.24.

Документация sync.Map выделяет два случая: ключ записан один раз и часто читается; либо горутины работают с разными наборами ключей. В большинстве задач она советует обычную map с блокировкой. Так проще задать типы и вместе с map защитить другие данные.

Как повторить замеры

В архиве есть map_test.go, go.mod, скрипт сводки и каталог results. Для Go-кода хватает стандартной библиотеки.

$ go version
go version go1.27.3 linux/amd64
$ go test -race ./...
$ go test -run '^$' -bench . -benchmem -benchtime=150ms -count=5 -timeout=2h > results/raw.txt
$ python3 summarize.py

Мой полный прогон занял около 52 минут. Большие map перед каждым замером заполняются заново. Это входит во время команды, но исключено из ns/op. Поэтому таймаут увеличен до двух часов.

Детектор гонок запускается отдельно через -race. Он проверяет работу обёрток. Скорость измеряется без него.

Чтобы проверить близкие числа, увеличьте время теста и число повторов. Например, для чтения при восьми горутинах:

$ go test -run '^$' -bench '^BenchmarkMaps$/^parallel$/^read$/^8$' -benchmem -benchtime=2s -count=10

Для миллиона ключей с 50% записей:

$ go test -run '^$' -bench '^BenchmarkMaps$/^size$/^w50$/^1000000$' -benchmem -benchtime=2s -count=10 -timeout=2h

Вывод отдельной серии сохраните в другой файл. Скрипт сводки ждёт в raw.txt полный набор. Подробнее о выборе теста писал в статье о бенчмарках Go.

Здесь не проверены постоянный рост map, Range, большие значения и работа внутри блокировки. Нет шардированной map, задержек p95/p99 и теста при нехватке RAM.

Итог

В моих замерах выбор меняется с нагрузкой. На чтении 4096 ключей при P=12 быстрее SyncMap. На чтении 10 млн ключей быстрее RWMutex. На одном ключе со смесью 50/50 тоже RWMutex.

Я бы начал с обычной map и блокировки под свою задачу. Затем сравнил Mutex, RWMutex и sync.Map на своих ключах. Размер таблицы, горячие ключи и время под блокировкой важны не меньше доли записей.

Последний шаг: измерить весь сервис. Если map занимает малую часть времени запроса, выигрыш на ней мало изменит итоговую скорость.


Об авторе: Александр Бруяко — руководитель разработки с опытом в backend, инфраструктуре и управлении командами.


Теги: