Генерация 1млн билетов со случайными уникальными ID

Добрый день!

Задача следующая:
Есть 1 млн «билетиков», которые могут покупаться игроками. При покупке игроку выдается билетик с номером от 1 до 999999. Номера должны быть случайные (не последовательные) и уникальные. Каждый игрок может купить сколько угодно билетиков.

Средства:
PHP + MySQL. Скорее всего, табличка (ticket_id, user_id).

Условия:
Высокая нагрузка. Вероятна продажа всех билетов.

Интересно, кто как решил бы такую задачу? Мне пока на ум пришло только:

1. При покупке билетика генерируем рандомное число (номер билета) и проверяем, есть ли такое в базе (уже куплено). Если есть — генерируем следующее. Не нравится тем, что чем больше продано билетов, тем дольше оно будет работать. А уж купить последний билет вообще кажется невозможным.

2. Заранее сгенерить в базе 1млн записей с номерами билетов и выбирать WHERE user_id=0 ORDER BY RAND() LIMIT 1;

Пока склоняюсь ко второму варианту. Может кто-то еще знает (или придумает) какие-то решения?
Спасибо
  • Вопрос задан
  • 10830 просмотров
Пригласить эксперта
Ответы на вопрос 24
negasus
@negasus
Developer
Как вариант заранее сгенерить табличку с 1млн записей, только уже в табличке эти записи перемешать.
А выдавать — по-порядку.
Ответ написан
Комментировать
evgeny_eJ
@evgeny_eJ
Записать в базу 1 млн билетов в случайном порядке. После этого выбирать по порядку билеты.
Ответ написан
Stdit
@Stdit
Как вариант, можно завести дополнительное поле (sort) у билетов и заполнить его уникальными случайными числами. После этого сделать индекс по user_id и sort. Тест на таблице в миллион записей (InnoDB):

mysql> SELECT * from ticket WHERE user_id = 0 ORDER BY sort LIMIT 10;
+--------+------+---------+
| id     | sort | user_id |
+--------+------+---------+
| 923164 |    1 |       0 |
| 171274 |    2 |       0 |
| 217458 |    3 |       0 |
| 182627 |    4 |       0 |
| 183120 |    5 |       0 |
| 483756 |    6 |       0 |
| 210156 |    7 |       0 |
| 362920 |    8 |       0 |
| 311591 |    9 |       0 |
| 545096 |   10 |       0 |
+--------+------+---------+
10 rows in set (0.00 sec)

mysql> UPDATE ticket SET user_id = 1 WHERE id IN (923164, 171274, 217458);
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3  Changed: 3  Warnings: 0

mysql> SELECT * from ticket WHERE user_id = 0 ORDER BY sort LIMIT 10;
+--------+------+---------+
| id     | sort | user_id |
+--------+------+---------+
| 182627 |    4 |       0 |
| 183120 |    5 |       0 |
| 483756 |    6 |       0 |
| 210156 |    7 |       0 |
| 362920 |    8 |       0 |
| 311591 |    9 |       0 |
| 545096 |   10 |       0 |
| 230442 |   11 |       0 |
| 472816 |   12 |       0 |
| 138187 |   13 |       0 |
+--------+------+---------+
10 rows in set (0.00 sec)

Ответ написан
Комментировать
Koroed
@Koroed
в случае с условием 1 млн от 0 до 999999 лучше все сгененрировать и перемешать заранее.
а вот если вам надо действительно много билетов, то вам может помочь GUID/UUID, благо где он только не реализован всеми возможными способами.
Ответ написан
antonevich
@antonevich
захэшировать, отфильтровать по хэшу =))
Ответ написан
Комментировать
IDDQD
@IDDQD
простейший вариант:
$rand = rand(1, 1000000);
… WHERE ticket_id >=$rand AND user_id=0 LIMIT 1;

