mengqiankang 1 месяц назад
Родитель
Сommit
9a83dc529c
3 измененных файлов с 64 добавлено и 12 удалено
  1. 6 2
      alien_gateway/config.py
  2. 10 0
      alien_gateway/main.py
  3. 48 10
      alien_util/redis_client.py

+ 6 - 2
alien_gateway/config.py

@@ -24,11 +24,13 @@ class Settings(BaseSettings):
     # Redis Sentinel 高可用配置
     # Redis Sentinel 高可用配置
     # 例: REDIS_SENTINELS=192.168.2.251:36379,192.168.2.252:36379,192.168.2.253:36379
     # 例: REDIS_SENTINELS=192.168.2.251:36379,192.168.2.252:36379,192.168.2.253:36379
     REDIS_SENTINELS: str = os.getenv("REDIS_SENTINELS", "")
     REDIS_SENTINELS: str = os.getenv("REDIS_SENTINELS", "")
-    REDIS_MASTER_NAME: str = os.getenv("REDIS_MASTER_NAME", "")
+    REDIS_MASTER_NAME: str = os.getenv("REDIS_MASTER_NAME", "mymaster")
     REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
     REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
     REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
     REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
     REDIS_SENTINEL_USERNAME: str = os.getenv("REDIS_SENTINEL_USERNAME", "")
     REDIS_SENTINEL_USERNAME: str = os.getenv("REDIS_SENTINEL_USERNAME", "")
     REDIS_SENTINEL_PASSWORD: str = os.getenv("REDIS_SENTINEL_PASSWORD", "")
     REDIS_SENTINEL_PASSWORD: str = os.getenv("REDIS_SENTINEL_PASSWORD", "")
+    REDIS_SOCKET_TIMEOUT: float = float(os.getenv("REDIS_SOCKET_TIMEOUT", "0.5"))
+    REDIS_CONNECT_TIMEOUT: float = float(os.getenv("REDIS_CONNECT_TIMEOUT", "1.0"))
     # 下游服务地址
     # 下游服务地址
     STORE_BASE_URL: str = os.getenv("STORE_BASE_URL")  # alien_store 服务地址
     STORE_BASE_URL: str = os.getenv("STORE_BASE_URL")  # alien_store 服务地址
 
 
@@ -51,7 +53,9 @@ class Settings(BaseSettings):
             entry = item.strip()
             entry = item.strip()
             if not entry:
             if not entry:
                 continue
                 continue
-            host, port = entry.split(":")
+            if ":" not in entry:
+                raise ValueError(f"Invalid REDIS_SENTINELS entry: {entry}")
+            host, port = entry.split(":", 1)
             nodes.append((host.strip(), int(port.strip())))
             nodes.append((host.strip(), int(port.strip())))
         return nodes
         return nodes
 
 

+ 10 - 0
alien_gateway/main.py

@@ -6,6 +6,7 @@ from fastapi import FastAPI, Request, Response, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.status import HTTP_502_BAD_GATEWAY
 from starlette.status import HTTP_502_BAD_GATEWAY
 from alien_gateway.config import settings
 from alien_gateway.config import settings
