不能用触发器可靠限制单用户并发登录会话数,因其无法捕获连接建立/断开事件,且存在竞态、超限、漏减及异常断连无法清理等问题。

不能用触发器可靠限制单用户并发登录会话数。 MySQL 的触发器在 SQL 执行阶段才触发,而连接建立、断开完全发生在 SQL 层之下,触发器根本无法捕获这些事件。强行用 INSERT/UPDATE 触发器维护计数,大概率导致计数漂移、超限、漏减,且无法处理异常断连。
为什么 BEFORE/AFTER 触发器对会话控制完全失效
常见误解是:在 user_sessions 表上建 BEFORE INSERT 触发器就能“拦截超额登录”。但实际流程是:TCP 连接建立 → 认证通过 → Session 初始化完成 → 才轮到第一条 SQL 执行。此时连接早已存在,触发器只是事后补救,毫无拦截能力。
更严重的问题包括:
-
CURRENT_USER()虽然可信,但若业务统一用app_rw@'%'登录,所有会话都显示同一值,失去用户粒度 - 多个并发请求同时读取当前计数再 +1,必然发生 check-then-act 竞态,计数失真
- 客户端崩溃、网络中断、连接池主动 kill 连接 → 没有对应 SQL 触发点,计数永远卡住
-
information_schema.PROCESSLIST在 MySQL 8.0+ 中已弃用,且默认权限不足;查它会报错ERROR 1227 (42000)
真正可用的实时检查方式:查 performance_schema.threads
MySQL 8.0 正确做法是放弃持久化计数表,每次关键操作前(如登录接口校验)直接查系统表获取真实活跃会话数。前提是提前授权:
GRANT SELECT ON performance_schema.threads TO 'app_user'@'%';
然后执行:
SELECT COUNT(*) FROM performance_schema.threads WHERE TYPE = 'FOREGROUND' AND PROCESSLIST_USER = 'target_user'@'%' AND PROCESSLIST_STATE IS NOT NULL;
注意要点:
- 必须用
PROCESSLIST_USER(不是USER),它和CURRENT_USER()一致,反映认证身份 -
TYPE = 'FOREGROUND'排除后台线程(如复制IO/SQL线程) -
PROCESSLIST_STATE IS NOT NULL过滤掉刚建立但尚未执行任何命令的空闲连接(避免误判) - 该查询毫秒级返回,无锁、无竞态、不依赖应用层状态同步
如果非要持久化计数,必须配锁 + 清理双机制
仅当业务强依赖本地缓存计数(比如做限流兜底),才考虑写入一张 session_count 表,但必须满足三个硬性条件:
- 计数更新必须原子:用
UPDATE session_count SET cnt = cnt + 1 WHERE user_name = CURRENT_USER() AND cnt ,并配合 <code>SELECT ... FOR UPDATE加行锁 - 必须部署定时清理任务(如每 30 秒):扫描
performance_schema.threads,对比发现已消失的THREAD_ID,主动减 1 - 禁止在触发器里调用
SLEEP()或发起远程请求 —— 会阻塞整个事务,引发雪崩
这本质上是在模拟一个脆弱的本地 cache,复杂度远高于直接查系统表,且仍存在几十秒的延迟窗口。
最易被忽略的一点:MySQL 本身不提供“强制单点登录”语义。MAX_USER_CONNECTIONS 控制的是并发连接数,不是逻辑会话;即使设为 1,用户仍可通过不同 host(如 'user'@'192.168.1.10' 和 'user'@'localhost')绕过限制——它们在 mysql.user 表中是两行独立记录。