Если нагрузки действительно высокие, то генерируем заранее таблицу ticket_id(PK)|rand_num|user_id с уникальным rand_num, а в кеше храним инкремент от последнего купленного ID. Читаем и апдейтим исключительно по PK, и как фейловер — небольшая процедура получения ID последнего купленного билета из бд на случай падения кеша.
Ответ написан
karellen
@karellen
Если это не синтетическая задача типа «как бы вы забили гвоздь, имея только молоток», то с такими задачами хорошо справляется Redis — в set можно записать заранее все номера (в любом виде — хоть числа, хоть строки, хранить миллион номеров понадобится всего пару-тройку МБ памяти) и через SPOP получать случайное число с алгоритмической сложностью О(1).
Ответ написан
CrazySquirrel
@CrazySquirrel
CREATE TABLE t (ticket_id INT, user_id INT default 0, PRIMARY KEY (ticket_id));
INSERT INTO t(ticket_id) VALUES(rand()*1000000); //Генерация может занять долго, зато 1 раз :-)
UPDATE t SET user_id = 1 WHERE user_id = 0 limit 1; //Продаём билет.
Ответ написан
@mithraen
Сделать таблицу с _3-мя_ полями:
ticket_id (внутренний ID, последовательный)
ticket_number (номер билета)
user_id (ID купившего)

При создании таблицы поле ticket_id заполнять последовательно, а ticket_number в случайном порядке. В итоге мы легко можем получить следующий случайный не проданный билет:

SELECT * FROM tickets WHERE user_id IS NULL ORDER BY ticket_id LIMIT 1;

индекс создаем один по двум полям — user_id и ticket_id. Этот индекс как ускорит этот запрос, так и поможет получить список билетов, принадлежащих конкретному пользователю.

только про блокировки надо не забывать — иначе при покупке билета могут быть коллизии.
Ответ написан
Комментировать
Edro
@Edro
Попробуйте это со своими коэффициентами. Где-то придется хранить предыдущее сгенерированное число.
Ответ написан
Комментировать
@softm
Разделите задачу на 100 частей, будет проще решать
Ответ написан
Комментировать
wscms
@wscms
ORDER BY RAND это будет жесть
Разбейте на интервалы с шагом, скажем, 5 000

Первый рандом — выбор интервала, скажем от 200000 до 205000
Потом проверка пробегитесь по этому интервалу и найдите все «пустые» значения. Среди них выберите случайное.
Если «пустых» нет — пометьте, что этот интервал занят полностью и больше его не используйте
Ответ написан
Комментировать
depporter
@depporter
В соседнем посте, как раз обсуждается вопрос рандомного выбора. habrahabr.ru/post/154905/
Ответ написан
Комментировать
@MikhailEdoshin
Мне нравится вариант с перемешать заранее, но если вам требуется не настоящая случайность, а просто чтобы номера не выглядели последовательными, то можно использовать последовательные номера с перемешанными битами. Например, если взять пять бит, перенумеровать биты по порядку справа налево 5, 4, 3, 2, 1 и перемешивать их, например, как 4, 1, 2, 5, 3, то последовательность 1, 2, 3, 4, 5, 6 превратится в 8, 4, 12, 16, 24, 20.

PS: Неужели MySQL затормозит на индексе в 1 млн. чисел?
Ответ написан
При количестве 1млн из 1мл допустимых значений ни какой рандомности не может быть. Думайте как выдавать вообще не используя таблицу.
Первое что пришло в голову мне, это использовать мемкеш для хранение уже выданных id.

$rand = rand(1, 1000000);
$memkey =«ticket_».$rand;
if ($get_result = $memcache->get($memkey))
{
Такой билет уже продан, генерим другой
}
{
Билет не продан. Записываем в БД и в мемкеш
$memcache->set($memkey, $rand, false);
}
Ответ написан
@tenshi
не надо заранее ничего генерировать. подумайте, что вы будете делать если в процессе работы сервиса вдруг выяснится, что число билетов надо увеличить или наоборот уменьшить.

www.php.net/manual/ru/function.uniqid.php

для каждого билета генерируем идентификатор и сохраняем в базу. контролируем чтобы число записей в базе не превышало нужного нам ограничения. всё. логика простая и эффективная.
Ответ написан
@stg34
Я (почти) такую задачу решал следующим образом
генерируем строки от
«000000»
до
«999999»
и шифруем их, и получаем миллион разных строк.

