LINUX.ORG.RU

PHP/MySQL unique record race condition


0

1

Положим имеется MySQL таблица:

create table users (
    id bigint unsigned auto_increment primary key,
    username varchar(32) not null,
    password varchar(32) not null
);

И форма для регистрации нового юзера. Естественно username должно быть уникально. Поэтому требуется проверить не существует ли уже юзер с таким username:

if (!isset($_POST['username']) || strlen($_POST['username']) > 32 || strlen($_POST['username']) == 0) {
    $errors[] = "Username cannot be blank / longer than 32 characters";
} else {
    $q = mysql_query(
        "SELECT COUNT(id) AS u_ex FROM users ".
        "WHERE username = '".mysql_real_escape_string($_POST['username'],$db)."'",
        $db
    ) or die("MySQL error: ".mysql_error($db)." on line ".__LINE__);
    $row = mysql_fetch_assoc($q);
    mysql_free_result($q);
    if ($row['u_ex'] != 0) {
        $errors[] = "Username already in use";
    }
}

// ................................
// ....Check other fields..........
// ................................

if (count($errors) == 0) {
    mysql_query(
        "INSERT INTO users SET ".
            "username = '".mysql_real_escape_string($_POST['username'],$db)."',".
            "password = '".mysql_real_escape_string($_POST['password'],$db)."'",
        $db
    ) or die("MySQL error: ".mysql_error($db)." on line ".__LINE__);
}

Однако в этом коде есть подвох. Несмотря на то что он проверяет наличие юзера с таким username («SELECT COUNT(id) AS u_ex....») между этой проверкой и «INSERT INTO users........» теоретически (хотя и маловероятно) параллельно исполняющийся процесс может осуществить проверку и вставить нового юзера с таким username, что в результате приведёт к существованию 2 юзеров с одинаковым username, что совершенно недопустимо. Т.е.:

1. Процесс #1 проверяет наличие юзера с таким username и приходит к выводу что путь свободен.
2. OS scheduler переключает контекст выполнения на другой процесс #2.
3. Процесс #2 проверяет наличие юзера с таким username, приходит к выводу что путь свободен, и успевает вставить юзера.
4. Управление возвращается к процессу #1. Процесс #1 уже проверял наличие юзера username и поэтому просто вставляет юзера с уже вставленным процессом #2 username'ом.

Вопрос как этого избежать.

Конечно можно (и нужно) сделать поле username c параметром UNIQUE (username VARCHAR(32) not null UNIQUE). Это приведёт к ошибке при 2-ом SQL запросе в процессе #1. Можно проверить значение возврата из mysql_query и если оно === FALSE (что сигнализирует об ошибке) выдать сообщение о том что «Username already in use». Однако что если в таблице есть ещё email которое то же должно быть уникальным для каждого юзера. Какую ошибку выдавать в браузер? О том что username используется или что email используется? Да и вообще не красиво это как-то, и теоретически могут возникнуть ошибки с этим не связанные.

Так же можно сделать «LOCK TABLES users WRITE» перед первым запросом. Однако это, насколько я понимаю, плохо для производительности, т.е. неэффективное решение.

Предлагайте варианты решения данной проблемы, желательно с примерами и кодом.

>Однако что если в таблице есть ещё email которое то же должно быть уникальным для каждого юзера. Какую ошибку выдавать в браузер?

У тебя уже должен быть готовый код на проверку уникальности login и email. Вот на него при обнаружении ошибки и заворачивать.

Так же можно сделать «LOCK TABLES users WRITE» перед первым запросом.


У тебя планируется регистрировать по 1000 пользователей в секунду? O_o

KRoN73 ★★★★★ ()
Ответ на: комментарий от KRoN73

У тебя уже должен быть готовый код на проверку уникальности login и email. Вот на него при обнаружении ошибки и заворачивать.

Прочитайте внимательно, я же разъяснил что может возникнуть ситуация в которой этого кода будет не достаточно.

У тебя планируется регистрировать по 1000 пользователей в секунду?

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

par12b12 ()
Ответ на: комментарий от par12b12

>Прочитайте внимательно, я же разъяснил что может возникнуть ситуация в которой этого кода будет не достаточно.

Ты не понял. Речь идёт не о первичной проверке, а об отсылке на тот же код в случае возникновения MySQL-ошибки на вставке UNIQUE.

На пальцах последовательность в случае такой ошибки:

1. Юзер вводит данные в форму
2. Проверяем валидность введённых данных. Если есть ошибка - заворачиваем на 1. с индикацией ошибки
3. Создаём юзера. Если возникла ошибка создания из-за конфликта при UNIQUE-вставке, то заворачиваем на пункт 2. Ошибка там всплывёт и покажется юзеру как если бы конфликта не было.

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


Я начинаю понимать людей, которые считают преждевременную оптимизацию быдлокодерством :)

KRoN73 ★★★★★ ()
Ответ на: комментарий от KRoN73

Если возникла ошибка создания из-за конфликта при UNIQUE-вставке, то заворачиваем на пункт 2. Ошибка там всплывёт и покажется юзеру как если бы конфликта не было.

Вот спасибо, и как я сам не догадался. Ещё вопрос: можно ли как то определить что ошибка из-за UNIQUE а не по какой-то другой причине?

par12b12 ()
Ответ на: комментарий от par12b12

хотя вобще то ясно, что если проверка не дала ошибки значит причина другая, если же дала значит понятно в чём ошибка. Спасибо за подсказку, теперь всё ясно.

par12b12 ()

топик не читал и тред тоже, но тебя спасут транзакции.

true_admin ★★★★★ ()
Ответ на: комментарий от par12b12

Error: 1169 SQLSTATE: 23000 (ER_DUP_UNIQUE)
Message: Can't write, because of unique constraint, to table '%s'

RR ()
Ответ на: комментарий от true_admin

> есть желание профессионально расти на случай трудоустройства в гугле))))


автор как бы в гугель с пыхом рвётся :/

commit ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.