Сохраните лучшие результаты и включите всех с равным граничным результатом

RedisBeginner
Практиковаться сейчас

Введение

На пункт подсчёта результатов Field Day попытки поступают не по порядку. Три попытки Alex дают 80, 90 и 85 очков. Лучший результат — 90: это абсолютные результаты, а не очки, которые нужно складывать. Другого участника тоже зовут Alex, поэтому имена не могут идентифицировать соревнующихся. Если результат третьего места делят несколько участников, проходят все с этим результатом.

Вы сохраните функции Redis для записи и определения прошедших участников, пригодные для повторного использования, обработаете настоящий лист попыток и опубликуете фактический список прошедших. Затем вы выполните небольшое упражнение с забегом на время, где меньший результат лучше. Оба упражнения используют подготовленные синтетические идентификаторы и целочисленные результаты. Предварительно нужны базовые навыки навигации в терминале и подключения к Redis, знание GET/SET; сортированные множества и предоставленные шаблоны функций Python объясняются перед использованием. Redis и Terminal готовы к работе.

Работайте в /home/labex/project/score-desk. Создайте main.py и practice.py; обновляйте только score:main:best, score:practice:best и временные ключи, начинающиеся с score:demo:. Сохраните исходные CSV-листы, хеши списков участников, предыдущий раунд и все предоставленные инструменты. Ранее записанные 88 очков одного участника в основном раунде и 72 секунды другого в тренировочном должны сохраниться, хотя их попытки отсутствуют в новых листах.

Одна попытка должна безопасно обновлять лучший результат своего участника, даже если одновременно поступает другая попытка. Определение прошедших выполняется после закрытия приёма попыток; два чтения не дают снимок изменяющегося соревнования. Результаты — целые числа от 1 до 1000000. Это упражнение не реализует апелляции по результатам, удаление ошибочного лучшего результата, обнаружение мошенничества, измерение дробных долей секунды или сохранение данных на диске и переключение при отказе.

Сохраните лучший абсолютный результат попытки

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

Используйте Terminal, начиная с обычной командной оболочки. Предоставленная программа desk читает настоящий лист и базу данных. Она вызывает ваши сохранённые функции без изменений и не вычисляет и не исправляет за вас правильный лучший результат. ./ означает запуск файла в текущем каталоге. Перейдите в каталог и изучите лист и список участников:

cd /home/labex/project/score-desk
./desk attempts main
./redis HGETALL score:main:names
./desk data main

player_id — стабильный идентификатор; score — абсолютное число очков одной попытки. ID p01 и p02 принадлежат разным людям по имени Alex. Подготовленное основное сортированное множество сейчас содержит только p90 с результатом 88. Он должен остаться в нём. Redis хранит имена отдельно в хеше — отображении ID участника в имя. HGETALL читает это отображение; редактировать его не требуется.

Сортированное множество хранит каждый элемент один раз с числовым результатом. Здесь элемент — ID участника. Redis упорядочивает элементы по результату. Обычная команда ZADD key score member добавляет новый элемент или заменяет его существующий результат. Используйте временный демонстрационный ключ, чтобы увидеть, почему последний поступивший результат не обязательно лучший:

./redis ZADD score:demo:last 80 p01
./redis ZADD score:demo:last 90 p01
./redis ZADD score:demo:last 85 p01
./redis --raw ZSCORE score:demo:last p01

Итоговый результат — 85, потому что последняя команда перезаписала 90. ZSCORE читает результат одного элемента; --raw запрашивает у CLI значение без оформления. ZINCRBY вместо этого складывал бы попытки, получая 255 из этих трёх значений. Такая операция подходит для приращений, но эти попытки не являются приращениями.

ZADD с GT добавляет новый элемент или меняет существующий результат только тогда, когда новое значение больше. Сравнение и обновление выполняются внутри одной команды Redis. Отдельное чтение на стороне клиента с последующей безусловной записью может привести к гонке с другим вызывающим клиентом. Попробуйте атомарное условие:

./redis ZADD score:demo:best GT 80 p01
./redis ZADD score:demo:best GT 90 p01
./redis ZADD score:demo:best GT 85 p01
./redis ZADD score:demo:best GT 90 p01
./redis --raw ZSCORE score:demo:best p01

Результат остаётся 90, в том числе после повторной попытки с тем же значением. По умолчанию ответ ZADD считает новые элементы, поэтому 0 может означать как успешное обновление существующего элемента, так и проигнорированную попытку. Это не ошибка. Прочитайте фактический результат, чтобы различить эти случаи. GT не препятствует добавлению нового участника; добавление XX запретило бы первые попытки и здесь было бы неверным.

Сохраните функцию для повторного использования, чтобы каждая отправленная строка следовала одному правилу. Предоставленный потребитель передаёт клиент Redis, фактический ключ, ID участника и результат. Python def определяет функцию; имена в скобках получают эти значения. Строка return с отступом отправляет команду и возвращает её фактический ответ. Слова в кавычках — буквальные аргументы команды Redis; имена без кавычек используют значения вызывающей стороны. Устанавливать пакет Python для Redis не нужно: предоставленный клиент отправляет собственные команды Redis.

