本文目录导读:

- 数据库层面:乐观锁(推荐)
- 使用 Redis 原子操作替代锁
- 数据库事务隔离级别调整
- 表设计层面:降级锁粒度
- 使用消息队列解耦
- 读写分离
- 行锁 vs 表锁的选择
- 代码设计:避免长事务
- 使用 PHP 扩展级别的原子操作
- 查询设计:利用条件更新避免竞态
- 最佳实践选择
在 PHP 中避免加锁(或减少锁竞争),通常是为了提升性能和避免死锁,以下是几种常见的策略,按场景分类:
数据库层面:乐观锁(推荐)
乐观锁的核心是:不加锁,而是在更新时检查数据是否被修改过。
实现方式(使用版本号或时间戳):
// 查询时带上版本号
$user = $db->query("SELECT * FROM users WHERE id = 1");
$version = $user['version'];
// 更新时带上 WHERE 版本号条件
$result = $db->exec(
"UPDATE users SET balance = 100, version = version + 1
WHERE id = 1 AND version = $version"
);
// 检查影响行数,如果为 0 说明数据已被修改
if ($result === 0) {
// 冲突处理:重试或返回错误
throw new Exception('数据已被他人修改');
}
优点:
- 不会持有数据库锁,读取永远不被阻塞。
- 适合读多写少的场景。
使用 Redis 原子操作替代锁
很多场景下,可以用 Redis 的原子指令 来替代显式的锁。
场景:计数器、库存扣减
// 错误方式:先读后写(需要锁)
$count = $redis->get('stock');
$redis->set('stock', $count - 1);
// 正确方式:原子递减(不需要锁)
$redis->decr('stock'); // 或 decrby('stock', 1)
// 检查是否超卖
if ($redis->get('stock') < 0) {
// 回滚
$redis->incr('stock');
throw new Exception('库存不足');
}
场景:分布式锁替代(使用 Redis SETNX)
// 获取分布式锁
$lock = $redis->set('lock:key', 'lock_value', ['NX', 'EX' => 10]);
if ($lock) {
try {
// 业务逻辑
} finally {
$redis->del('lock:key');
}
}
数据库事务隔离级别调整
如果你必须使用数据库锁,可以通过调整隔离级别来减少锁的粒度:
| 隔离级别 | 锁的粒度 | 适用场景 |
|---|---|---|
| READ UNCOMMITTED | 无锁(脏读) | 数据准确性要求不高(如日志) |
| READ COMMITTED | 行级锁 | 大多数业务场景 |
| REPEATABLE READ | 共享锁/间隙锁(MySQL 默认) | 金融、订单等 |
| SERIALIZABLE | 表级锁 | 极少使用,性能极差 |
// 在 MySQL 中设置隔离级别
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
表设计层面:降级锁粒度
使用冗余字段减少锁冲突
博客的点赞数,不要每次点赞都更新主表的 likes 字段,而是:
- 写入点赞记录表(无锁或行级锁)。
- 定期(如每隔 5 分钟)聚合统计并更新主表。
-- 点赞记录表(每次点赞都 INSERT,没有 UPDATE) INSERT INTO post_likes (post_id, user_id) VALUES (?, ?); -- 定时任务聚合 UPDATE posts p SET likes = (SELECT COUNT(*) FROM post_likes WHERE post_id = p.id) WHERE p.id = ?;
使用消息队列解耦
如果某个操作是高频写入且需要串行化,可以将操作放入队列,串行消费:
// 将写入操作投递到队列
$queue->push([
'type' => 'increment_balance',
'user_id' => 1,
'amount' => 10
]);
// 消费者串行处理(天然无锁)
while ($task = $queue->pop()) {
// 单线程处理,不需要加锁
$db->exec("UPDATE users SET balance = balance + ? WHERE id = ?",
[$task['amount'], $task['user_id']]);
}
读写分离
将读操作和写操作分发到不同的数据库实例:
- 主库:负责写(可以加锁,但锁竞争少)。
- 从库:负责读(完全无锁)。
// 使用 PHP 的数据库读写分离扩展(如 ParagonIE 的 PDO)
$readPdo = new PDO('mysql:host=slave.example.com', ...);
$writePdo = new PDO('mysql:host=master.example.com', ...);
行锁 vs 表锁的选择
在 MySQL 中,确保 使用索引 来触发行锁,避免全表扫描导致表锁:
-- 使用索引,走行锁 UPDATE users SET balance = 100 WHERE id = 1; -- id 有索引 -- 没有索引,可能升级为表锁 UPDATE users SET balance = 100 WHERE email = 'xxx'; -- email 无索引
代码设计:避免长事务
持锁时间越长,锁冲突概率越大,尽量减少事务中的操作:
// 错误:事务中做了大量远程调用
$pdo->beginTransaction();
$pdo->exec("UPDATE ...");
callExternalApi(); // 耗时操作,一直持锁
$pdo->exec("UPDATE ...");
$pdo->commit();
// 正确:仅将必要操作放在事务内
$data = callExternalApi();
$pdo->beginTransaction();
$pdo->exec("UPDATE ... WHERE ...");
$pdo->commit();
使用 PHP 扩展级别的原子操作
如果只是单进程(如 CLI 脚本单进程消费队列),可以用 PHP 扩展:
// Swoole 的原子计数(单进程内原子操作) $atomic = new Swoole\Atomic(100); $atomic->sub(1); echo $atomic->get(); // 99
查询设计:利用条件更新避免竞态
// 场景:扣减用户余额,且不允许负数
$result = $pdo->exec(
"UPDATE users
SET balance = balance - 100
WHERE id = ? AND balance >= 100" // 条件防止超额扣减,无需锁
);
最佳实践选择
| 场景 | 推荐方案 |
|---|---|
| 扣库存/计数 | Redis 原子操作 |
| 更新用户资料 | 乐观锁(版本号) |
| 订单并发扣款 | 条件 UPDATE + 事务 |
| 跨服务共享数据 | Redis 分布式锁 |
| 频繁读不频繁写 | 读写分离 |
| 高并发写入热点 | 消息队列串行化 |
核心思想:能不用锁就不用锁;必须用锁时,让锁的持有时间尽可能短、锁的粒度尽可能小。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播