fix(usage): session touch 独立提交避免行锁阻塞 & 管理员页面顺序加载降低并发压力

后端: 将 session touch 的 commit 从请求事务中分离,防止管理员 usage
页面的长查询持有 user_sessions 行锁阻塞后续请求。touch_session 改为
返回 bool 以支持按需提交。

前端: 管理员 Usage 页面将并行 API 调用改为顺序加载,优先显示记录表格,
统计面板在后台异步刷新,避免瞬时并发打满后端 worker。loadRecords 支持
传入 dateRange 参数确保时间范围一致性。
This commit is contained in:
fawney19
2026-03-18 00:12:00 +08:00
parent eeb5f41bad
commit 684689a82b
6 changed files with 373 additions and 57 deletions

View File

@@ -57,6 +57,30 @@ class ApiRequestPipeline:
self.usage_service = usage_service
self.audit_service = audit_service
def _commit_session_touch(self, db: Session, *, scope: str) -> None:
"""Persist session last_seen updates immediately to avoid holding row locks.
Admin usage views can execute heavy read queries after authentication.
If the request later stalls, leaving the session touch inside the request
transaction can block all subsequent requests that update the same
`user_sessions` row. Commit the touch in its own short transaction so
later long-running reads cannot keep the session row locked.
"""
original_expire_on_commit = getattr(db, "expire_on_commit", None)
try:
if original_expire_on_commit is not None:
db.expire_on_commit = False
db.commit()
except Exception as exc:
try:
db.rollback()
except Exception as rollback_exc:
logger.debug("[Pipeline] {} session touch rollback failed: {}", scope, rollback_exc)
logger.warning("[Pipeline] failed to persist {} session touch: {}", scope, exc)
finally:
if original_expire_on_commit is not None:
db.expire_on_commit = original_expire_on_commit
async def run(
self,
adapter: ApiAdapter,
@@ -492,11 +516,13 @@ class ApiRequestPipeline:
if not session:
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
SessionService.assert_session_device_matches(session, client_device_id)
SessionService.touch_session(
session_touched = SessionService.touch_session(
session,
client_ip=get_client_ip(request),
user_agent=request.headers.get("user-agent", "unknown"),
)
if session_touched:
self._commit_session_touch(db, scope="admin")
request.state.user_session_id = session.id
request.state.user_id = db_user.id
@@ -549,11 +575,13 @@ class ApiRequestPipeline:
if not session:
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
SessionService.assert_session_device_matches(session, client_device_id)
SessionService.touch_session(
session_touched = SessionService.touch_session(
session,
client_ip=get_client_ip(request),
user_agent=request.headers.get("user-agent", "unknown"),
)
if session_touched:
self._commit_session_touch(db, scope="user")
request.state.user_session_id = session.id
request.state.user_id = db_user.id
return db_user, None

View File

@@ -328,19 +328,20 @@ class SessionService:
*,
client_ip: str | None,
user_agent: str,
) -> None:
) -> bool:
now = datetime.now(timezone.utc)
last_seen_at = session.last_seen_at
if last_seen_at.tzinfo is None:
last_seen_at = last_seen_at.replace(tzinfo=timezone.utc)
if (now - last_seen_at).total_seconds() < SESSION_TOUCH_INTERVAL_SECONDS:
return
return False
session.last_seen_at = now
if client_ip:
session.ip_address = client_ip
if user_agent:
session.user_agent = user_agent[:1000]
return True
@staticmethod
def revoke_session(