说到PHP事务处理,这东西简直就是程序员的“生死簿”。你要是处理得好,数据一致性稳稳的;处理得不好,轻则数据错乱,重则系统瘫痪。今天就来聊聊我在项目中用PHP处理事务的那些事,顺便把坑都给你踩一遍。
你得知道什么是事务。简单来说,事务就是一组SQL操作,要么全部成功,要么全部失败。想象一下你在银行转账,A给B转100块,A账户减100,B账户加100。这两步操作必须同时成功或失败,不然就会出现A钱没了,B钱没到账的尴尬局面。
在PHP中,事务处理通常是通过PDO来实现的。PDO是PHP Data Objects的缩写,是一个数据库访问抽象层,支持多种数据库。先来段代码热热身:
if ($pdo->beginTransaction()) {
try {
$stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$stmt1->execute();
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
die("Transaction failed: " . $e->getMessage());
}
}
这段代码就是典型的事务处理流程。beginTransaction开启事务,commit提交事务,rollBack回滚事务。如果在这个过程中有任何一条SQL语句执行失败,catch块会捕获异常并回滚事务,保证数据一致性。
看起来很简单对?但别高兴得太早,坑多着。
坑1:事务嵌套
有些数据库支持事务嵌套,比如MySQL。事务嵌套的意思是在一个事务中还可以开启另一个事务。听起来挺高级的,但如果你不小心用错了,后果很严重。
try {
$stmt2->execute();
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
die("Inner transaction failed: " . $e->getMessage());
}
}
}
}
这段代码在MySQL中执行,内层事务会和外层事务一起提交或回滚。但如果你切换到PostgreSQL,内层事务的提交或回滚不会影响外层事务,这就会导致数据不一致。所以,事务嵌套一定要谨慎使用,最好避免。
坑2:自动提交模式
PDO默认是自动提交模式,也就是说每一条SQL语句都会立即提交到数据库。如果你不小心在事务中执行了自动提交的SQL语句,那事务就失效了。
// 这里不小心执行了自动提交的SQL
$pdo->exec("INSERT INTO log (message) VALUES ('Transaction started')");
}
}
在这段代码中,$pdo->exec("INSERT INTO log (message) VALUES ('Transaction started')");这条语句会立即提交到数据库,即使事务后面失败了,这条日志记录依然存在。
为了避免这个问题,你可以在事务开始前关闭自动提交模式:
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
}
}
坑3:死锁
死锁是事务处理中最头疼的问题之一。简单来说,死锁就是两个或多个事务互相等待对方释放资源,导致谁都动不了。
举个例子,有两个事务A和B:
事务A先锁定了账户1,然后尝试锁定账户2。
如果这两个操作同时发生,事务A在等事务B释放账户2,事务B在等事务A释放账户1,结果就卡住了。
解决死锁的办法有很多,比如调整SQL执行顺序、减少事务执行时间、增加重试机制等。
function retryTransaction($pdo, $retries = 3) {
for ($i = 0; $i < $retries; $i++) {
try {
if ($pdo->beginTransaction()) {
$stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$stmt1->execute();
return true;
} catch (Exception $e) {
$pdo->rollBack();
if (strpos($e->getMessage(), 'Deadlock found') !== false && $i < $retries - 1) {
// 如果是死锁,等待一会儿再重试
usleep(100000);
} else {
throw $e;
}
return false;
}
在这个例子中,如果发生死锁,程序会等待0.1秒再重试,最多重试3次。
坑4:事务隔离级别
事务隔离级别决定了事务之间的可见性。MySQL有四种隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
默认情况下,MySQL的隔离级别是可重复读,但这可能会导致幻读问题。所谓幻读,就是一个事务中两次查询的结果集不一致。
举个例子:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
$stmt1 = $pdo->query("SELECT * FROM accounts WHERE balance > 100");
$result1 = $stmt1->fetchAll(PDO::FETCH_ASSOC);
// 另一个事务在这时插入了一条新记录
$pdo->exec("INSERT INTO accounts (id, balance) VALUES (3, 200)");
print_r($result1);
}
}
在这个例子中,result1和result2的结果可能会不一致,因为另一个事务在中间插入了一条新记录。为了避免幻读,你可以将隔离级别提升到串行化:
$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
但要注意的是,串行化会严重影响并发性能,所以要根据实际情况权衡。
坑5:长事务
长事务是指执行时间较长的事务。长事务会占用数据库资源,增加死锁风险,甚至导致数据库性能下降。
解决长事务的办法有很多,比如将大事务拆分成多个小事务、减少事务中的业务逻辑等。
php
function updateAccount($pdo, $fromId, $toId, $amount) {
if ($pdo->beginTransaction()) {
$stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?");
$stmt1->execute([$amount, $fromId]);
$pdo->commit();