别着急,坐和放宽
SELECT ... FOR UPDATE
,性能差;单实例 Redis SETNX,单点故障导致超卖。代数 | 实现 | 容错 | 语言生态 | 备注 |
---|---|---|---|---|
1 | SETNX + EXPIRE | 0 | 所有语言 | 脚本原子性差 |
2 | Lua脚本(SET NX EX) | 0 | 所有语言 | 解决原子性,仍单点 |
3 | Redlock | N/2-1 | Java/Go/PHP | 多数派投票,官方算法 |
问题:
SET key uuid NX PX ttl
。used = now - T1
。成功节点数 ≥ N/2+1
且 used < ttl
,则视为加锁成功;否则向所有节点发送 DEL。ttl - used
,客户端需在过期前释放或续期。数学证明:
模式 | 连接池 | Sentinel | Cluster | Redlock |
---|---|---|---|---|
支持 | ✅ | ✅ | ✅ | ❌(需扩展) |
结论:Hyperf 只做“连接”,不做“协调”。
quorum = N/2 + 1
。ttl > 2 * RTT_max + drift
。config/autoload/redis.php
app/Service/AbstractRedLockService.php
锁续期不是 Redlock 标准的一部分,但生产必备。实现思路:
代码片段:
LockTimeoutException
,由上层重试或降级。RedisDownException
,Prometheus 报警。并发 | 平均 QPS | 锁冲突率 | 99th 延迟 | 备注 |
---|---|---|---|---|
50 | 4.8k | 1.2% | 18 ms | 无节点故障 |
50 | 4.5k | 1.5% | 22 ms | 随机挂 1 节点 |
100 | 9.1k | 2.3% | 31 ms | 随机挂 2 节点 |
结论:在 5 节点 Redlock 下,挂 2 台节点依旧保持 9k+ QPS,延迟增长 <10 ms。
redis_lock_success_total
(Counter)redis_lock_fail_total{reason="timeout|quorum|node_down"}
问题 | 症状 | 解决 |
---|---|---|
集群误用 | Redlock 需要独立实例,Cluster 会 hash-slot 散列 | 单独部署 5 台 |
TTL 过短 | 业务没跑完锁就过期 | 看门狗或提高 ttl |
时钟漂移大 | 两台客户端同时拿到锁 | NTP + 200 ms 阈值 |
Lua 脚本误删 | DEL 不带校验 | 使用官方脚本 |
大 Key | 锁 key 过长导致网络放大 | 采用短哈希 |
附录 A Redlock 数学证明: Redlock revisited – antirez 博文
附录 B 完整源码仓库地址:https://github.com/yourname/hyperf-redlock-demo
Client ──► SETNX lock:sku:123 <uuid> // 成功=1 → 执行业务 → DEL
┌─────────────┐
│ 业务 Pod │ (Hyperf + Coroutine)
└─────┬───────┘
│ Lua 原子脚本
┌─────┴──────────────┐
│ 5 台独立 Redis │ (非 Cluster,主从异步关闭)
│ 10.0.1.11 ~ .15 │
└────────────────────┘
composer require pudongping/hyperf-wise-locksmith:^1.2 -vvv
php bin/hyperf.php vendor:publish --provider="Pudongping\HyperfWiseLocksmith\LockerProvider"
return [
'default' => [...], // 业务缓存
'redLockNode1' => ['host'=>'10.0.1.11','port'=>6379,'auth'=>null,'db'=>0],
'redLockNode2' => ['host'=>'10.0.1.12','port'=>6379,'auth'=>null,'db'=>0],
'redLockNode3' => ['host'=>'10.0.1.13','port'=>6379,'auth'=>null,'db'=>0],
'redLockNode4' => ['host'=>'10.0.1.14','port'=>6379,'auth'=>null,'db'=>0],
'redLockNode5' => ['host'=>'10.0.1.15','port'=>6379,'auth'=>null,'db'=>0],
];
<?php
declare(strict_types=1);
namespace App\Service;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Redis\RedisFactory;
use Pudongping\HyperfWiseLocksmith\Locker;
use Throwable;
abstract class AbstractRedLockService
{
protected Locker $locker;
public function __construct(
RedisFactory $redisFactory,
StdoutLoggerInterface $logger
) {
$nodes = array_map(
fn($pool) => $redisFactory->get($pool),
['redLockNode1','redLockNode2','redLockNode3','redLockNode4','redLockNode5']
);
$this->locker = new Locker($nodes);
$this->locker->setLogger($logger);
}
/**
* 执行业务并自动加/解锁
* @param string $resource 锁资源名
* @param callable $callback 业务闭包
* @param int $ttl 锁有效期(ms)
* @param int $retry 重试次数
* @return mixed
* @throws Throwable
*/
public function withRedLock(string $resource, callable $callback, int $ttl = 10000, int $retry = 3)
{
return $this->locker->redLock(
$resource,
$callback,
$ttl,
$retry
);
}
}
// 在 withRedLock 内部
$renew = function () use ($resource, $uuid) {
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
foreach ($this->nodes as $node) {
$node->eval($script, 1, $resource, $uuid, $ttl);
}
};
$timer = new Timer();
$timer->tick(intval($ttl/3), $renew);
try {
return $callback();
} finally {
$timer->clear();
}
# 挂掉节点 10.0.1.14
docker stop redis-14
wrk -t4 -c100 -d30s http://127.0.0.1:9501/lock/test
时间点 事件
t0 正常
t1 kill 节点 14
t1+200ms Prometheus 报警
t1+500ms 业务锁成功率恢复 100%