+from alien_util.redis_client import check_redis_connection
 
 
 app = FastAPI(
 app = FastAPI(
     title=f"{settings.PROJECT_NAME} - Gateway & Auth Service",
     title=f"{settings.PROJECT_NAME} - Gateway & Auth Service",
@@ -55,6 +56,15 @@ logger = _init_logger()
 async def health():
 async def health():
     return {"service": "alien_gateway", "status": "ok"}
     return {"service": "alien_gateway", "status": "ok"}
 
 
+
+@app.get("/health/redis")
+async def redis_health():
+    try:
+        return check_redis_connection()
+    except Exception as exc:
+        logger.error("redis health check failed err=%s", exc)
+        raise HTTPException(status_code=HTTP_502_BAD_GATEWAY, detail="Redis unavailable")
+
 # 此模块未来将承担 JWT 签发、权限校验中间件、路由聚合等核心功能
 # 此模块未来将承担 JWT 签发、权限校验中间件、路由聚合等核心功能
 @app.post("/auth/login")
 @app.post("/auth/login")
 async def login():
 async def login():

+ 48 - 10
alien_util/redis_client.py

@@ -1,3 +1,5 @@
+from functools import lru_cache
+
 from redis import Redis
 from redis import Redis
 from redis.asyncio import Redis as AsyncRedis
 from redis.asyncio import Redis as AsyncRedis
 from redis.asyncio.sentinel import Sentinel as AsyncSentinel
 from redis.asyncio.sentinel import Sentinel as AsyncSentinel
@@ -10,17 +12,40 @@ def _sentinel_kwargs() -> dict:
     return settings.REDIS_SENTINEL_KWARGS
     return settings.REDIS_SENTINEL_KWARGS
 
 
 
 
+def _redis_connection_kwargs() -> dict:
+    return {
+        "password": settings.REDIS_PASSWORD or None,
+        "db": settings.REDIS_DB,
+        "socket_timeout": settings.REDIS_SOCKET_TIMEOUT,
+        "socket_connect_timeout": settings.REDIS_CONNECT_TIMEOUT,
+        "retry_on_timeout": True,
+        "decode_responses": True,
+    }
+
+
+def _ensure_sentinel_nodes() -> None:
+    if not settings.REDIS_SENTINEL_NODES:
+        raise RuntimeError("REDIS_SENTINELS 未配置,无法通过 Sentinel 连接 Redis。")
+
+
+@lru_cache(maxsize=1)
 def get_sentinel_client() -> Sentinel:
 def get_sentinel_client() -> Sentinel:
+    _ensure_sentinel_nodes()
     return Sentinel(
     return Sentinel(
         settings.REDIS_SENTINEL_NODES,
         settings.REDIS_SENTINEL_NODES,
-        socket_timeout=0.5,
+        socket_timeout=settings.REDIS_SOCKET_TIMEOUT,
+        socket_connect_timeout=settings.REDIS_CONNECT_TIMEOUT,
         sentinel_kwargs=_sentinel_kwargs(),
         sentinel_kwargs=_sentinel_kwargs(),
     )
     )
 
 
+
+@lru_cache(maxsize=1)
 def get_async_sentinel_client() -> AsyncSentinel:
 def get_async_sentinel_client() -> AsyncSentinel:
+    _ensure_sentinel_nodes()
     return AsyncSentinel(
     return AsyncSentinel(
         settings.REDIS_SENTINEL_NODES,
         settings.REDIS_SENTINEL_NODES,
-        socket_timeout=0.5,
+        socket_timeout=settings.REDIS_SOCKET_TIMEOUT,
+        socket_connect_timeout=settings.REDIS_CONNECT_TIMEOUT,
         sentinel_kwargs=_sentinel_kwargs(),
         sentinel_kwargs=_sentinel_kwargs(),
     )
     )
 
 
@@ -28,32 +53,28 @@ def get_async_sentinel_client() -> AsyncSentinel:
 def get_redis_master() -> Redis:
 def get_redis_master() -> Redis:
     return get_sentinel_client().master_for(
     return get_sentinel_client().master_for(
         service_name=settings.REDIS_MASTER_NAME,
         service_name=settings.REDIS_MASTER_NAME,
-        password=settings.REDIS_PASSWORD or None,
-        db=settings.REDIS_DB,
+        **_redis_connection_kwargs(),
     )
     )
 
 
 
 
 def get_redis_slave() -> Redis:
 def get_redis_slave() -> Redis:
     return get_sentinel_client().slave_for(
     return get_sentinel_client().slave_for(
         service_name=settings.REDIS_MASTER_NAME,
         service_name=settings.REDIS_MASTER_NAME,
-        password=settings.REDIS_PASSWORD or None,
-        db=settings.REDIS_DB,
+        **_redis_connection_kwargs(),
     )
     )
 
 
 
 
 def get_async_redis_master() -> AsyncRedis:
 def get_async_redis_master() -> AsyncRedis:
     return get_async_sentinel_client().master_for(
     return get_async_sentinel_client().master_for(
         service_name=settings.REDIS_MASTER_NAME,
         service_name=settings.REDIS_MASTER_NAME,
-        password=settings.REDIS_PASSWORD or None,
-        db=settings.REDIS_DB,
+        **_redis_connection_kwargs(),
     )
     )
 
 
 
 
 def get_async_redis_slave() -> AsyncRedis:
 def get_async_redis_slave() -> AsyncRedis:
     return get_async_sentinel_client().slave_for(
     return get_async_sentinel_client().slave_for(
         service_name=settings.REDIS_MASTER_NAME,
         service_name=settings.REDIS_MASTER_NAME,
-        password=settings.REDIS_PASSWORD or None,
-        db=settings.REDIS_DB,
+        **_redis_connection_kwargs(),
     )
     )
 
 
 
 
@@ -65,3 +86,20 @@ def get_redis() -> Redis:
 def get_async_redis() -> AsyncRedis:
 def get_async_redis() -> AsyncRedis:
     # 默认返回主节点客户端,统一写入入口
     # 默认返回主节点客户端,统一写入入口
     return get_async_redis_master()
     return get_async_redis_master()
+
+
+def check_redis_connection() -> dict:
+    """
+    通过 Sentinel 获取当前主节点并执行 ping,返回连接状态与主节点地址。
+    """
+    sentinel = get_sentinel_client()
+    redis_client = get_redis_master()
+    pong = redis_client.ping()
+    host, port = sentinel.discover_master(settings.REDIS_MASTER_NAME)
+    return {
+        "ok": bool(pong),
+        "master_name": settings.REDIS_MASTER_NAME,
+        "master_host": host,
+        "master_port": port,
+        "db": settings.REDIS_DB,
+    }