perf: 引入 Redis Cache-Aside 模式优化 BOM 读取,TTL=12h,写操作后主动失效缓存

This commit is contained in:
DXC
2026-05-19 10:14:55 +08:00
parent c0ab3ce6d2
commit c60112f5f8
2 changed files with 131 additions and 3 deletions

View File

@ -1,6 +1,6 @@
from flask import Blueprint, request, jsonify, current_app
from sqlalchemy import or_
from app.services.bom_service import BomService
from app.services.bom_service import BomService, _cache_delete
from app.models.base import MaterialBase
from app.models.bom import BomTable
from app.extensions import db
@ -225,6 +225,11 @@ def delete_bom(bom_no):
db.session.delete(rec)
db.session.commit()
# ===== 删除成功后立刻清除缓存Cache Invalidation =====
_cache_delete(bom_no, version)
current_app.logger.info(f"[BOM Cache] delete_bom → 缓存已失效 bom_no={bom_no} version={version}")
return jsonify({
'code': 200,
'msg': '删除成功',

View File

@ -5,8 +5,96 @@ from app.models.inbound.buy import StockBuy
from sqlalchemy import func, distinct, or_, case
from collections import defaultdict
import uuid
import json
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
# Redis 缓存键前缀 + TTL
BOM_CACHE_PREFIX = 'bom:tree'
BOM_CACHE_TTL = 43200 # 12小时
def _get_redis():
"""
获取 Redis 客户端实例,带容错保护。
若 extensions 中没有 redis_client 或连接失败,返回 None。
绝不抛出异常,确保业务不因此中断。
"""
try:
from app.extensions import redis_client
return redis_client
except Exception:
return None
def _cache_get(bom_no, version=None):
"""
从 Redis 读取 BOM 缓存。
键 = bom:tree:{bom_no} 或 bom:tree:{bom_no}:{version}
返回:反序列化后的 dict 或 None
"""
client = _get_redis()
if not client:
return None
key = f"{BOM_CACHE_PREFIX}:{bom_no}" + (f":{version}" if version else "")
try:
raw = client.get(key)
if raw:
logger.debug(f"[BOM Cache] HIT key={key}")
return json.loads(raw)
logger.debug(f"[BOM Cache] MISS key={key}")
return None
except Exception as e:
logger.warning(f"[BOM Cache] GET 失败,降级查库. key={key}, err={e}")
return None
def _cache_set(bom_no, version, data):
"""
将 BOM 数据写入 Redis设置 12 小时 TTL。
即使写入失败也只是打日志,不阻断业务流程。
"""
client = _get_redis()
if not client:
return
key = f"{BOM_CACHE_PREFIX}:{bom_no}" + (f":{version}" if version else "")
try:
client.setex(key, BOM_CACHE_TTL, json.dumps(data, ensure_ascii=False))
logger.debug(f"[BOM Cache] SET key={key} ttl={BOM_CACHE_TTL}s")
except Exception as e:
logger.warning(f"[BOM Cache] SET 失败,已忽略. key={key}, err={e}")
def _cache_delete(bom_no, version=None):
"""
删除 Redis 中指定 BOM 的缓存条目。
在写操作(增/改/删)成功后调用,确保后续读请求拿到最新数据。
"""
client = _get_redis()
if not client:
return
# 删除版本级缓存
if version:
key = f"{BOM_CACHE_PREFIX}:{bom_no}:{version}"
try:
client.delete(key)
logger.debug(f"[BOM Cache] DEL key={key}")
except Exception as e:
logger.warning(f"[BOM Cache] DEL 失败,已忽略. key={key}, err={e}")
# 同时删除"最新版"缓存(不带版本后缀),避免缓存不一致
key_latest = f"{BOM_CACHE_PREFIX}:{bom_no}"
try:
client.delete(key_latest)
logger.debug(f"[BOM Cache] DEL key={key_latest}")
except Exception as e:
logger.warning(f"[BOM Cache] DEL 失败,已忽略. key={key_latest}, err={e}")
class BomService:
# ====================== 新版 BOM 逻辑(基于 bom_no ======================
@ -121,8 +209,25 @@ class BomService:
@staticmethod
def get_bom_detail(bom_no, version=None):
"""
根据 bom_no (和 version) 获取配方详情
根据 bom_no (和 version) 获取配方详情
Cache-Aside 模式(三步走):
1. 先查 Redis有值直接返回Cache Hit
2. 无值或 Redis 报错查数据库Cache Miss → Fallback
3. 数据库查好后写入 RedisTTL=12h供下次命中
注意:查询"最新版"version=None缓存键不带版本后缀
写入时也写入不带版本的键,这样无需指定 version 就能命中。
"""
# ===== 第一步:尝试从 Redis 读取缓存 =====
cached = _cache_get(bom_no, version if version else None)
if cached is not None:
# Cache Hit直接返回缓存数据不再查库
logger.debug(f"[BOM] get_bom_detail bom_no={bom_no} version={version} → 命中缓存")
return cached
# ===== 第二步Cache Miss → 查数据库 =====
logger.debug(f"[BOM] get_bom_detail bom_no={bom_no} version={version} → 查询数据库")
query = db.session.query(
BomTable,
MaterialBase.name.label('child_name'),
@ -141,6 +246,8 @@ class BomService:
if not latest_ver:
return None
query = query.filter(BomTable.version == latest_ver)
# 记录本次实际查的版本,用于缓存键
version = latest_ver
rows = query.all()
if not rows:
@ -160,7 +267,7 @@ class BomService:
'remark': bom.remark or ''
})
return {
result = {
'bom_no': bom_no,
'version': first.BomTable.version,
'parent_id': parent_id,
@ -170,6 +277,11 @@ class BomService:
'children': children
}
# ===== 第三步:写入 Redis 缓存TTL=12h失败只打日志不阻断 =====
_cache_set(bom_no, version, result)
return result
@staticmethod
def save_bom(data):
"""保存 BOM (支持多版本),新增跨版本内容查重"""
@ -239,6 +351,12 @@ class BomService:
db.session.add(bom)
db.session.commit()
# ===== 写入后立刻清除缓存Cache Invalidation =====
# 确保后续 get_bom_detail 读取到最新数据,而不是 stale cache
_cache_delete(bom_no, version)
logger.info(f"[BOM Cache] save_bom → 缓存已失效 bom_no={bom_no} version={version}")
return bom_no
@staticmethod
@ -312,6 +430,11 @@ class BomService:
)
db.session.add(bom)
db.session.commit()
# ===== 写入后立刻清除缓存Cache Invalidation =====
_cache_delete(bom_no, version)
logger.info(f"[BOM Cache] create_or_update_bom → 缓存已失效 bom_no={bom_no} version={version}")
return True
@staticmethod