Files
cursor/plans/symfony-lock-distributed-lock-5511b223.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

92 lines
4.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 5511b223-7953-4aa5-bd6e-94f0d6c56b21 -->
---
todos:
- id: "dep"
content: "在 php82 容器执行 composer require symfony/lock:7.4.9"
status: pending
- id: "factory"
content: "新增 app/support/OtaLockFactory.php 封装 symfony LockFactory + RedisStore用 support\\Redis::connection()->client()"
status: pending
- id: "refactor"
content: "重构 OtaCheckLogic 锁方法acquire(false) 重试+SERVICE_BUSYrelease(),删除手写 SET/Lua 与 support\\Redis 依赖"
status: pending
- id: "verify"
content: "运行 phpunit + php -l + 完成门禁,确认全绿"
status: pending
isProject: false
---
# 用 symfony/lock 7.4.9 替换 OTA 分布式锁
## 目标与范围
- 仅替换 [app/logic/OtaCheckLogic.php](app/logic/OtaCheckLogic.php) 中的锁实现(`withDeviceFirmwareLock` / `acquireLock` / `releaseLock`)。
- 业务流程(先查后建/复用、§7.5 幂等)、错误码 `SERVICE_BUSY`、TTL/重试参数语义保持不变。
- 单测/集成测试结构不变(仍依赖 docker `redis`,跳过守卫不变)。
## 为什么可行
- `php82` = 8.2.24,满足 symfony/lock 7.4PHP ≥ 8.2)。
- symfony `RedisStore` 接受原生 `\Redis`,可由 `support\Redis::connection()->client()` 取得(见 `vendor/illuminate/redis/Connections/Connection.php:81``client()`)。
- symfony `Lock` 自带唯一 token + 所有者校验的 `release()`,可删除手写 Lua 释放脚本。
```mermaid
flowchart LR
Logic[OtaCheckLogic] -->|create resource| Factory[OtaLockFactory]
Factory -->|RedisStore| RawRedis["support Redis connection client (\\Redis)"]
Logic -->|"acquire(false) 重试 / release()"| LockObj[Symfony Lock]
```
## 改动点
### 1. 依赖
-`php82` 容器内执行dev-environment 规则,禁止宿主机 php/composer
`docker exec -w /app/www/ai-device/ota php82 composer require symfony/lock:7.4.9`
- 结果会更新 [composer.json](composer.json) / `composer.lock` / `vendor`
### 2. 新增 `app/support/OtaLockFactory.php`
- 职责:封装 symfony 锁与 phpredis 客户端的装配,返回可用的 `LockInterface`
- 每次调用基于「当前」连接构建 `RedisStore`(不缓存客户端,规避 webman 连接池跨请求换连接导致的陈旧句柄):
```php
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\RedisStore;
use support\Redis;
final class OtaLockFactory
{
/** 基于当前 redis 连接创建一个带 TTL 的命名分布式锁 */
public static function create(string $resource, float $ttlSeconds): LockInterface
{
$store = new RedisStore(Redis::connection()->client());
return (new LockFactory($store))->createLock($resource, $ttlSeconds);
}
}
```
### 3. 重构 [app/logic/OtaCheckLogic.php](app/logic/OtaCheckLogic.php)
- 删除 `use support\Redis;`;新增 `use Symfony\Component\Lock\LockInterface;``use app\support\OtaLockFactory;`
- 保留常量 `LOCK_KEY_PREFIX` / `LOCK_TTL_SECONDS` / `LOCK_MAX_RETRY` / `LOCK_RETRY_INTERVAL_US`
- `withDeviceFirmwareLock`
```php
$lock = OtaLockFactory::create(self::LOCK_KEY_PREFIX . $deviceId . ':' . $firmwareId, (float) self::LOCK_TTL_SECONDS);
$this->acquireOrFail($lock);
try {
return $criticalSection();
} finally {
$this->releaseQuietly($lock);
}
```
- `acquireOrFail(LockInterface $lock)`:循环 `LOCK_MAX_RETRY` 次调用 `$lock->acquire(false)`,成功 return否则 `usleep(LOCK_RETRY_INTERVAL_US)`;用尽后 `Log::warning` + `throw new BusinessException(OtaErrorCode::SERVICE_BUSY)`
- `releaseQuietly(LockInterface $lock)``try { $lock->release(); } catch (Throwable) { Log::warning(...) }`释放失败不影响主流程TTL 兜底)。
- 删除手写 token 生成与 Lua 释放脚本(`acquireLock`/`releaseLock` 旧实现)。
## 验证
- `docker exec -w /app/www/ai-device/ota php82 ./vendor/bin/phpunit`(含 `OtaCheckLogicTest` 真连 redis 验证加锁/幂等)。
- 对改动文件容器内 `php -l`
- 运行完成门禁 `~/.cursor/skills/slot-backend-completion-report/scripts/report.sh`
## 备注 / 取舍
- 可选:将 [composer.json](composer.json) 的 `"php": ">=8.1"` 提升为 `>=8.2` 以与 symfony/lock 对齐(非必需,运行时已是 8.2)。
- 备选方案:不新增 `OtaLockFactory`,把装配内联为 `OtaCheckLogic` 私有方法;本方案选独立 support 类以隔离基础设施、便于复用与替换 Store。
- 阻塞策略:沿用 `acquire(false)` 有界重试而非 `acquire(true)` 无限阻塞,避免阻塞 webman worker并保留 `SERVICE_BUSY` 行为。