При чем, в моем случае, номера билетов можно было не хранить в базе, их можно было расшифровывать.
Ответ написан
Комментировать
DemiGray
@DemiGray
Что-то или я туплю или вы ищете слишком сложное решение.
Сделайте таблицу free_tickets в которой будет 1 миллион записей (и где у каждого билета будет GUID) и таблицу purchased_tickets с пустыми записями.

Когда покупают билет — кидайте рендом от 0 до количества зписей в free_tickets. Выпадает порядковый номер записи в таблице. Этот билет перемещаем в purchased_tickets.

Как бы… всё :) ЧЯДНТ?
Ответ написан
la0
@la0
Парампампам. КостылиКостылиКостылиКостылиКостылиКостыли

Генерируйте ваш мильён записей
tbl (тип — MEMORY!!!!!)
id|crc
Считаете любой хеш (напр.CRC32) записи, берёте 5-8 символов с любой стороны и пишите в поле (один долгий запрос вида, update tbl set crc = substr (5,7,CRC(concat(id,'somerandsmthng'))) ага) обязательно ставите индекс по этому остатку CRC.
insert into newtbl select id as tktid, 0 as uid from tbl order by crc;
В итоге мильён довольно перемешанных записей.
Как-то так.
Ответ написан
DeathCore
@DeathCore
SELECT t.id, t.flag FROM test t,
    (SELECT id mn FROM test WHERE flag = 0 LIMIT 1) tmp1,
    (SELECT ROUND((SELECT MAX(id) FROM test)*rand()) rnd FROM test LIMIT 1) tmp2
WHERE t.id >= rnd AND t.id > mn AND t.flag = 0
LIMIT 1

Первичный ключ id и комбинированный индекс на (id, flag)

на таблице в 36 миллионов записей проходит за 0.002 секунды.
Ответ написан
DeathCore
@DeathCore
SELECT t.id, t.flag FROM test t,
    (SELECT id mn FROM test WHERE flag = 0 LIMIT 1) tmp1,
    (SELECT ROUND(((SELECT id FROM test WHERE flag = 0 ORDER by id DESC LIMIT 1)-(SELECT id FROM test WHERE flag = 0 LIMIT 1))*rand()+(SELECT id FROM test WHERE flag = 0 LIMIT 1)) rnd FROM test LIMIT 1) tmp2
WHERE t.id >= rnd AND t.id >= mn AND t.flag = 0
LIMIT 1


Учитывая вот этот вот комментарий:
Разве при таком подходе каждый купленный билет не повышает вероятность покупки следующего порядкового билета? К примеру, если куплены все билеты от 0 до 500000, то любое случайное число от 0 до 500000 приведёт к покупке 500001 билета. Таким образом, вероятность его покупки будет примерно 50%.


Переделал запрос. Теперь в такой ситуации будет генерироваться случайное число от 500 000 до 1 000 000
Ответ написан
Какого формата номер должен быть?
Ответ написан
Комментировать
@GreenPeace
Могу предложить следующий способ:

Выбирается некоторое число, взаимно простое с общим числом билет — в данном случае с числом 999999. Выбрать такое число несложно. Далее — первый билет это остаток от деления выбранного числа на общее число билетов, следующий — это остаток удвоенного выбранного числа. И т.д. Приведу пример. Если выбранное число 1 — то получим обычный порядок по возрастанию. Если 2, то — 2,4,6,8… 999998,1,3… Не трудно показать, что такая последовательность будет содержать все возможные варианты.

Плюсы такого подхода:
1) Очень простая генерация следующего варианта
2) Нет проблем с получением последнего билета

Минусы:
1) Это приближенное решение задачи, и получить любую последовательность таким образом не получиться. В частности общее число различных последовательностей = 999999 — [количество чисел < 999999 и не взаимно простых с ним].
2) В последовательности можно отследить некоторые закономерности. Так, например, номер каждого 3-го билета будет кратен 3-м.
Ответ написан
Комментировать
@Samuel_Leonardo
Немного скоректировал вариант IDDQD, может вам подойдет?
SELECT Count(*) as count_aviable from ticket WHERE user_id = 0;
$rand = rand(1, count_aviable);
… WHERE user_id=0 LIMIT $rand,1;
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы