Ściśle mówiąc, test na unikalność nie zagwarantuje unikalności przy jednoczesnym obciążeniu. Problem polega na tym, że sprawdzasz unikalność przed (i oddzielnie od) miejscem, w którym wstawiasz wiersz, aby „odebrać” nowo wygenerowany kod dostępu. Innym procesem może być robienie tego samego w tym samym czasie. Oto jak to działa...
Dwa procesy generują dokładnie ten sam kod dostępu. Każdy z nich zaczyna się od sprawdzenia wyjątkowości. Ponieważ żaden proces (jeszcze) nie wstawił wiersza do tabeli, oba procesy nie znajdą pasującego hasła w bazie danych, więc oba procesy przyjmą, że kod jest unikalny. Teraz, gdy każdy z procesów kontynuuje swoją pracę, w końcu będą obie wstaw wiersz do files
tabela za pomocą wygenerowanego kodu - a tym samym otrzymasz duplikat.
Aby obejść ten problem, musisz przeprowadzić kontrolę i wstawić w jednej „atomowej” operacji. Poniżej znajduje się wyjaśnienie tego podejścia:
Jeśli chcesz, aby hasło było unikalne, powinieneś zdefiniować kolumnę w swojej bazie danych jako UNIQUE
. Zapewni to unikalność (nawet jeśli Twój kod php tego nie robi) poprzez odmowę wstawienia wiersza, który spowodowałby zduplikowanie hasła.
CREATE TABLE files (
id int(10) unsigned NOT NULL auto_increment PRIMARY KEY,
filename varchar(255) NOT NULL,
passcode varchar(64) NOT NULL UNIQUE,
)
Teraz użyj SHA1()
mysql i NOW()
aby wygenerować kod dostępu jako część instrukcja wstawiania. Połącz to z INSERT IGNORE ...
(dokumentacja
) i wykonaj pętlę, aż wiersz zostanie pomyślnie wstawiony:
do {
$query = "INSERT IGNORE INTO files
(filename, passcode) values ('whatever', SHA1(NOW()))";
$res = mysql_query($query);
} while( $res && (0 == mysql_affected_rows()) )
if( !$res ) {
// an error occurred (eg. lost connection, insufficient permissions on table, etc)
// no passcode was generated. handle the error, and either abort or retry.
} else {
// success, unique code was generated and inserted into db.
// you can now do a select to retrieve the generated code (described below)
// or you can proceed with the rest of your program logic.
}
Uwaga: Powyższy przykład został zredagowany w celu uwzględnienia doskonałych obserwacji zamieszczonych przez @martinstoeckli w sekcji komentarzy. Wprowadzono następujące zmiany:
- zmieniono
mysql_num_rows()
(dokumenty ) domysql_affected_rows()
(dokumenty ) — liczba_wierszy nie dotyczy wstawek. Usunięto również argument domysql_affected_rows()
, ponieważ ta funkcja działa na poziomie połączenia, a nie na poziomie wyniku (w każdym razie wynikiem wstawienia jest wartość logiczna, a nie numer zasobu). - dodano sprawdzanie błędów w warunku pętli i dodano test na błąd/powodzenie po zakończeniu pętli. Obsługa błędów jest ważna, ponieważ bez niej błędy bazy danych (takie jak utracone połączenia lub problemy z uprawnieniami) spowodują, że pętla będzie się obracać w nieskończoność. Podejście pokazane powyżej (używając
IGNORE
imysql_affected_rows()
i testowanie$res
oddzielnie dla błędów) pozwala nam odróżnić te „prawdziwe błędy bazy danych” od unikatowego naruszenia ograniczenia (co jest całkowicie prawidłowym warunkiem braku błędu w tej sekcji logiki).
Jeśli chcesz uzyskać kod dostępu po jego wygenerowaniu, po prostu wybierz rekord ponownie:
$res = mysql_query("SELECT * FROM files WHERE id=LAST_INSERT_ID()");
$row = mysql_fetch_assoc($res);
$passcode = $row['passcode'];
Edytuj :zmieniono powyższy przykład, aby użyć funkcji mysql LAST_INSERT_ID()
, a nie funkcję PHP. Jest to wydajniejszy sposób na osiągnięcie tego samego, a wynikowy kod jest czystszy, jaśniejszy i mniej zagracony.