
本文介绍在 yii2 框架中实现 url 级别的实时独占访问控制:当某注册用户已打开特定页面时,其他用户尝试访问将被自动拦截;用户离开或关闭页面后资源立即释放,避免依赖定时缓存导致的延迟与不准确。
本文介绍在 yii2 框架中实现 url 级别的实时独占访问控制:当某注册用户已打开特定页面时,其他用户尝试访问将被自动拦截;用户离开或关闭页面后资源立即释放,避免依赖定时缓存导致的延迟与不准确。
在 Yii2 中实现“页面独占访问”(即同一 URL 同一时刻仅允许一个已登录用户访问),关键在于状态感知 + 实时校验 + 可靠释放。单纯依赖缓存 TTL(如 yii\caching\Cache::set($key, $value, 300))无法满足“用户关闭浏览器即释放”的强实时性要求,必须结合服务端状态管理与客户端协同机制。
核心思路:基于会话的独占标识 + 前端心跳保活
Yii2 默认使用 PHP Session,可利用其存储轻量级页面占用状态,并通过中间件(ActionFilter)统一拦截请求:
✅ 步骤一:定义独占页面标识规则
为需保护的 URL 设计唯一键名,例如:
// 对于 /admin/update-promo?id=123 → 键名为 'promo_edit_123'
$resourceKey = 'promo_edit_' . (int) Yii::$app->request->get('id');
✅ 步骤二:创建独占访问过滤器(ActionFilter)
新建 components/ExclusiveAccessFilter.php:
<?php namespace app\components;
use Yii;
use yii\base\ActionFilter;
use yii\web\NotFoundHttpException;
use yii\web\UnauthorizedHttpException;
class ExclusiveAccessFilter extends ActionFilter
{
public $resourceKeyCallback; // function($action): string
public $redirectUrl = '/site/index';
public $timeoutSeconds = 60; // 客户端无心跳超时(防死锁)
public function beforeAction($action)
{
if (Yii::$app->user->isGuest) {
throw new UnauthorizedHttpException('Login required.');
}
$resourceKey = call_user_func($this->resourceKeyCallback, $action);
$currentUser = Yii::$app->user->id;
// 读取当前占用者(支持 Redis 或 DB 缓存,推荐 Redis)
$occupiedBy = Yii::$app->cache->get($resourceKey);
if ($occupiedBy !== false && $occupiedBy != $currentUser) {
Yii::$app->session->setFlash('error', '该页面正由其他用户编辑,请稍后再试。');
$this->owner->redirect($this->redirectUrl)->send();
return false;
}
// 占用当前资源(带过期时间,兜底保障)
Yii::$app->cache->set($resourceKey, $currentUser, $this->timeoutSeconds);
return true;
}
}
✅ 步骤三:在控制器中启用过滤器
<?php namespace app\controllers;
use yii\web\Controller;
use app\components\ExclusiveAccessFilter;
class PromoController extends Controller
{
public function behaviors()
{
return [
'exclusive' => [
'class' => ExclusiveAccessFilter::class,
'resourceKeyCallback' => function ($action) {
return 'promo_edit_' . (int) Yii::$app->request->get('id');
},
'redirectUrl' => ['/promo/index'],
'timeoutSeconds' => 45,
],
];
}
public function actionUpdate($id)
{
// 业务逻辑:加载并渲染编辑页
return $this->render('update', ['model' => $this->findModel($id)]);
}
}
✅ 步骤四:前端协同 —— 页面卸载时主动释放(关键!)
在视图 update.php 底部添加 JS:
<script>
// 页面卸载前主动释放锁(关闭标签页、刷新、跳转均触发)
window.addEventListener('beforeunload', function () {
fetch('/promo/release-lock?id=<?= $model->id ?>', {
method: 'POST',
headers: { 'X-CSRF-Token': '<?= Yii::$app->request->csrfToken ?>', },
credentials: 'same-origin'
});
});
// 可选:每30秒发送心跳延长占用(防止误释放)
setInterval(() => {
fetch('/promo/keep-alive?id=<?= $model->id ?>', {
method: 'POST',
headers: { 'X-CSRF-Token': '<?= Yii::$app->request->csrfToken ?>', }
});
}, 30000);
</script>
并在控制器中补充两个轻量接口:
public function actionReleaseLock($id)
{
Yii::$app->cache->delete('promo_edit_' . (int)$id);
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return ['success' => true];
}
public function actionKeepAlive($id)
{
$key = 'promo_edit_' . (int)$id;
if (Yii::$app->cache->exists($key)) {
Yii::$app->cache->set($key, Yii::$app->user->id, $this->timeoutSeconds);
}
return ['success' => true];
}
⚠️ 注意事项与最佳实践
- 缓存驱动建议使用 Redis:yii\redis\Cache 支持原子操作与 TTL,比文件/DB 缓存更可靠;
- CSRF 保护必须开启:所有 /release-lock 和 /keep-alive 接口需校验 token,防止恶意释放;
- 避免全局锁滥用:仅对高冲突敏感操作(如订单编辑、配置发布)启用,非必要不加锁;
- 用户体验优化:前端可轮询锁状态,在按钮上显示“被 XXX 占用中…”而非直接跳转;
- 服务端兜底机制:timeoutSeconds 是安全阀,确保异常断连后锁自动释放(45–60 秒较合理)。
通过上述方案,你获得的是一个响应及时、释放可靠、扩展性强的独占访问系统——它不依赖浏览器行为猜测,而是以服务端状态为核心,辅以前端主动协作,真正实现“所见即所得”的并发控制。