Следующий here-document записывает полный файл. cat > main.py перезаписывает этот файл. Маркер PY в кавычках не даёт оболочке интерпретировать содержимое; завершающий PY должен стоять отдельно на строке. Сохраните четыре пробела перед return. Вставьте весь блок в Terminal:

cat > main.py <<'PY'
def record(client, key, player, score):
    return client.command("ZADD", key, "GT", score, player)
PY

Воспроизведите подготовленный лист через вашу функцию. Затем воспроизведите его снова и ещё раз отправьте запоздавшую попытку с меньшим результатом:

./desk replay main
./desk data main
./desk replay main
./desk submit main p01 85
./redis --raw ZSCORE score:main:best p01
./redis ZREVRANK score:main:best p01

p01 остаётся с результатом 90; другой Alex, p02, остаётся отдельным элементом с результатом 90. Полное множество также содержит p05=100, p03=95, p06=90, p04=70 и ранее записанный p90=88. data показывает чередующиеся ID и строки результатов в порядке возрастания Redis.

ZREVRANK даёт позицию с нумерацией от нуля для элемента в порядке убывания: первая позиция — 0. Здесь p01 имеет позицию 4, потому что для равных результатов применяется обратный побайтовый порядок ID. Эта позиция не лишает права прохода участника с результатом, равным граничному. Для определения прошедших мы будем использовать результаты, а не фиксированное число ID.

Если вы ошиблись в имени файла или команде, прочитайте ошибку, используйте ls для проверки фактических файлов и повторите исправленную команду. Score desk error отличается от успешного пустого результата. Если вы случайно применили неправильное правило подсчёта, сначала исправьте main.py, изучите исходный лист и используйте обычный ZADD, чтобы восстановить истинный лучший результат каждого затронутого участника; например, ./redis ZADD score:main:best 90 p01. Не редактируйте лист или список участников, не стирайте раунд и не отбрасывайте ранее записанного участника. Запустите проверку шага после изучения фактических результатов.

Включите всех участников на границе отбора

На этом шаге вы превратите граничную позицию в включающую равенство границу по результату, чтобы прошли все участники с одинаковым граничным результатом.

Оставайтесь во вкладке Terminal и в каталоге score-desk. Для этой части приём попыток закрыт. Правило: первые три позиции плюс все, кто набрал столько же, сколько участник на третьей позиции. Здесь два отдельных решения: найти этот результат, затем включить всех участников, соответствующих ему.

ZRANGE читает сортированное множество. Без опции выборки по результату его границы — включительные позиции с нумерацией от нуля. REV меняет порядок результатов на обратный; WITHSCORES включает результат каждого элемента. Третья позиция имеет индекс 2:

./redis --raw ZRANGE score:main:best 0 2 REV WITHSCORES
./redis --raw ZRANGE score:main:best 2 2 REV WITHSCORES

Первая команда возвращает только p05, p03 и p06. Вторая возвращает p06 и 90: граничный результат равен 90. Если остановиться на трёх участниках, будут потеряны p02 и p01, которые оба проходят по указанному правилу равенства результатов.

