别着急,坐和放宽
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%