
Token 存储该用 TEXT 还是 VARCHAR?
直接说结论:用 VARCHAR(255),别用 TEXT。JWT 或 UUID 类 Token 长度基本固定,VARCHAR 支持索引、比较快、排序稳定;而 TEXT 在多数 mysql 版本里不能建普通索引(除非前缀索引),且 WHERE token = ? 查询可能隐式转成全表扫描。
常见错误现象:select * FROM sessions WHERE token = 'xxx' 执行慢,EXPLAIN 显示 type: ALL —— 很大概率是字段类型用了 TEXT 又没加前缀索引。
- UUID v4 是 36 字符(含连字符),JWT 通常在 150–300 字节之间,
VARCHAR(255)足够且留有余量 - 如果用的是 base64url 编码的随机字节(如 32 字节 → 约 44 字符),
VARCHAR(64)就够了 - 别为了“省事”设成
VARCHAR(1024)或更大——InnoDB 行长度限制和内存排序开销会悄悄拖慢批量操作
Session 表要不要存 user_id?
要,而且必须加 INDEX。单点登录的核心逻辑是“一个用户只能有一个有效 Token”,所有校验、踢出、续期都依赖 user_id 快速定位。
使用场景:用户重新登录时,需立刻失效旧 Token;管理员强制下线某用户时,得按 user_id 删除多条记录。
-
user_id类型必须和用户主表一致(比如用户表是BIGint UNSIGNED,这里就不能用INT) - 建联合索引
(user_id, expires_at),既支持“查用户当前 Token”,也支持“清理过期 Token” - 别只建单列
INDEX(user_id)—— 过期清理语句delete FROM sessions WHERE user_id = ? AND expires_at 会走不到索引末尾
expires_at 字段为什么必须用 dateTIME 而非 timestamp?
因为 TIMESTAMP 会受 MySQL 时区设置影响,而 Token 过期时间必须绝对精确、与服务器本地时区解耦。
常见错误现象:测试环境时区设为 +08:00,生产环境是 UTC,结果 Token 提前 8 小时失效,前端报“登录已过期”但用户刚登 10 分钟。
-
DATETIME存的是字面值,写入什么就查出什么,不受time_zone变更影响 - 插入时统一用 UTC 时间(如 PHP 的
date('Y-m-d H:i:s', time())+ 手动转 UTC,或 Python 的datetime.now(timezone.utc)) - 别依赖 MySQL 的
NOW()或CURRENT_TIMESTAMP默认值——它们默认跟随会话时区
Token 冲突和并发写入怎么防?
靠唯一约束 + INSERT ... ON DUPLICATE KEY UPDATE,而不是先 SELECT 再 INSERT。否则高并发下必然出现双写、覆盖、旧 Token 漏删。
性能影响:唯一索引冲突时,MySQL 会加间隙锁,但比应用层重试+悲观锁轻得多;只要 token 字段有 UNIQUE 约束,就天然防重复。
- 建表时必须加
UNIQUE KEY uk_token (token) - 登录成功写入:用
INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), expires_at = VALUES(expires_at) - 不要用
REPLACE INTO—— 它本质是 DELETE + INSERT,在有外键或触发器时行为不可控
真正容易被忽略的是:Token 生成必须带足够熵(比如 random_bytes(32) 而非 md5(time().rand())),否则再严谨的存储设计也挡不住撞库。