You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
5.9 KiB
148 lines
5.9 KiB
package echomeet
|
|
|
|
/*
|
|
缓存组件:把高频访问的 public 公共模板加载到 Redis,并对外提供读取接口。
|
|
|
|
public 模板存于 admin MySQL(海外),所有用户共享、每次 GetTemplates 都要读取,是慢查询热点。
|
|
本组件采用"全量预热 + 定时刷新 + 事件驱动刷新"策略,使读请求恒命中 Redis:
|
|
- 预热:Start() 时全量写入 Redis
|
|
- 定时刷新:内部 cron 每 10 分钟全量刷新
|
|
- 事件驱动:后台增删改 public 模板触发 Rpc_ModifyEchomeetTemplate,由本组件监听并刷新
|
|
|
|
用户私有模板(ServiceDB,高基数、专属)不预热,相关读取在缓存未命中时回退查 DB。
|
|
*/
|
|
|
|
import (
|
|
"context"
|
|
"yunyan/comm"
|
|
"yunyan/lego/base"
|
|
"yunyan/lego/core"
|
|
"yunyan/lego/core/cbase"
|
|
"yunyan/lego/sys/cron"
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/pb"
|
|
"yunyan/sys/cache"
|
|
natssys "yunyan/sys/nats"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
gonats "github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// 缓存兜底 TTL:正常由定时/事件全量刷新覆盖,TTL 仅作异常情况下的过期保护
|
|
const cacheTTL = time.Hour * 24
|
|
|
|
type modelCacheComp struct {
|
|
cbase.ModuleCompBase
|
|
module *Echomeet
|
|
service base.IRPCXService
|
|
}
|
|
|
|
func (this *modelCacheComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
|
|
this.ModuleCompBase.Init(service, module, comp, opt)
|
|
this.module = module.(*Echomeet)
|
|
this.service = service.(base.IRPCXService)
|
|
return
|
|
}
|
|
|
|
func (this *modelCacheComp) Start() (err error) {
|
|
if err = this.ModuleCompBase.Start(); err != nil {
|
|
return
|
|
}
|
|
this.service.Register(string(comm.Rpc_ModifyEchomeetTemplate), this.Rpc_ModifyEchomeetTemplate) //事件驱动刷新
|
|
go this.Refresh() //启动预热(异步,避免海外 DB 慢查询阻塞模块启动)
|
|
cron.AddFunc("0 */10 * * * ?", this.Refresh) //每 10 分钟全量刷新
|
|
this.subscribeConfigChanged() //订阅 console 配置变更广播
|
|
return
|
|
}
|
|
|
|
// subscribeConfigChanged 订阅 console 的「配置已变更」广播(core NATS pub/sub,fanout)。
|
|
// 收到 template 类即全量刷新 public 模板缓存(与 Rpc_ModifyEchomeetTemplate 同效);
|
|
// 其余类型本模块不关心。NATS 未就绪则跳过——仍有 10 分钟定时刷新兜底。
|
|
func (this *modelCacheComp) subscribeConfigChanged() {
|
|
conn := natssys.Conn()
|
|
if conn == nil {
|
|
this.module.Warn("echomeet: NATS 未就绪,配置变更广播订阅未启用(定时刷新兜底)")
|
|
return
|
|
}
|
|
if _, err := conn.Subscribe(comm.Nats_ConfigChangedSubject, func(msg *gonats.Msg) {
|
|
var ev comm.ConfigChangedEvent
|
|
if e := json.Unmarshal(msg.Data, &ev); e != nil {
|
|
return
|
|
}
|
|
if ev.Kind == comm.ConfigKindTemplate {
|
|
this.Refresh()
|
|
this.module.Infof("echomeet: 按配置变更广播刷新模板缓存 action=%s", ev.Action)
|
|
}
|
|
}); err != nil {
|
|
this.module.Error("echomeet: 订阅配置变更广播失败", log.Field{Key: "err", Value: err.Error()})
|
|
}
|
|
}
|
|
|
|
// 事件驱动:后台增删改 public 模板后刷新 Redis 缓存
|
|
func (this *modelCacheComp) Rpc_ModifyEchomeetTemplate(ctx context.Context, args *pb.Rpc_EmptyReq, reply *pb.Rpc_EmptyResp) (err error) {
|
|
this.Refresh()
|
|
return
|
|
}
|
|
|
|
// Refresh 全量刷新缓存数据集到 Redis。best-effort:失败仅记日志
|
|
func (this *modelCacheComp) Refresh() {
|
|
models, err := this.module.model.getcommtemplates("")
|
|
if err != nil {
|
|
this.module.Error("refresh echomeet template load db", log.Field{Key: "err", Value: err.Error()})
|
|
return
|
|
}
|
|
items := make(map[string]any, len(models))
|
|
for _, m := range models {
|
|
items[fmt.Sprintf("%d", m.Id)] = m
|
|
}
|
|
if err = cache.ReplaceAll(context.Background(), comm.Cache_EchomeetTemplate, items, cacheTTL); err != nil {
|
|
this.module.Error("refresh echomeet template write redis", log.Field{Key: "err", Value: err.Error()})
|
|
}
|
|
}
|
|
|
|
// ---- 对外读取接口(缓存优先 + DB 回退)-----------------------------------------
|
|
|
|
// GetCommTemplates 读取 public 公共模板:缓存优先,language 为空返回全部,否则按语言过滤
|
|
func (this *modelCacheComp) GetCommTemplates(language string) (models []*pb.DBEchoMeetTemplate, err error) {
|
|
list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate)
|
|
if cerr != nil || len(list) == 0 {
|
|
return this.module.model.getcommtemplates(language) //回退查 DB
|
|
}
|
|
models = make([]*pb.DBEchoMeetTemplate, 0, len(list))
|
|
for _, m := range list {
|
|
if language == "" || m.Language == language {
|
|
models = append(models, m)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetTemplateForId 按 id 读取模板:先查 public 缓存命中即返回,否则回退 DB(覆盖用户私有模板)
|
|
func (this *modelCacheComp) GetTemplateForId(id uint64) (template *pb.DBEchoMeetTemplate, err error) {
|
|
if v, found, cerr := cache.GetOne[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate, fmt.Sprintf("%d", id)); cerr == nil && found {
|
|
return v, nil
|
|
}
|
|
return this.module.model.gettemplateforid(id)
|
|
}
|
|
|
|
// GetTemplateForTid 按 tid+language 读取模板:先扫 public 缓存命中即返回,否则回退 DB
|
|
func (this *modelCacheComp) GetTemplateForTid(tid string, language string) (template *pb.DBEchoMeetTemplate, err error) {
|
|
if list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate); cerr == nil {
|
|
for _, m := range list {
|
|
if m.Tid == tid && m.Language == language {
|
|
return m, nil
|
|
}
|
|
}
|
|
}
|
|
return this.module.model.gettemplatefortid(tid, language)
|
|
}
|
|
|
|
// ResolveTemplate tid 非空则按 tid+language 查找,否则按主键查找(与 model.resolvetemplate 行为一致)
|
|
func (this *modelCacheComp) ResolveTemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) {
|
|
if tid != "" {
|
|
return this.GetTemplateForTid(tid, language)
|
|
}
|
|
return this.GetTemplateForId(templateid)
|
|
}
|
|
|