최고 기록을 유지하고 경계 동점자 포함하기

RedisBeginner
지금 연습하기

소개

Field Day 점수 데스크에는 시도 결과가 순서대로 도착하지 않습니다. Alex의 세 시도 점수는 80, 90, 85점입니다. 최고 기록은 90입니다. 이것들은 더해야 할 점수가 아니라 각 시도의 절대 기록입니다. 다른 참가자의 이름도 Alex이므로 이름으로 참가자를 식별할 수 없습니다. 3위 점수가 동점이면 그 점수의 모든 참가자가 진출합니다.

재사용 가능한 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라는 이름의 서로 다른 사람에게 속합니다. 준비된 본 라운드 정렬 집합에는 현재 점수 88인 p90만 있습니다. 이 참가자는 계속 남아 있어야 합니다. 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

마지막 명령이 90을 덮어썼기 때문에 최종 점수는 85입니다. ZSCORE는 한 멤버의 점수를 읽고, --raw는 CLI에 일반 값 출력을 요청합니다. ZINCRBY는 대신 시도 값을 더해 이 세 값에서 255를 만듭니다. 증가분에는 적합한 연산이지만 이 시도들은 증가분이 아닙니다.

GT를 사용한 ZADD는 새 멤버를 추가하거나 새 값이 더 클 때만 기존 점수를 변경합니다. 비교와 갱신은 하나의 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는 Redis 오름차순으로 ID와 점수 문자열을 번갈아 표시합니다.

ZREVRANK는 내림차순에서 멤버의 0부터 시작하는 위치를 알려 줍니다. 첫 위치는 0입니다. 동점에서는 ID의 바이트 단위 역순을 사용하므로 여기서 p01의 위치는 4입니다. 그 위치 때문에 진출 점수의 동점자가 탈락하는 것은 아닙니다. 진출자 선정에는 고정된 수의 ID가 아니라 점수를 사용합니다.

파일 이름이나 명령을 잘못 입력했다면 오류를 읽고 ls로 실제 파일을 확인한 뒤 수정한 명령을 다시 시도하세요. Score desk error는 성공한 빈 결과와 다릅니다. 실수로 잘못된 점수 규칙을 적용했다면 먼저 main.py를 수정하고 원본 기록표를 확인한 다음 일반 ZADD로 영향을 받은 각 참가자의 실제 최고 기록을 복원하세요. 예를 들어 ./redis ZADD score:main:best 90 p01입니다. 기록표나 명단을 편집하거나, 라운드를 지우거나, 이전 참가자를 버리지 마세요. 실제 결과를 확인한 후 단계 검사를 실행하세요.

경계 점수의 모든 참가자 포함하기

이 단계에서는 모든 동점자가 진출하도록 순위 경계를 같은 점수까지 포함하는 점수 경계로 바꿉니다.

계속 Terminal과 score-desk 디렉터리를 사용하세요. 이 부분에서는 제출이 마감되었습니다. 규칙은 상위 세 자리와 3위 점수에 동점인 모든 참가자입니다. 판단은 두 가지로 나뉩니다. 그 점수를 찾은 다음 조건을 만족하는 모든 참가자를 포함합니다.

ZRANGE는 정렬 집합을 읽습니다. 점수 옵션이 없으면 경계는 양 끝을 포함하는 0부터 시작하는 위치입니다. 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은 요청된 위치를 0부터 시작하는 인덱스로 변환합니다. 대입 =는 그 응답을 지역 변수 boundary에 저장합니다.

if not boundary는 리스트가 비었는지 검사합니다. k가 참가자 수보다 크면 모두 진출합니다. 위치 0부터 마지막 멤버를 뜻하는 -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 값으로 나타납니다. 두 번째는 100인 p05만 반환합니다. 마지막은 88인 p90과 70인 p04를 포함한 일곱 참가자 모두를 반환합니다. 순위표를 읽는다고 새 시도가 제출되지는 않습니다.

이 함수의 두 읽기는 마감된 라운드를 위한 것입니다. 제출이 계속되는 동안 하나의 일관된 스냅샷을 보장하지 않습니다. 단일 조건부 기록 명령이 원자적이라는 주장과는 다릅니다. 기록 함수가 계속 동작하게 유지하고 단계 검사를 실행하세요.

같은 규칙을 기록 경주에 적용하기

이 단계에서는 독립적인 연습 라운드에서 낮을수록 좋은 기록 및 진출자 선정 방법을 완성하고 사용합니다.

계속 Terminal과 같은 디렉터리를 사용하세요. 짧은 연습 경주는 양의 정수인 **경과 시간(초)**을 기록하므로 점수가 낮을수록 좋습니다. 독립 기록표에는 새 ID가 있으며 이전 주자 t90에게는 이미 72초가 기록되어 있습니다. 쓰기 전에 확인하세요:

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

t01의 시도는 70초, 60초, 65초입니다. 최고 기록은 65가 아니라 60입니다. 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

경계는 60인 t01입니다. 상위 두 자리와 동점자를 포함하는 순위표에는 t03=50, t01=60, t02=60, t04=60이 들어 있습니다. 같은 이름 Sam은 서로 다른 ID에 속합니다. 늦게 도착한 65는 60을 교체할 수 없고 기록표 전체를 재생해도 최고 기록은 바뀌지 않습니다. 큰 경계값은 t90=72를 포함한 주자 여섯 명 모두를 포함합니다. 본 라운드의 다섯 명 진출자도 계속 확인할 수 있습니다.

같은 시간에서는 Redis가 ID를 바이트 단위 오름차순으로 정렬합니다. 높은 점수의 본 라운드 순위표는 REV 때문에 바이트 단위 내림차순을 사용합니다. 어느 관례도 동점 참가자를 제외하지 않습니다. 준비된 이 ASCII ID에서는 명시된 순서가 안정적입니다.

이제 두 방향의 기록 및 진출자 선정 방법을 모두 완성했습니다. 파일 두 개, 라운드 두 개, 원본 기록표, 명단, 이전 라운드 결과를 유지하세요. 검사는 변경하지 않은 함수를 다른 ID, 값, 도착 순서, 경계값으로 실행합니다. 실제 결과를 확인한 뒤 최종 검사를 실행하세요.

요약

안정적인 참가자 식별 정보를 유지하고, 원자적인 Redis 조건부 갱신으로 각 시도의 절대 최고 기록을 저장했으며, 점수의 우수 방향과 표시 순서를 구분했습니다. 실제 k번째 점수를 사용해 이름이 같은 참가자를 포함한 모든 경계 동점자를 진출시키고 이전 기록을 보존했습니다. 저장한 함수는 높은 점수 경기와 정수 초 단위 기록 경주에 모두 동작합니다. 진출자 선정의 읽기는 제출이 마감되었다고 가정합니다. 영속성, 이의 제기, 실시간 스냅샷은 별도의 작업입니다.