PHP后端分布式锁的实现方案比较:从数据库到Redis的实战演进
作为一名在电商领域摸爬滚打多年的PHP开发者,我深刻体会到分布式锁在高并发场景下的重要性。记得去年双十一大促,我们的订单系统因为锁竞争问题出现了严重的超卖现象,那次惨痛经历让我对各种分布式锁方案有了更深入的研究。今天就来分享几种主流的PHP分布式锁实现方案,希望能帮助大家避开我踩过的那些坑。
为什么需要分布式锁?
在单体应用时代,我们使用PHP的flock文件锁或者MySQL的行锁就能解决并发问题。但随着业务发展,系统演进到分布式架构,多个PHP应用实例同时操作共享资源时,传统的单机锁就力不从心了。比如库存扣减、优惠券发放、订单创建等场景,都需要一个跨进程、跨机器的锁机制来保证数据一致性。
基于数据库的分布式锁实现
这是最传统的实现方式,利用数据库的唯一索引或行锁特性来实现。我们先来看基于唯一索引的方案:
class DatabaseLock
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function acquire(string $lockName, int $timeout = 10): bool
{
$identifier = uniqid();
$expireTime = time() + $timeout;
try {
$stmt = $this->pdo->prepare(
"INSERT INTO distributed_locks (lock_name, identifier, expire_time) VALUES (?, ?, ?)"
);
$stmt->execute([$lockName, $identifier, $expireTime]);
return true;
} catch (PDOException $e) {
// 锁已存在,检查是否过期
$this->clearExpiredLock($lockName);
return false;
}
}
public function release(string $lockName, string $identifier): bool
{
$stmt = $this->pdo->prepare(
"DELETE FROM distributed_locks WHERE lock_name = ? AND identifier = ?"
);
return $stmt->execute([$lockName, $identifier]);
}
private function clearExpiredLock(string $lockName): void
{
$stmt = $this->pdo->prepare(
"DELETE FROM distributed_locks WHERE lock_name = ? AND expire_time < ?"
);
$stmt->execute([$lockName, time()]);
}
}
这种方案的优点是实现简单,依赖少。但缺点也很明显:性能较差,数据库压力大,而且在主从架构下可能存在数据延迟问题。我曾经在生产环境使用这种方案,在QPS超过1000时,数据库连接数就撑不住了。
基于Redis的SETNX方案
Redis因为其高性能和丰富的数据结构,成为分布式锁的首选方案。最基本的实现是使用SETNX命令:
class RedisLock
{
private $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
public function acquire(string $lockKey, int $expire = 10): bool
{
$identifier = uniqid();
// 使用SETNX尝试获取锁
$result = $this->redis->setnx($lockKey, $identifier);
if ($result) {
// 设置过期时间,防止死锁
$this->redis->expire($lockKey, $expire);
return true;
}
return false;
}
public function release(string $lockKey, string $identifier): bool
{
// 验证是否是自己持有的锁
if ($this->redis->get($lockKey) === $identifier) {
return $this->redis->del($lockKey) > 0;
}
return false;
}
}
这个方案比数据库方案性能好很多,但在实际使用中我发现了一个严重问题:SETNX和EXPIRE不是原子操作,如果在设置过期时间前进程崩溃,会导致死锁。这个坑让我在凌晨三点被报警叫醒过多次。
基于Redis的SET扩展方案
为了解决原子性问题,Redis 2.6.12之后提供了带选项的SET命令:
class RedisAdvancedLock
{
private $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
public function acquire(string $lockKey, int $expire = 10, int $retry = 3): bool
{
$identifier = uniqid();
for ($i = 0; $i < $retry; $i++) {
// 使用SET NX EX保证原子性
$result = $this->redis->set(
$lockKey,
$identifier,
['NX', 'EX' => $expire]
);
if ($result) {
return true;
}
if ($i < $retry - 1) {
usleep(100000); // 等待100ms重试
}
}
return false;
}
public function release(string $lockKey, string $identifier): bool
{
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
// 使用Lua脚本保证原子性
$result = $this->redis->eval($script, [$lockKey, $identifier], 1);
return $result > 0;
}
}
这个方案解决了原子性问题,但还有一个隐患:如果业务执行时间超过锁的过期时间,会导致锁提前释放,其他进程可能获取到锁。我曾经因为一个复杂的库存计算逻辑执行了15秒,而锁只设置了10秒过期,导致了数据不一致。
Redlock算法实现
在Redis集群环境下,Redis作者提出了Redlock算法。这里我用PHP实现一个简化版本:
class RedLock
{
private $redisInstances;
public function __construct(array $redisConfigs)
{
foreach ($redisConfigs as $config) {
$redis = new Redis();
$redis->connect($config['host'], $config['port']);
$this->redisInstances[] = $redis;
}
}
public function acquire(string $lockKey, int $ttl = 10000): bool
{
$identifier = uniqid();
$startTime = microtime(true) * 1000;
$quorum = (int)(count($this->redisInstances) / 2) + 1;
$lockedCount = 0;
foreach ($this->redisInstances as $redis) {
try {
if ($redis->set($lockKey, $identifier, ['NX', 'PX' => $ttl])) {
$lockedCount++;
}
} catch (Exception $e) {
// 忽略单个节点故障
continue;
}
if ($lockedCount >= $quorum) {
return true;
}
}
// 获取锁失败,释放已获取的锁
$this->release($lockKey, $identifier);
return false;
}
}
Redlock提供了更好的可靠性,但实现复杂,性能开销较大。在实际项目中,我们需要根据业务场景权衡选择。
实战经验与选型建议
经过多个项目的实践,我总结了一些选型建议:
1. 简单业务场景:使用Redis的SET NX EX方案,配合Lua脚本释放锁,这能满足90%的需求。
2. 高可用场景:如果对可靠性要求极高,可以考虑Redlock,但要接受性能损失。
3. 数据库方案:只有在没有Redis且并发量不大的情况下使用。
这里分享一个我在电商项目中使用的生产级代码:
class DistributedLockManager
{
private $redis;
private $locks = [];
public function withLock(string $lockKey, callable $businessLogic, int $timeout = 10)
{
$lock = new RedisAdvancedLock($this->redis);
$identifier = uniqid();
if (!$lock->acquire($lockKey, $timeout)) {
throw new RuntimeException("获取分布式锁失败: " . $lockKey);
}
try {
$this->locks[$lockKey] = $identifier;
return $businessLogic();
} finally {
$lock->release($lockKey, $identifier);
unset($this->locks[$lockKey]);
}
}
}
// 使用示例
$lockManager = new DistributedLockManager($redis);
$result = $lockManager->withLock('order_create_123', function() {
// 业务逻辑
return createOrder();
});
这种封装方式确保了锁的正确释放,避免了因为异常导致的死锁问题。
总结
分布式锁没有银弹,每种方案都有其适用场景。在选择时,我们需要考虑业务的数据一致性要求、性能要求、系统复杂度等因素。从我个人的经验来看,Redis的SET NX EX方案+Lua脚本是最实用的选择,它在性能、可靠性和实现复杂度之间取得了很好的平衡。
最后提醒大家:分布式锁只是解决并发问题的一种手段,在设计系统时,我们还可以考虑使用乐观锁、队列、状态机等其他方案来避免锁竞争。记住,最好的锁就是不用锁!

评论(0)