第四章:分布式锁——基于Redis的分布式锁实现与PHP代码实战
说到分布式锁,我脑子里第一个蹦出来的场景就是:几年前我们团队做秒杀系统,库存只有100件,结果一瞬间涌进来10万请求。那时候还没用分布式锁,数据库直接被干趴下了。嗯,从那以后,我对分布式锁的敬畏心就上来了。
说白了,分布式锁就是解决一个核心问题:在分布式环境下,如何保证同一时间只有一个进程能访问共享资源。你想想看,单机环境下我们用文件锁、用synchronized就能搞定。但到了多台服务器,这些招数全都不好使了。
4.1 为什么需要分布式锁
我习惯用一个例子来解释:假设你有一个计数器,存在Redis里。两个PHP进程同时执行:
$redis->incr('counter');
这行代码在单机下没问题,因为incr是原子操作。但如果你要做的是:
$count = $redis->get('counter');
$count++;
$redis->set('counter', $count);
这就出事了。两个进程同时读到count=5,都加1,最后写回去还是6。本该是7的。这就是典型的竞态条件。
注意: 我曾经在项目中见过有人用SETNX加锁,但忘了设置过期时间。结果进程挂了,锁永远不释放,整个系统就卡死了。这种坑,踩一次就记住了。
4.2 Redis分布式锁的核心原理
Redis实现分布式锁,核心就三个操作:
- 加锁:用SETNX或SET命令,设置一个唯一的key
- 解锁:删除这个key,但要确保是锁的持有者才能删
- 超时:设置过期时间,防止死锁
我个人习惯用SET命令的扩展参数,一步到位:
SET lock_key unique_value NX PX 30000
这行命令的意思是:如果key不存在就设置,过期时间30秒。NX保证原子性,PX设置过期时间。一步搞定,不用分两步走。
4.3 一个完整的PHP实现
来看一个我常用的实现。这个版本我用了好几年,线上跑得挺稳:
<?php
class RedisDistributedLock
{
private $redis;
private $lockKey;
private $lockValue;
private $expireTime = 30000; // 毫秒
public function __construct($redis, $lockKey)
{
$this->redis = $redis;
$this->lockKey = $lockKey;
$this->lockValue = uniqid('', true);
}
public function acquire($timeout = 10000)
{
$start = microtime(true) * 1000;
while (true) {
$result = $this->redis->set(
$this->lockKey,
$this->lockValue,
['NX', 'PX' => $this->expireTime]
);
if ($result) {
return true;
}
// 超时检查
if ((microtime(true) * 1000 - $start) >= $timeout) {
return false;
}
usleep(1000); // 1ms后重试
}
}
public function release()
{
// 用Lua脚本保证原子性
$script = <<<'LUA'
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
}
}
小技巧: 解锁时用Lua脚本判断value是否匹配,这个习惯是我从一次线上事故中学到的。不加这个判断,A进程可能把B进程的锁给解了,那画面太美不敢看。
4.4 使用示例
实际用起来很简单。我习惯这样写:
$lock = new RedisDistributedLock($redis, 'order:123:lock');
if ($lock->acquire(5000)) {
try {
// 处理订单
processOrder(123);
} finally {
$lock->release();
}
} else {
echo "获取锁失败,请稍后重试";
}
这里有个细节:finally块里释放锁。不管业务代码抛不抛异常,锁都得释放。这个习惯帮我避免过好几次线上故障。
4.5 避坑指南
做分布式锁,坑不少。我一个个说:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 死锁 | 进程崩溃,锁没释放 | 设置过期时间,用PX参数 |
| 锁误删 | A进程删了B进程的锁 | 解锁时校验value,用Lua脚本 |
| 锁过期 | 业务执行时间超过锁时间 | 启动守护线程自动续期 |
| Redis宕机 | 主节点挂了,锁丢失 | 用RedLock算法,多节点加锁 |
我曾经遇到过最头疼的问题就是锁过期。业务处理了40秒,锁30秒就过期了。别的进程拿到锁,两个进程同时处理同一笔订单。后来我加了个自动续期的机制,每10秒检查一次,如果业务还在跑就续期。
4.6 性能优化建议
说几个我实际用下来的经验:
- 锁粒度要小:别锁整个用户,锁具体某个订单就行
- 超时时间要合理:太短容易误判,太长影响并发
- 重试间隔别太短:1ms差不多了,太短会打满CPU
- 考虑用RedLock:对一致性要求高的场景,用多个Redis节点
核心要点:
- 加锁用SET NX PX,一步到位
- 解锁用Lua脚本,校验value
- 业务时间超过锁时间要续期
- finally块里释放锁,别偷懒
嗯,分布式锁就这些了。说白了就是一套规则:谁先拿到锁谁干活,干完活记得还锁。但细节决定成败,每个环节都得想清楚。下次遇到高并发场景,试试这个方案,应该能帮你省不少心。