审查 Redis 用法时找到的四处小账,都是确定性改动,语义不变: - `getUserByToken` 里两条串行 EXPIRE 合成一次 pipeline。这是全后端最热的 Redis 路径 —— 每个带鉴权的 HTTP 请求都要续一次会话和反向索引。上次给 `touchSession` 修的正是同一个形状,这处漏了,两边现在一致。 - `createSession` 的 SET / SADD / EXPIRE 三趟并成一趟。登录是突发的, 一个班同时登录时差别全压在这一下。 - 限流的 Lua 从 `redis.eval` 改成 `defineCommand`,稳态走 EVALSHA 只发 40 字节 sha1,不再每次带上 937 字节的脚本全文;NOSCRIPT 由 ioredis 自动回退成 EVAL 重新灌,Redis 重启和 SCRIPT FLUSH 都不用管。 - 删掉 websocket 订阅连接上重复的 error 监听 —— `withErrorLogging` 已经 打过一遍且带连接名,留着只会把同一条错误打两份。 实跑验证(dev 栈,API 跑在 3999):登录后 `session:*` 多一条、 `user-sessions:1` 的 scard 和 ttl(604800) 都对;把两个键的 TTL 压到 100 再打一次带鉴权的请求,两个都回到 604800。限流侧 `info commandstats` 显示 evalsha calls=3/failed=1 + eval calls=2 —— 失败那次正是 SCRIPT FLUSH 之后 的 NOSCRIPT 回退;令牌桶数值与旧实现逐位一致(10 个初始额度扣 3 剩 7, 再要 20 个被拒并返回 wait=433.33 = (20-7)/0.03)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
This commit is contained in:
@@ -72,14 +72,14 @@ export async function createSession(
|
||||
previousLogin,
|
||||
contestPasswords: {},
|
||||
}
|
||||
await redis.set(
|
||||
sessionKey(token),
|
||||
JSON.stringify(value),
|
||||
"EX",
|
||||
config.sessionTtlSeconds,
|
||||
)
|
||||
await redis.sadd(userSessionsKey(userId), token)
|
||||
await redis.expire(userSessionsKey(userId), config.sessionTtlSeconds)
|
||||
// 三条写进一个 pipeline:一个班四十号人同时登录时,三趟往返和一趟的差别
|
||||
// 全压在登录这一下上
|
||||
await redis
|
||||
.pipeline()
|
||||
.set(sessionKey(token), JSON.stringify(value), "EX", config.sessionTtlSeconds)
|
||||
.sadd(userSessionsKey(userId), token)
|
||||
.expire(userSessionsKey(userId), config.sessionTtlSeconds)
|
||||
.exec()
|
||||
setCookie(c, config.sessionCookie, token, {
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
@@ -193,10 +193,14 @@ async function getUserByToken(token: string | undefined): Promise<SessionResult>
|
||||
return { user: null, reason: "disabled" }
|
||||
}
|
||||
|
||||
await redis.expire(sessionKey(token), config.sessionTtlSeconds)
|
||||
// 反向索引跟着会话一起续期,否则活跃用户的索引会先于会话到期,
|
||||
// 之后再吊销就找不到这张会话了
|
||||
await redis.expire(userSessionsKey(session.userId), config.sessionTtlSeconds)
|
||||
// 之后再吊销就找不到这张会话了。两条走一次 pipeline —— 这是全后端最热的 Redis
|
||||
// 路径,每个带鉴权的请求都要走一趟,形状和 touchSession 里那对保持一致
|
||||
await redis
|
||||
.pipeline()
|
||||
.expire(sessionKey(token), config.sessionTtlSeconds)
|
||||
.expire(userSessionsKey(session.userId), config.sessionTtlSeconds)
|
||||
.exec()
|
||||
// 唯一的收窄点。库里是 text 列,认不出来的值降成最低权限,见 toAdminType 的注释。
|
||||
return {
|
||||
user: {
|
||||
|
||||
Reference in New Issue
Block a user