В прошлом уроке отсортированное множество хранило журнал запросов. Теперь его главная работа. Магазин запускает читательский марафон: покупатели читают книги в приложении и получают очко за каждую страницу. На главной нужна таблица лидеров, а каждому покупателю хочется видеть своё место и тех, кто рядом. В уроке про sorted sets мы уже строили рейтинг продаж. Сегодня доводим таблицу лидеров до настоящей: место с соседями, ничьи, рейтинг за неделю и страницы.
Табло марафона
На финише городского марафона висит большое табло. Сверху первая десятка. А у каждого бегуна в приложении своя строчка: «Ты 1342-й, впереди Иванов на 4 секунды, позади Петров на 2». Табло обновляется каждую секунду, пока тысячи бегунов пересекают отметки.
Хранить такое табло в обычной таблице базы дорого: чтобы узнать место бегуна, базе нужно посчитать, сколько людей бежит быстрее, а это проход по большому индексу на каждый запрос. Отсортированное множество держит элементы упорядоченными всегда, поэтому место элемента Redis знает почти сразу, даже среди миллионов.
Общий рейтинг
Каждая прочитанная пачка страниц это ZINCRBY: прибавить очки покупателю, а если его ещё нет, создать с этими очками.
127.0.0.1:6379> ZINCRBY lb:readers 40 user:7
"40"
127.0.0.1:6379> ZINCRBY lb:readers 120 user:3
"120"
127.0.0.1:6379> ZINCRBY lb:readers 75 user:12
"75"
127.0.0.1:6379> ZINCRBY lb:readers 75 user:5
"75"
127.0.0.1:6379> ZINCRBY lb:readers 30 user:9
"30"
127.0.0.1:6379> ZINCRBY lb:readers 210 user:1
"210"
127.0.0.1:6379> ZINCRBY lb:readers 15 user:7
"55"
Покупатель 7 дочитал ещё 15 страниц, и ZINCRBY вернул его новый счёт 55. Тройка лидеров:
127.0.0.1:6379> ZRANGE lb:readers 0 2 REV WITHSCORES
1) "user:1"
2) "210"
3) "user:3"
4) "120"
5) "user:5"
6) "75"
REV переворачивает порядок: больше очков, выше место.
Моё место
ZREVRANK возвращает позицию элемента в перевёрнутом порядке. Позиции считаются с нуля, так что человеческое место это ответ плюс один:
127.0.0.1:6379> ZREVRANK lb:readers user:12
(integer) 3
127.0.0.1:6379> ZREVRANK lb:readers user:12 WITHSCORE
1) (integer) 3
2) "75"
127.0.0.1:6379> ZREVRANK lb:readers user:404
(nil)
Покупатель 12 на четвёртом месте. Флаг WITHSCORE появился в Redis 7.2 и сразу добавляет очки, без отдельного ZSCORE. Для покупателя, которого в рейтинге нет, ответ (nil): он ещё не прочитал ни страницы.
Соседи по таблице
Строчка «впереди Иванов, позади Петров» собирается из двух команд. Узнаём позицию через ZREVRANK, потом берём диапазон вокруг неё через ZRANGE ... REV. Покупатель 12 на позиции 3, соседи по два с каждой стороны это позиции с 1 по 5:
127.0.0.1:6379> ZRANGE lb:readers 1 5 REV WITHSCORES
1) "user:3"
2) "120"
3) "user:5"
4) "75"
5) "user:12"
6) "75"
7) "user:7"
8) "55"
9) "user:9"
10) "30"
На Node.js это короткая функция. Единственная тонкость: у лидера соседей сверху нет, и начало диапазона не должно уйти в минус.
async function aroundMe(key, member, radius = 2) {
const rank = await redis.zRevRank(key, member);
if (rank === null) return null;
const start = Math.max(0, rank - radius);
const rows = await redis.zRangeWithScores(key, start, rank + radius, { REV: true });
return rows.map((row, i) => ({ place: start + i + 1, ...row }));
}
console.log(await aroundMe('lb:readers', 'user:12'));
console.log(await aroundMe('lb:readers', 'user:1'));
[
{ place: 2, value: 'user:3', score: 120 },
{ place: 3, value: 'user:5', score: 75 },
{ place: 4, value: 'user:12', score: 75 },
{ place: 5, value: 'user:7', score: 55 },
{ place: 6, value: 'user:9', score: 30 }
]
[
{ place: 1, value: 'user:1', score: 210 },
{ place: 2, value: 'user:3', score: 120 },
{ place: 3, value: 'user:5', score: 75 }
]
Конец диапазона за пределы таблицы уходить может: ZRANGE просто вернёт меньше элементов. Две команды между собой не связаны, и между ними чей-то ZINCRBY может сдвинуть таблицу. Для экрана «твоё место» это не страшно, а если нужна точность, обе отправляют в одной MULTI.
урок из курса
Урок из курса «Redis с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.