Ситуация: каждый браузер посетителя получает свой номер в куках, который отныне хранит вечно (в реальности как получится). Это номер нужен мне впоследствии для статистики, учета комментариев и т.п. (например, чтобы голосовали только по одному разу). Это данность, которая лежит в основе движка, и обсуждать это не надо, вопрос в другом.
Практика показала, что пользуются учетным номером в полной мере (регистрируются, прописывают логин-пароль, заполняют карточку) не более 1% посетителей. Глупо хранить пустую на 99% mysql-таблицу с кучей полей. Глупо искать в базе данные при каждом обращении, хотя в 99% случаев данных нет.
SELECT COUNT(*) FROM `unic`
651144SELECT COUNT(*) FROM `unic` WHERE (`login`!='' AND `password`!='') OR `openid`!=''
4140SELECT COUNT(*) FROM `unic` WHERE (`login`!='' AND `password`!='') OR `openid`!='' OR `realname`!='' OR `lju`!='' OR `mail`!='' OR `site`!='' OR `birth`!='0000-00-00'
42769
Но PHP не позволяет где-то хранить или генерировать новый номер+1 с гарантией отсутствия дублей. Как быть? Варианта я вижу два:
1. Две таблицы. В одной — только номер, для которого выполянется auto_increment, после чего номер можно выдать в куку посетителю, а запись смело удалять:
CREATE TABLE `uni` (
`id` int(10) unsigned NOT NULL auto_increment,
)
В другой — вся регистрационная информация, если посетитель ее ввел:
CREATE TABLE `reg` (
`id` int(10) unsigned NOT NULL auto_increment,
`uni` int(10) unsigned NOT NULL,
`login` .....
`password` .....
`realname` ....
....
)
2. Одна таблица `reg` (первую таблицу `uni` не делать). Вместо этого выдавать посетителю длинный номер BIGINT вида 2010122611530000001, где первые цифры 201012261153 — число и время обращения за номером, а 0000001 — случайное число.
Это не вполне чистое решение, потому что теоретически при обращении в одну и ту же секунду, скрипт может выдать два одинаковых номера. Кроме того, тогда придется всюду в учетных записях указывать номер посетителя bigint вместо int, не слишком ли это большая трата ресурсов?
Можно вкратце - что это?
А в чем, простите, отличие UUID от любой другой кодовой строки? Тот же md5, только разделенный в четырех местах минусиками?
SELECT UUID();
Из русской вики:
"Общее количество уникальных ключей UUID составляет 2128 = 25616 или около 3.4 ? 1038. Это означает, что генерируя 1 триллион ключей каждую наносекунду, перебрать все возможные
значения удастся лишь за 10 миллиардов лет."
MD5 и SHA1 не решают вашей задачи, они не генерируют строку, они её модифицируют. Грубо говоря, им на вход нужно давать что-то. И если это что-то генерировать рандомом, это получится просто тоже самое, но запутанней.
Т. к. UUID - стандарт, уже существует множество готовых библиотек под любые языки программирования. Думаю что под PHP тоже существует какая-то стандартная библиотека (я не работаю с этим языком, чтобы сказать уверенней), но быстрый поиск дал вариант проще, вот функция которая сгененрирует вам UUID:
Помимо номера в куках некий md5 с солью и перцем, который подтверждает, что предъявляемый номер действительно выдан мной, а не сфальсифицирован пользователем.
Сам по себе абстрактный рандомный UUID для этой задачи вообще не годится. Что я с ним буду делать? Я выдам пользователю один UUID, а он подставит свой UUID, и я даже не узнаю об этом. Или предлагаете мне бережно хранить на своей стороне все UUID, которые я когда-либо выдавал пользователям, и по предъявлении делать поиск по гигантской базе среди индексированного поля varchar(64)? Так это будет не улучшение существующей сейчас системы с оптимизацией ресурсов, а многократное ее ухудшение.
Та функция, которую я скинул в прошлом сообщении, генерирует случайный UUID в текстовом виде. В таком виде вы его кладёте в кукизы и выводите в админке (причина того что ююиды разделены дефизами - именно для улучшения восприятия). С помощью этой строчки, сгенерированный текстовый UUID можно превратить в число:
$bin = pack("h*", str_replace('-', '', $uuid));
А после этого сохранить его в БД, в поле BINARY(16).
UUID и надо использовать как "идентификатор пользователя в виде номера", тут вы совершенно правы, именно для таких задач он и придуман. В распределённых базах данных, ююиды вообще используются как PRIMARY KEY в таблицах, вместо автоинкремента.
Ещё сделаю небольшую ремарку. Я программирую на Python, и поэтому, когда я узнал о существовании ююидов, передо мной не стоял вопрос использовать их или не использовать. Т. к. в питоне эта штука уже встроена, мне было гораздо удобнее вызвать одну готовую функцию, чем писать что-то своё подобное. Но если в вашем случае, в PHP нет стандартизированных инструментов генерации уникальных идентификаторов, возможно вам и не стоит заморачиваться, а действительно, просто самому сгенерировать случайное число, выдать пользователю и записать в базу. В конце концов, единственное, зачем нужно следовать стандартам - чтобы те люди, которые будут позже разбираться в вашем коде не разбирались в лишних придуманных сущностях. Если это не входит в ваши приоритеты, сделайте как проще. Без иронии, действительно так считаю.
Я: Сейчас у меня скопилось в шкафу много пустых листков бумаги с номером на каждого посетителя, хочу от них избавиться, чтобы номер называли сами посетители.
Вы: Есть прекрасное решение: хранить на каждого посетителя не один лист, а целую папку со случайными кодами!
Я: Кажется вы не поняли, мне даже листки с номером мешают, зачем мне папка с кодами?
Вы: Вы не понимаете! Это чудесная папка с волшебным случайным кодом на 100 страницах! Этот код уникален! Другого такого в мире не найти! Его гениальность в том, что он придуман фирмой Microsoft и вызывается прекрасной встроенной функцией! Вы обязательно должны завести такую папку на каждого своего посетителя!
Я: Позвольте, но проблема в том, что мне даже номер хранить накладно. Я хочу хранить лишь те листки, где есть заполненные поля. А посетителю достаточно самому назвать свой номер и пароль к номеру, чтобы я просто мог удостовериться, что номер выдал ему именно я. Вопрос был лишь в том, как сохранить сквозную нумерацию.
Вы: Нет-нет! Вы не поняли, о чем я говорю! Вы не хотите понять! Это чудесная неповторимая папка с кодом! Она волшебная! И толщиной всего 2 сантиметра! Вам надо завести такую папку и хранить ее на каждого посетителя! Причем, в каждом из ваших шкафов, где вы ведете работу с посетителями!
И так далее по кругу.
> Вы: Есть прекрасное решение: хранить на каждого посетителя не один лист, а целую папку со случайными кодами!
Вот это то место где вы меня неправильно поняли. Я не предлагаю заводить на каждого посетителся какую-то папку. Как раз я согласен с вашим вариантом - каждому посетителю один номер. Просто этот номер не с балды генерить, а использовать в качестве такого номера UUID. "UUID" расшифровывается как "Универсальный Уникальный Идентификатор", т. е. это та штука которую все используют в качестве уникальных номеров любым объектам, в вашем случае - пользователям. Этот стандарт придумала Open Software Foundation, а не Microsoft. Microsoft, по большому счёту - конкурент OSF, они, позже, сделали реализацию этого стандарта и назвали её "GUID", но это не имеет отношения к делу.
Всё остальное, с разряжением таблицы, как вы описали во втором варианте в вашем посте, я нахожу совершенно верным. Мало того, не думаю, что есть какой-то другой способ сделать это более эффективно.
Опять же, мне кажется, что в вашем случае, возможно не имеет смысла разбираться с ненужными стандартами, если это не входит в ваши приоритеты.
1. UUID пользователь может запросто менять на свой, а я об этом даже никогда не узнаю. Это неприятно. В итоге есть мизерная, но вероятность, что однажды код совпадет. И случайный пользователь увидит, например, чужое личное или подзамки.
2. И в базе пользователей, и во всех прочих базах (голосования, комментарии, плюсики-минусики, журнал посещений и т.п.), где я использовал ключевое поле INT, мне придется иcпользовать ключевое поле varchar(16). Что, мягко говоря грубо выражаясь, очень даже совсем не улучшит быстродействие и очень даже не сократит объемы данных в таблицах.
"номер посетителя bigint вместо int" и даже BINARY(16), как предлагали, не будет большой тратой ресурсов, учитывая, что вы хотите избавиться от 651144 пустых записей и оставить только 4140+42769 заполненных
Хотя вероятность коллизии в данном случае маловероятна
Create a table to hold the sequence counter and initialize it:
mysql> CREATE TABLE sequence (id INT NOT NULL);
mysql> INSERT INTO sequence VALUES (0);
Use the table to generate sequence numbers like this:
mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1);
mysql> SELECT LAST_INSERT_ID();
The UPDATE statement increments the sequence counter and causes the next call to LAST_INSERT_ID() to return the updated value. The SELECT statement retrieves that value. The mysql_insert_id() C API function can also be used to get the value. See Section 20.8.3.37, “mysql_insert_id()”.
Функция возвращает значение для текущего соединения с базой, а update гарантирует атомарность.
Можно завести таблицу для сиквенсов, добавлять в нее поля с именем последовательности и сделать функци на пхп в которую передается название, а она увеличивает сиэквенс и возвращает значение.
seq_getnext(seq_name)
UPDATE sequence SET seq_name=LAST_INSERT_ID(seq_name+1);
SELECT LAST_INSERT_ID();
2. microtime() + сборка результата в bigint , два обращения в микросекунду маловероятны.
UPDATE `site` SET `value`=`value`+1 WHERE `name`='last_id'
SELECT `value` FROM `site` WHERE `name`='last_id'
последовательно
a=5
1.update set a=a+1;
1.select a;
2.update set a=a+1;
2.select a;
result.1=6
result.2=7
OK
===
параллельно
a=5
1.UPDATE SET a=a+1;
2.UPDATE SET a=a+1;
1.SELECT a;
2.SELECT a;
result.1=7
result.2=7
не ОК
LAST_INSERT_ID() возвращает последнее записанное значение в текущем соединении с базой
т.е. если есть десяток подключений, и в каждом идет UPDATE, то каждому вернется значение, записанное именно в его соединении с базой, а не последний UPDATE на текущий момент.
В этом случае между update и select можно хоть час поставить паузу, главное не выполнять других запросов к базе и не рвать соединение.
Для задач, когда нужен независимый счетчик для вообще всех объектов, уникальный и увеличивающийся.
if(msq_add("INSERT ...."))===false) { trevoga("DB ADD FALSE!!!!"); }
$unic=mysql_insert_id(); if(!$unic) { trevoga("insert_id():".$unic); }
Шо интересно: за весь год "trevoga()" в логе ни разу не сработала, хотя я поначалу опасался каких-то сбоев. Надо ее убирать.
Это очень дорогая операция, тем более на таблицах с миллионами записей.
mysql> create table sequences (guid int(10) unsigned not null auto_increment, primary key (guid));
Query OK, 0 rows affected (0.04 sec)
mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.02 sec)
mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.01 sec)
mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)
mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)
mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
4 rows in set (0.00 sec)
mysql> delete from sequences;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)
mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
set @uni:=@uni+1;
release_lock()
ну и при старте сервака
select max() into @uni from ...
Но решение универсально, можно написать и на PHP.
Переменная может быть и пхп-шной, если они там есть, и вообще чем угодно глобальным.
Пустые колонки места на диске физически не занимают, поэтому переживать, что каждый новый посетитель создает новую запись в БД, не стоит. Разумно только не создавать записи для заведомо единичных посещений (например, поисковые роботы), и ограничить время хранения незарегистрированных записей, если они повторно не заходят (завести колонку "время последнего обращения", обновляющуюся при каждом запросе браузера, а по крону периодически удалять незарегистрированные записи, время последнего обращения у которых больше года, скажем).
При таком подходе любое первичное обращение пользователя, по существу, считается его регистрацией на сайте (подтверждение по почте не обязательно). На пользователя заводится (пустая) карточка, которую, при желании, он может заполнить (опять же, частично). Например, я не хочу разглашать свой емэйл, но желаю указать дату своего рождения - пожалуйста!
В любой момент пользователь может подтвердить свою регистрацию, указав емэйл или OpenID
2) И что, какая сложность в поиске по первичному ключу? Да хоть миллиард записей будет, это сильно не напряжет сервер.
3) Тридцать мегов база на миллион записей - это порядка тридцати байтов на одну запись. Что-то мне кажется, что многовато для почти пустой строки. Какая структура у таблицы посетителей?
Но у него ведь не было регистрации, всех дел, у него будет уже другой браузер и другой компьютер. Зачем хранить запись незарегистрированного пользователя бесконечно? чтобы узнать когда он зайдёт в следующий раз? некоторые меняют ОС и айпи каждый день.
Но вот подзамок, капчу-карму и прочие настройки все равно придется выносить в отдельную базу - ведь они теперь будут разными в зависимости от того, кто владелец в данном разделе.
А вот с md5 хотелось бы разобраться. Говорят, он взламывается перебором за несколько часов при помощи софтин, использующих процессоры видеокарт. Надо спрашивать Зефирова.
Про md5 не понял. Я говорю про идентификатор пользователя, который и так виден, стоит линую карточку открыть. А так, да, взламывается. Штука в том, что одному md5-хешу могут соответствовать несколько разных текстовых строк.
Я бы сделал две таблицы, как в первом варианте (и смотреть номер через last_insert_id(), как правильно сказали, никаких microtime()), но в первую таблицу добавил бы второй столбец - индекс в таблице 'reg'. Чтобы поиском по первой таблице можно было сразу понять, регистрировался человек или нет, надо ли искать его данные во второй таблице.
Другой вариант: в одной таблице просто счётчик (один столбец, одна строка, атомарное увеличение), в другой - регистрационные данные, а хранить информацию о том, зарегистрирован ли пользователь, на стороне пользователя, и не искать его данные в таблице 'reg', если он сам куками не сказал, что он зарегистрирован.
Применение алгоритмов, которые изначально (by design) с некоторой (малой) вероятностью дают глюк, я считаю злом. Глюков обычно хватает и от реализации, не нужно их вероятность изначально в алгоритм закладывать, даже если эта вероятность мала.
Если делать уникальные ID без счетчика (как в пункте 2, только придумать алгоритм чтобы не было одинаковых номеров 100%, или, как уже предлагали, через GUID) - то оставить все как есть, просто не записывать данные пустые.
Подозреваю, что вопрос решается просто разделением таблицы на две. В принципе если б я знал структуру базы, то просто мог бы что называется ткнуть пальшцем где и чего надо подпровлять.
Вопрос не решается базой, поскольку вопрос именно в отказе от базы в 95% случаев. И я его давно решил, если что, с помощью перехода на bigint с разделением диапазонов, только пока времени не было реализовать решение.
всего комментариев: 49