perf: 引入 Redis Cache-Aside 模式优化 BOM 读取,TTL=12h,写操作后主动失效缓存
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, request, jsonify, current_app
|
from flask import Blueprint, request, jsonify, current_app
|
||||||
from sqlalchemy import or_
|
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.base import MaterialBase
|
||||||
from app.models.bom import BomTable
|
from app.models.bom import BomTable
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
@ -225,6 +225,11 @@ def delete_bom(bom_no):
|
|||||||
db.session.delete(rec)
|
db.session.delete(rec)
|
||||||
|
|
||||||
db.session.commit()
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': '删除成功',
|
'msg': '删除成功',
|
||||||
|
|||||||
@ -5,8 +5,96 @@ from app.models.inbound.buy import StockBuy
|
|||||||
from sqlalchemy import func, distinct, or_, case
|
from sqlalchemy import func, distinct, or_, case
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
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:
|
class BomService:
|
||||||
# ====================== 新版 BOM 逻辑(基于 bom_no) ======================
|
# ====================== 新版 BOM 逻辑(基于 bom_no) ======================
|
||||||
@ -121,8 +209,25 @@ class BomService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bom_detail(bom_no, version=None):
|
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. 数据库查好后写入 Redis,TTL=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(
|
query = db.session.query(
|
||||||
BomTable,
|
BomTable,
|
||||||
MaterialBase.name.label('child_name'),
|
MaterialBase.name.label('child_name'),
|
||||||
@ -141,6 +246,8 @@ class BomService:
|
|||||||
if not latest_ver:
|
if not latest_ver:
|
||||||
return None
|
return None
|
||||||
query = query.filter(BomTable.version == latest_ver)
|
query = query.filter(BomTable.version == latest_ver)
|
||||||
|
# 记录本次实际查的版本,用于缓存键
|
||||||
|
version = latest_ver
|
||||||
|
|
||||||
rows = query.all()
|
rows = query.all()
|
||||||
if not rows:
|
if not rows:
|
||||||
@ -160,7 +267,7 @@ class BomService:
|
|||||||
'remark': bom.remark or ''
|
'remark': bom.remark or ''
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
'bom_no': bom_no,
|
'bom_no': bom_no,
|
||||||
'version': first.BomTable.version,
|
'version': first.BomTable.version,
|
||||||
'parent_id': parent_id,
|
'parent_id': parent_id,
|
||||||
@ -170,6 +277,11 @@ class BomService:
|
|||||||
'children': children
|
'children': children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ===== 第三步:写入 Redis 缓存(TTL=12h),失败只打日志不阻断 =====
|
||||||
|
_cache_set(bom_no, version, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_bom(data):
|
def save_bom(data):
|
||||||
"""保存 BOM (支持多版本),新增跨版本内容查重"""
|
"""保存 BOM (支持多版本),新增跨版本内容查重"""
|
||||||
@ -239,6 +351,12 @@ class BomService:
|
|||||||
db.session.add(bom)
|
db.session.add(bom)
|
||||||
|
|
||||||
db.session.commit()
|
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
|
return bom_no
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -312,6 +430,11 @@ class BomService:
|
|||||||
)
|
)
|
||||||
db.session.add(bom)
|
db.session.add(bom)
|
||||||
db.session.commit()
|
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
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user