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 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': '删除成功',
|
||||
|
||||
@ -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. 数据库查好后写入 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(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user