Estritamente falando, seu teste de exclusividade não garantirá exclusividade sob uma carga simultânea. O problema é que você verifica a exclusividade antes (e separadamente) do local em que insere uma linha para "reivindicar" sua senha recém-gerada. Outro processo poderia estar fazendo a mesma coisa, ao mesmo tempo. Aqui está como isso vai...
Dois processos geram exatamente a mesma senha. Cada um deles começa verificando a singularidade. Como nenhum processo (ainda) inseriu uma linha na tabela, ambos os processos não encontrarão nenhuma senha correspondente no banco de dados e, portanto, ambos os processos assumirão que o código é exclusivo. Agora, à medida que os processos continuam seu trabalho, eventualmente eles ambos insira uma linha nos
files
table usando o código gerado -- e assim você obtém uma duplicata. Para contornar isso, você deve realizar a verificação e fazer a inserção em uma única operação "atômica". Segue uma explicação dessa abordagem:
Se você deseja que a senha seja exclusiva, você deve definir a coluna em seu banco de dados como
UNIQUE
. Isso garantirá exclusividade (mesmo que seu código php não o faça) recusando-se a inserir uma linha que causaria uma senha duplicada. CREATE TABLE files (
id int(10) unsigned NOT NULL auto_increment PRIMARY KEY,
filename varchar(255) NOT NULL,
passcode varchar(64) NOT NULL UNIQUE,
)
Agora, use o
SHA1()
do mysql e NOW()
para gerar sua senha como parte de a instrução de inserção. Combine isso com INSERT IGNORE ...
(documentos
) e faça um loop até que uma linha seja inserida com sucesso: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.
}
Observação: O exemplo acima foi editado para dar conta das excelentes observações postadas por @martinstoeckli na seção de comentários. As seguintes alterações foram feitas:
- alterado
mysql_num_rows()
(documentos ) paramysql_affected_rows()
(documentos ) -- num_rows não se aplica a inserções. Também removeu o argumento paramysql_affected_rows()
, pois esta função opera no nível da conexão, não no nível do resultado (e em qualquer caso, o resultado de uma inserção é booleano, não um número de recurso). - adicionada verificação de erros na condição de loop e adicionado um teste de erro/sucesso após a saída do loop. O tratamento de erros é importante, pois sem ele, erros de banco de dados (como conexões perdidas ou problemas de permissão) farão com que o loop gire para sempre. A abordagem mostrada acima (usando
IGNORE
emysql_affected_rows()
e testando$res
separadamente para erros) nos permite distinguir esses "erros de banco de dados reais" da violação de restrição exclusiva (que é uma condição de não erro completamente válida nesta seção da lógica).
Se você precisar obter a senha depois de gerada, basta selecionar o registro novamente:
$res = mysql_query("SELECT * FROM files WHERE id=LAST_INSERT_ID()");
$row = mysql_fetch_assoc($res);
$passcode = $row['passcode'];
Editar :alterado o exemplo acima para usar a função mysql
LAST_INSERT_ID()
, em vez da função do PHP. Essa é uma maneira mais eficiente de realizar a mesma coisa, e o código resultante é mais limpo, mais claro e menos confuso.