Webアプリケーションへの同時アクセス対策メモ
XAMPPの Apache Bench で同時アクセスを再現するメモ。テストした環境はWindows7。Apache Bench の使い方は以下を参考に。
XAMPPをCドライブ直下にインストールした場合、以下の手順で Apache Bench を使える。
> cd C:\xampp\apache\bin
> ab -n 15 -c 15 http://localhost/~test/ab/
「-n 15 -c 15」で「15人が同時にアクセスする(合計15回アクセス)」状況がテストできる。(「-n 15 -c 1」なら「1人が合計15回アクセスする」状況になるので、同時アクセスにはならない。)
以下で同時アクセスのテストとその対策を行う。
ファイルを使った例
<?php
/*
* No.2をカウントアップ(最大値100)
*/
if ($fp = fopen('logs/count.log', 'r+')) {
$data = '';
while ($line = fgets($fp)) {
list($id, $count) = explode("\t", rtrim($line));
if ($id == 2) {
if ($count < 100) {
$data .= "$id\t" . ++$count . "\n";
} else {
exit('Limit');
}
} else {
$data .= $line;
}
}
rewind($fp);
fwrite($fp, $data);
ftruncate($fp, ftell($fp));
fclose($fp);
} else {
exit('Error');
}
exit('Complete');
logs/count.log
の内容は以下のとおり。
1 10
2 20
3 30
4 40
5 50
これで上のPHPプログラムにアクセスすると、2の行が1カウントアップされる。何度もリロードすると、その回数だけカウントアップされる。
> ab -n 30 -c 30 http://localhost/~test/ab/count.php
30人の同時アクセスを発生させてみると、正しくカウントできないのが確認できる。
<?php
/*
* No.2をカウントアップ(最大値100)
*/
if ($fp = fopen('logs/count.log', 'r+')) {
flock($fp, LOCK_EX); //ファイルロック
$data = '';
while ($line = fgets($fp)) {
list($id, $count) = explode("\t", rtrim($line));
if ($id == 2) {
if ($count < 100) {
$data .= "$id\t" . ++$count . "\n";
} else {
exit('Limit');
}
} else {
$data .= $line;
}
}
rewind($fp);
fwrite($fp, $data);
ftruncate($fp, ftell($fp));
fclose($fp);
} else {
exit('Error');
}
exit('Complete');
ファイルロックを行えば、正しくカウントされるようになる。
データベースを使った例
<?php
/*
* No.2をカウントアップ(最大値100)
*/
try {
$pdo = new PDO('mysql:dbname=test;host=localhost', 'root', '1234', array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
$pdo->query('SET NAMES utf8');
$stmt = $pdo->prepare('SELECT count FROM count_test WHERE id = :id');
$stmt->bindValue(':id', 2, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data['count'] < 100) {
$stmt = $pdo->prepare('UPDATE count_test SET count = count + 1 WHERE id = :id');
$stmt->bindValue(':id', 2, PDO::PARAM_INT);
$stmt->execute();
}
} catch (PDOException $e) {
exit($e->getMessage());
}
exit('Complete');
count_test
の定義は以下のとおり。
CREATE TABLE count_test(
id INT UNSIGNED NOT NULL,
count INT UNSIGNED,
PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
これで上のPHPプログラムにアクセスすると、2の行が1カウントアップされる。何度もリロードすると、その回数だけカウントアップされる。
ab -n 200 -c 200 http://localhost/~test/ab/count.php
200人の同時アクセスを発生させてみると、正しくカウントできないのが確認できる。
<?php
/*
* No.2をカウントアップ(最大値100)
*/
try {
$pdo = new PDO('mysql:dbname=test;host=localhost', 'root', '1234', array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
$pdo->query('SET NAMES utf8');
$pdo->beginTransaction(); //トランザクション開始
$stmt = $pdo->prepare('SELECT count FROM count_test WHERE id = :id FOR UPDATE'); //行ロック
$stmt->bindValue(':id', 2, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data['count'] < 100) {
$stmt = $pdo->prepare('UPDATE count_test SET count = count + 1 WHERE id = :id');
$stmt->bindValue(':id', 2, PDO::PARAM_INT);
$stmt->execute();
}
$pdo->commit(); //コミット
} catch (PDOException $e) {
$pdo->rollBack(); //ロールバック
exit($e->getMessage());
}
exit('Complete');
トランザクションと行ロックを行えば、正しくカウントされるようになる。