Положим имеется 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» перед первым запросом. Однако это, насколько я понимаю, плохо для производительности, т.е. неэффективное решение.
Предлагайте варианты решения данной проблемы, желательно с примерами и кодом.