Читай реальный код лимитера, конфиг ID, вызов размера bloom-фильтра и запрос geohash, затем выбери ответ, на который подписался бы senior: гонка потерянного обновления, фрагментирующий индекс ключ, насыщенный фильтр и пропущенный граничный сосед.
SDSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги строительных блоков прячутся в коде, не в прозе: read-then-write, что не атомарен, первичный ключ, фрагментирующий индекс, фильтр, размеренный под неверный N, запрос, забывший соседние ячейки. Читай каждый сниппет, найди дефект и выбери починку, на которую подписался бы senior.
Практикуй цикл ревью, что ты гоняешь на реальном коде блоков: найди операцию, проверь, атомарна ли она / упорядочена по времени / верно размерена / учитывает границу, и выбери изменение, которого механизм действительно требует.
Сниппет 1 — инкремент лимитера
def allow(key, limit): n = redis.get(key) or 0 # чтение if int(n) < limit: redis.set(key, int(n) + 1) # modify-write (отдельная операция) return True return False
Викторина
Completed
Этот лимитер работает на многих серверах к одному Redis. Под конкурентностью какой баг вводит read-then-write и какова починка?
Heads-up Redis сериализует отдельные команды, но GET и SET здесь — ОТДЕЛЬНЫЕ команды с зазором между ними. Два запроса могут оба сделать GET одного значения до того, как любой сделает SET. Нужен один атомарный INCR (или Lua), не две операции.
Heads-up Преобразование int() в порядке. Настоящий баг в том, что чтение и запись — две неатомарные операции, поэтому параллельные запросы гоняются в потерянное обновление. Используй атомарный INCR.
Heads-up Он ПЕРЕсчитывает клиента (пускает слишком много): параллельные запросы оба видят устаревший низкий n и оба пускают. Починка для потерянного обновления — атомарная инкремент-и-проверка.
При миллионах вставок/час этот кластеризованный случайно-UUID первичный ключ вредит. В чём проблема и минимальное изменение?
Heads-up Ширина вторична. Удар по пропускной способности — фрагментация от СЛУЧАЙНЫХ ключей на кластеризованном индексе. Упорядоченный по времени UUIDv7 сохраняет выгоды UUID, восстанавливая вставки у правого края.
Heads-up Стоимость генерации пренебрежима рядом с фрагментацией индекса от случайных ключей. Используй UUIDv7 (упорядоченный по времени), чтобы починить паттерн вставки, не ускорить генерацию.
Heads-up На кластеризованном индексе разброс вставок по всему дереву — ПЛОХО: он вызывает расщепления страниц и рушит локальность кэша. Нужны последовательные вставки у правого края, что даёт упорядоченный по времени UUIDv7.
Сниппет 3 — размер bloom-фильтра
# размерен один раз при стартеbf = BloomFilter(expected_items=1_000, fp_rate=0.01)# ...позже, в проде, он принимает каждый ключ событияfor key in stream: # миллионы ключей со временем bf.add(key)
Викторина
Completed
Фильтр размерен под 1 000 элементов, но принимает миллионы. Что происходит и что остаётся гарантированным?
Heads-up Насыщение никогда не вызывает ложноотрицательных — эта гарантия структурна (добавление лишь ставит биты). Оно вызывает рост доли ложноПОЛОЖИТЕЛЬНЫХ к 100%, делая фильтр бесполезным, но никогда не ошибающимся про «точно нет».
Heads-up Простой фильтр Блума не может увеличиваться. За своим размером N он насыщается, и доля ложноположительных лезет. Надо перестроить больший (или использовать scalable bloom filter, сцепляющий бо́льшие фильтры).
Heads-up 1% держится лишь при размеренном N. Вставь в 1000× больше — и массив насыщается, толкая долю ложноположительных к 100%. Размеряй под реальное N.
Сниппет 4 — запрос близости
def nearby(lat, lng): cell = geohash_encode(lat, lng, precision=6) # ячейка ~1 км return db.query( "SELECT * FROM drivers WHERE geohash LIKE %s", cell + '%' ) # лишь собственная ячейка пассажира
Викторина
Completed
Этот возвращает ноль водителей, даже когда один припаркован через улицу. Что не так и какова починка?
Heads-up Точность не чинит границу: водитель через ЛЮБОЙ край ячейки всё равно имеет другой префикс. Починка — расширить запрос до 8 соседних ячеек (сетка 3×3), не менять точность.
Heads-up Префиксный LIKE ('cell%') отлично использует строковый/B-tree индекс — в этом весь смысл geohash. Баг — запрос лишь одной ячейки; включи 8 соседей.
Heads-up Ранжирование по точному расстоянию тоже нужно, но баг пропущенного соседа — это проблема границы: одноклеточный запрос не видит водителя за краем. Сперва добавь 8 соседних ячеек, затем ранжируй по расстоянию.
Вспомните перед уходом
01
Как заметить и починить гонку распределённого лимитера в коде?
02
Почему случайно-UUID кластеризованный первичный ключ вредит пропускной способности вставок и минимальная починка?
03
Что происходит, когда фильтр Блума размерен под слишком мало элементов, и что сохраняется?
Итог
Каждое решение по блоку в этом юните читается прямо с кода. Лимитер, делающий отдельные чтение и запись на флоте, имеет гонку потерянного обновления — схлопни в атомарный INCR или Lua-скрипт. Случайно-UUID кластеризованный первичный ключ фрагментирует B-дерево на высоких темпах вставки — перейди на упорядоченный по времени UUIDv7 ради вставок у правого края. Фильтр Блума, размеренный под слишком мало элементов, насыщается (доля ложноположительных → 100%, но никогда ложноотрицательных) — перестрой под истинное N. А запрос близости geohash, сканирующий лишь ячейку пассажира, попадает в проблему границы — включи 8 соседних ячеек, затем ранжируй по точному расстоянию. Senior-привычка одна и та же каждый раз: найди операцию, назови инвариант, что она должна держать (атомарность / упорядоченность по времени / верный размер / учёт границы), и выбери починку, его восстанавливающую.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.