С BYSCORE границы — это результаты, а не позиции. При обратном порядке сначала указывайте верхнюю границу. +inf означает отсутствие верхнего предела. Обычная граница результата включает равенство; граница, начинающаяся с (, исключила бы равенство и ошибочно потеряла бы всех участников с граничным результатом. Прочитайте полный набор прошедших:

./redis --raw ZRANGE score:main:best +inf 90 BYSCORE REV WITHSCORES

Появляются все пять ID в порядке p05, p03, p06, p02, p01. Порядок ID при равных результатах — соглашение отображения Redis, а не правило исключения участников с одинаковым результатом или сортировки их имён.

Сделайте это работающим для любого положительного k. Клиент возвращает Python-список чередующихся строк: для одного участника на границе это ["p06", "90"]. Индекс списка 0 — ID, а индекс 1 — результат. Поэтому boundary[1] использует фактический результат из Redis. k - 1 преобразует запрошенную позицию в индекс с нумерацией от нуля. Присваивание = сохраняет этот ответ в локальную переменную boundary.

if not boundary проверяет, пуст ли список. Когда k больше числа участников, проходят все; прочитайте позиции от 0 до -1, где -1 означает последний элемент. Для пустого раунда это также возвращает честный пустой список. Такой запасной путь использует настоящее множество Redis, а не пример ответа.

Добавьте следующую функцию в конец main.py. Здесь >> дописывает файл, а не заменяет существующую функцию записи. Python использует дополнительный отступ под if, чтобы определить условный возврат:

cat >> main.py <<'PY'

def qualify(client, key, k):
    boundary = client.command("ZRANGE", key, k - 1, k - 1, "REV", "WITHSCORES")
    if not boundary:
        return client.command("ZRANGE", key, 0, -1, "REV", "WITHSCORES")
    return client.command("ZRANGE", key, "+inf", boundary[1], "BYSCORE", "REV", "WITHSCORES")
PY

Предоставленное действие board вызывает qualify, а затем добавляет имена из настоящего списка участников. Оно сохраняет возвращённые вами элементы, значения и порядок. Оно не фильтрует заново, не добавляет пропущенного участника с равным результатом и не подставляет правильную границу. Проверьте разные запросы:

./desk board main 3
./desk board main 1
./desk board main 100

Первый запрос возвращает пять объектов участников. Обе записи Alex присутствуют с разными значениями player_id. Второй возвращает только p05 с результатом 100. Последний возвращает всех семерых, включая p90 с 88 и p04 с 70. Чтение таблицы прошедших не отправляет новых попыток.

Два чтения в этой функции предназначены для закрытого раунда. Они не гарантируют один согласованный снимок, пока поступление попыток продолжается. Единственная условная команда записи атомарна; это разные утверждения. Сохраните работоспособность функции записи и запустите проверку шага.

Примените те же правила к забегу на время

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

Оставайтесь во вкладке Terminal и в том же каталоге. Короткий тренировочный забег записывает положительное целое время в секундах, поэтому меньшие результаты лучше. Его независимый лист содержит новые ID; ранее записанный бегун t90 уже имеет 72 секунды. Изучите данные перед записью:

./desk attempts practice
./redis HGETALL score:practice:names
./desk data practice

У t01 есть попытки 70, 60 и 65 секунд. Лучший результат — 60, а не 65. LT меняет существующий результат только тогда, когда новое значение меньше; новые элементы он по-прежнему добавляет. Сравните те же три попытки на временном ключе:

./redis ZADD score:demo:time LT 70 t01
./redis ZADD score:demo:time LT 60 t01
./redis ZADD score:demo:time LT 65 t01
./redis --raw ZSCORE score:demo:time t01

Результат остаётся 60. Обоснование атомарного обновления и безопасности повторов такое же, как для GT; меняется только направление улучшения.

Для таблицы, в которой меньший результат лучше, используйте порядок возрастания, опустив REV. На граничной позиции k - 1 включайте всех с результатом меньше или равным этому результату. Прямой диапазон по результату начинается с -inf, что означает отсутствие нижнего предела. Сохраните полный независимый шаблон в practice.py; он использует точно те же интерфейсы функций, что и основной раунд:

cat > practice.py <<'PY'
def record(client, key, player, score):
    return client.command("ZADD", key, "LT", score, player)

def qualify(client, key, k):
    boundary = client.command("ZRANGE", key, k - 1, k - 1, "WITHSCORES")
    if not boundary:
        return client.command("ZRANGE", key, 0, -1, "WITHSCORES")
    return client.command("ZRANGE", key, "-inf", boundary[1], "BYSCORE", "WITHSCORES")
PY

Отправьте тренировочный лист, изучите границу и включающий её диапазон собственными командами Redis, а затем запросите результат у настоящего потребителя таблицы прошедших:

./desk replay practice
./redis --raw ZRANGE score:practice:best 1 1 WITHSCORES
./redis --raw ZRANGE score:practice:best -inf 60 BYSCORE WITHSCORES
./desk board practice 2
./desk replay practice
./desk submit practice t01 65
./desk board practice 2
./desk board practice 100
./desk board main 3

Граница — t01 с 60. Таблица первых двух позиций с учётом равенства содержит t03=50, t01=60, t02=60 и t04=60. Одинаковые имена Sam принадлежат разным ID. Запоздавшие 65 не могут заменить 60, а воспроизведение всего листа не меняет ни одного лучшего результата. Большая граница включает всех шестерых бегунов, в том числе t90=72. Основной отбор из пяти участников остаётся доступным.

При равном времени Redis упорядочивает ID побайтово по возрастанию; основная таблица на максимум очков использует побайтовый порядок убывания из-за REV. Ни одно соглашение не исключает участника с равным результатом. Для этих подготовленных ASCII ID указанный порядок стабилен.

Теперь у вас есть полные методы записи и отбора для обоих направлений. Сохраните оба файла, оба раунда, исходные листы, списки участников и результаты предыдущего раунда. Проверки испытывают ваши неизменённые функции на других ID, значениях, порядках поступления и границах отбора. Запустите итоговую проверку после изучения настоящих результатов.

Резюме

Вы сохранили стабильные идентификаторы участников, записали лучшие абсолютные результаты атомарными условными обновлениями Redis и отделили направление сравнения результатов от порядка отображения. Вы использовали фактический результат на k-й позиции, чтобы включить всех с равным результатом на границе, в том числе участников с одинаковыми именами, и сохранили ранее записанные результаты. Ваши функции подходят и для соревнований на максимум очков, и для забегов с целым числом секунд. Чтения для определения прошедших предполагают закрытый приём попыток; сохранение данных на диске, апелляции и снимки во время изменений — отдельные задачи.