影刀RPA店群自动化缓存架构实战:Python协同多级缓存与数据一致性设计
影刀RPA店群自动化缓存架构实战Python协同多级缓存与数据一致性设计每次采集商品数据都重新加载页面每次上货都重新查询运费模板。拼多多店群自动化报活动上架这些重复操作累积的延迟正在悄悄吃掉你的利润。在店群自动化的性能优化中我们注意到一个现象很多页面数据的读取是高频且重复的。比如采集竞品价格时同一个商品列表一天内被访问多次上货时运费模板ID、类目ID等配置信息几乎不变但每次流程都会重新查询一遍。如果能把“读多写少”的数据缓存起来避免重复的页面加载和API调用整体的任务执行速度将显著提升。于是我们设计了一套多级缓存系统将数据访问路径从“浏览器→网络→平台服务器”缩短到“本地内存→Redis→浏览器缓存”把延迟从秒级降到了毫秒级。TEMU店群矩阵自动化运营核价报活动一、缓存的三个层级本地、Redis、浏览器端存储针对店群自动化的数据访问特点我们把缓存分为三层L1 本地进程内存缓存最快但生命周期短Worker重启后丢失。适合缓存业务配置、店铺基础信息等极少变化的数据。L2 Redis 共享缓存跨Worker共享适合存储各店铺共用的平台规则、模板数据以及需要多个Worker协同更新的数据。L3 浏览器端存储IndexedDB / LocalStorage适合缓存当前店铺会话级的页面静态资源标识、已加载的商品列表等减少页面重新渲染。一个数据请求会先查L1未命中查L2再未命中查L3最后才通过浏览器发起真实网络请求。importtimeimportjsonfromfunctoolsimportwrapsclassCacheLayer:def__init__(self,local_cache,redis_cache,browser_storeNone):self.locallocal_cache# dict-like, 如 lru_cacheself.redisredis_cache# Redis客户端self.browserbrowser_store# 通过CDP操作浏览器存储asyncdefget(self,key:str,max_age300):# 1. 本地内存valueself.local.get(key)ifvalueandtime.time()-value[ts]max_age:returnvalue[data]# 2. Redisredis_valawaitself.redis.get(key)ifredis_val:datajson.loads(redis_val)self.local[key]{data:data,ts:time.time()}returndata# 3. 浏览器存储可选ifself.browser:browser_valawaitself.browser.get_item(key)ifbrowser_val:datajson.loads(browser_val)awaitself.redis.set(key,json.dumps(data),exmax_age)self.local[key]{data:data,ts:time.time()}returndatareturnNoneasyncdefset(self,key:str,data,ttl600):nowtime.time()self.local[key]{data:data,ts:now}awaitself.redis.set(key,json.dumps(data),exttl)ifself.browser:awaitself.browser.set_item(key,json.dumps(data)) local 使用 collections.OrderedDict 配合 LRU 淘汰算法限制最大条目数避免内存膨胀。---## 二、缓存什么三类数据的缓存策略不是所有数据都适合缓存。我们需要区分数据的类型和更新频率。**1.业务配置数据强缓存长TTL**例如运费模板列表、类目树、退货地址模板等。这些数据通常由运营手动维护变更频率极低数周甚至数月。 我们可以设置较长的缓存时间1小时甚至24小时并在运营后台修改时主动通知缓存失效。**2.平台规则数据中缓存中TTL**例如拼多多的发货时效规则、TEMU的佣金费率表。这些可能随平台政策调整但不频繁。 TTL设置为1-2小时并定时从平台公告页或API校验版本。**3.店铺运营数据弱缓存短TTL**例如商品列表、订单列表、竞品价格。这些数据随时变化但短时间内重复查询是有意义的如30秒内。 短TTL30-60秒可以有效减少同一页面重复加载。 python CACHE_POLICIES{config:shipping_template:{ttl:86400,max_age:86400},config:category_tree:{ttl:86400,max_age:43200},platform:fee_rate:{ttl:7200,max_age:3600},product:list:{ttl:60,max_age:30},order:list:{ttl:30,max_age:15},}---## 三、浏览器端缓存利用IndexedDB避免重复渲染对于商品列表、订单列表这类数据量较大、且页面渲染开销高的场景我们通过CDP操作浏览器的IndexedDB或LocalStorage将已加载的数据持久化到浏览器本地。 下一次访问同一页面时执行一段注入的JavaScript脚本先检查浏览器存储中是否有缓存数据。如果有并且未过期直接渲染缓存数据无需等待网络请求。 pythonclassBrowserCache:def__init__(self,cdp_session):self.cdpcdp_sessionasyncdefget_item(self,key:str):scriptf (async () {{ const db await window.indexedDB.open(automation_cache); // ... 读取逻辑 return cachedData; }})() resultawaitself.cdp.evaluate(script)returnresultasyncdefset_item(self,key:str,value:str):scriptf (async () {{ const db await window.indexedDB.open(automation_cache); // ... 写入逻辑 }})() awaitself.cdp.evaluate(script) 通过浏览器缓存一个店铺的商品列表在第二次打开时几乎可以瞬间显示影刀RPA后续的元素定位操作也变得更快。**实际效果竞品采集任务中第二次扫描相同关键词时页面加载时间从8秒降到1.5秒。**---## 四、缓存一致性当数据变更时如何让缓存失效缓存最大的挑战不是“怎么存”而是“怎么让缓存和源数据保持一致”。 店群自动化中很多数据变更并非通过我们的系统触发。 例如运营在手机端改了运费模板或者平台下架了某个类目。这些变更我们无法捕获事件。 我们采用“主动失效被动过期”的组合策略-**主动失效**当自动化任务执行了写操作如修改了运费模板在写操作成功后立即调用缓存失效接口删除对应的缓存键。--**被动过期**每个缓存键都有TTL过期时间即使没有主动失效过期后也会从源端重新加载。--**版本号校验**对于关键配置我们在Redis中存储一个版本号。缓存读取时先比较版本号是否一致不一致则认为缓存失效。 pythonclassCacheInvalidator:def__init__(self,redis,cache_layer):self.redisredis self.cachecache_layerasyncdefinvalidate(self,pattern:str):# 匹配删除所有相关键keysawaitself.redis.keys(pattern)ifkeys:awaitself.redis.delete(*keys)# 同时通知本地缓存forkeyinkeys:self.cache.local.pop(key,None)logger.info(fInvalidated cache keys matching{pattern})asyncdefcheck_version(self,config_type:str)-int:version_keyfversion:{config_type}versionawaitself.redis.get(version_key)returnint(version)ifversionelse0 例如运费模板配置的主键前缀是 config:shipping_template:*。 一旦有模板更新就调用 invalidate(config:shipping_template:*) 清除所有相关缓存。---## 五、缓存预热在任务高峰期前让数据就位每天凌晨是自动化任务的低峰期也是缓存预热的好时机。 我们编写了预热脚本在凌晨3点遍历所有活跃店铺提前把常用配置数据和首页数据加载到L2和L3缓存中。 pythonclassCacheWarmer:asyncdefwarmup(self,shop_ids:list):forshop_idinshop_ids:# 加载运费模板awaitself.load_and_cache(fconfig:shipping_template:{shop_id},fetch_shipping_templates,shop_id)# 加载类目树awaitself.load_and_cache(fconfig:category_tree:{shop_id},fetch_category_tree,shop_id)# 预加载首页数据awaitself.load_and_cache(fpage:home:{shop_id},fetch_home_page,shop_id)asyncdefload_and_cache(self,key,fetch_func,*args):dataawaitfetch_func(*args)awaitcache_layer.set(key,data,ttlpolicies[key.split(:)[0]][ttl]) 预热之后早上8点运营高峰期的任务几乎不会遇到缓存未命中浏览器打开店铺页面的速度明显加快。---## 六、缓存监控与容量规划缓存同样需要监控否则内存溢出或命中率过低都难以察觉。 关键指标-各级缓存的命中率L1/L2/L3--缓存键数量与内存占用--缓存未命中导致的额外延迟--缓存失效次数主动和被动 Grafana面板上展示命中率曲线。当L1命中率下降时可能是本地缓存淘汰策略过于激进需调整容量。 当L2命中率长期偏低时可能缓存策略设置不合理或数据变化过于频繁。---## 七、几个缓存实践中的细节**大对象的缓存。**商品详情页的完整HTML不适合直接缓存因为太大且包含时效性信息。我们选择只缓存数据层商品标题、价格、库存页面结构仍从浏览器加载但减少了网络请求。**并发写入的缓存更新。**多个Worker可能同时更新同一个缓存键。使用Redis的 SETNX 结合随机锁避免缓存击穿。**缓存穿透防护。**对于不存在的数据如查询不存在的类目ID我们缓存一个空值标记避免每次都穿透到后端。---## 八、写在最后缓存是性能优化中最古老也最有效的手段。 在RPA自动化场景中我们将它分别应用到进程内存、Redis和浏览器存储三个层面让“读”操作尽可能快地完成让“写”操作成为唯一需要付出网络代价的动作。 当浏览器页面秒开任务总耗时缩短一半时你会切身体会到缓存架构带来的价值。自动化不只是让机器操作得快还要让机器等待得少。---*作者林焱*