本文目录导读:

在 PHP 项目中,锁的粒度控制是提升并发性能的关键。粒度越细(锁定代码范围越小),并发能力越强,但实现复杂度越高。 以下是具体的细化策略和实践方法:
核心原则:只锁"临界区"
// ❌ 错误:锁定整个方法
function processOrder($orderId) {
$lock = new Lock('order_'.$orderId);
$lock->acquire();
// 大量非共享操作
$this->validateUser();
$this->checkInventory();
$this->calculateDiscount();
// 真正需要锁的操作
$this->updateOrderStatus();
$this->deductStock();
$lock->release();
}
// ✅ 正确:只锁定修改共享资源的部分
function processOrder($orderId) {
// 非共享操作放在锁外
$user = $this->validateUser();
$inventory = $this->checkInventory($orderId);
$discount = $this->calculateDiscount($user);
// 仅锁定更新操作
$lock = new Lock('order_'.$orderId);
$lock->acquire();
try {
$this->updateOrderStatus($orderId, 'processing');
$this->deductStock($orderId, $inventory);
} finally {
$lock->release();
}
}
关键路径分离法
将单个大锁拆分为多个小锁,实现"读写分离":
// 使用读写锁(需要底层支持,如 Redis RedLock 扩展)
class OrderInventoryManager {
private $readLocks = [];
private $writeLocks = [];
public function checkAndReduceStock($productId, $quantity) {
// 读阶段:使用读锁(可共享)
$readLock = $this->getReadLock('stock_'.$productId);
$readLock->acquire();
$currentStock = $this->redis->get('stock:'.$productId);
$readLock->release();
// 业务逻辑判断(无锁)
if ($currentStock < $quantity) {
throw new \Exception('库存不足');
}
// 写阶段:使用写锁(互斥)
$writeLock = $this->getWriteLock('stock_'.$productId);
$writeLock->acquire();
try {
// 再次检查(防止并发修改)
$currentStock = $this->redis->get('stock:'.$productId);
if ($currentStock >= $quantity) {
$this->redis->decrBy('stock:'.$productId, $quantity);
return true;
}
return false;
} finally {
$writeLock->release();
}
}
}
资源级锁定(最细粒度)
为每个独立资源创建独立锁键:
class FineGrainedLockManager {
public function updateUserBalance($userId, $amount) {
// 用户级别锁:不同用户互不影响
$lockKey = 'lock:user:'.$userId;
$lock = new RedisLock($lockKey, 3); // 3秒超时
$lock->acquire();
try {
$balance = $this->getBalance($userId);
$this->setBalance($userId, $balance + $amount);
$this->logTransaction($userId, $amount);
} finally {
$lock->release();
}
}
public function updateOrderItemQuantity($orderId, $itemId, $newQty) {
// 订单+商品级别锁(最细)
$lockKey = 'lock:order:'.$orderId.':item:'.$itemId;
$lock = new RedisLock($lockKey, 5);
$lock->acquire();
try {
// 仅修改这个订单项
$this->updateItem($orderId, $itemId, $newQty);
} finally {
$lock->release();
}
}
}
乐观锁(无锁/极细粒度)
使用版本号或条件更新,避免显式加锁:
// 使用 MySQL 乐观锁(推荐)
class OptimisticLockExample {
public function updateWithVersion($orderId) {
// 1. 读取当前数据及版本号
$order = $this->db->fetchOne(
"SELECT * FROM orders WHERE id = ?", [$orderId]
);
$currentVersion = $order['version'];
// 2. 业务处理(无锁)
$newStatus = $this->calculateNewStatus($order);
// 3. 使用版本号更新(原子操作)
$affected = $this->db->execute(
"UPDATE orders SET status = ?, version = version + 1
WHERE id = ? AND version = ?",
[$newStatus, $orderId, $currentVersion]
);
if ($affected === 0) {
throw new \Exception('并发冲突,请重试');
}
}
}
// 使用 Redis 原子操作(更细粒度)
class RedisAtomicExample {
public function decrementStock($productId, $quantity) {
$stockKey = 'stock:'.$productId;
// 原子检查并减少(一条命令完成)
$luaScript = <<<'LUA'
local stock = redis.call('GET', KEYS[1])
if stock and tonumber(stock) >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
end
return -1
LUA;
$result = $this->redis->eval($luaScript, [$stockKey], [$quantity]);
return $result >= 0;
}
}
分片锁(分散热点)
针对高频访问资源,使用哈希分片:
class ShardedLockManager {
private $shardCount = 128; // 分片数
public function lockUser($userId) {
// 计算分片编号
$shardId = crc32("user_{$userId}") % $this->shardCount;
$lockKey = "lock:shard:{$shardId}";
$lock = new RedisLock($lockKey, 2);
$lock->acquire();
return $lock;
}
}
// 使用示例
$shardManager = new ShardedLockManager();
foreach ($userIds as $userId) {
$lock = $shardManager->lockUser($userId);
// 不同用户大概率映射到不同分片,实现细粒度并发
}
锁超时与降级
为锁设置合理超时,避免长时间占用:
class SmartLock {
private $lockTimeout = 2; // 秒
private $retryDelay = 100; // 毫秒
private $maxRetries = 3;
public function acquire($key, $callback) {
// 尝试获取锁
$lock = new RedisLock($key, $this->lockTimeout);
if (!$lock->acquire()) {
// 锁超时降级:执行降级逻辑
return $this->fallbackExecute($callback);
}
try {
// 设置更短的内部超时
$start = microtime(true);
$result = $callback();
$elapsed = microtime(true) - $start;
if ($elapsed > $this->lockTimeout * 0.8) {
// 超过锁有效期的80%,记录警告
$this->logger->warning("Lock too long: ".$key);
}
return $result;
} finally {
$lock->release();
}
}
private function fallbackExecute($callback) {
// 降级策略:无锁执行但标记脏数据
$result = $callback();
$this->markAsDirty($result);
return $result;
}
}
粒度选择决策树
需要锁保护吗?
├── 是 → 修改共享资源吗?
│ ├── 是 → 可以接受失败重试吗?
│ │ ├── 是 → 使用乐观锁(版本号/CAS)
│ │ └── 否 → 需要原子性吗?
│ │ ├── 是 → Redis Lua 脚本/事务
│ │ └── 否 → 悲观锁(最小化临界区)
│ └── 否 → 读多写少?
│ ├── 是 → 读写锁(共享锁)
│ └── 否 → 细粒度资源锁
└── 否 → 数据一致性要求高?
├── 是 → 分布式事务(补偿锁)
└── 否 → 无锁设计(最终一致性)
实际建议
- 先无锁,再加锁:多数 PHP 应用(尤其是页面渲染)不需要锁
- 优先使用乐观锁:数据库行级锁+版本号,实现最简单、粒度最细
- Redis 分布式锁+短超时:2-3秒最合适
- 做好性能监控:如果锁争用率 > 5%,说明粒度可能过粗
- 考虑业务边界:用户级锁"通常比"全局锁"好100倍
最佳实践示例:电商下单场景,只锁定"库存扣减"和"订单状态更新"两个原子操作,而用户验证、价格计算等都放在锁外部。