perf: 优化快照接口——缓存预序列化 JSON,消除重复序列化
- Caffeine 缓存改为存储 bundle JSON 字符串而非 SiteSnapshot 对象(减少内存占用) - 新增 getActiveBundleJson 方法,SnapshotController 直接使用缓存 JSON - bundle JSON 新增 checksum 字段,ETag 比较无需额外查库 - 删除 SnapshotController.toJson 重复序列化代码 - publish 时复用已序列化的 JSON 上传 CDN
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.par.api.controller;
|
package com.par.api.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.par.core.dto.ApiResponse;
|
import com.par.core.dto.ApiResponse;
|
||||||
import com.par.core.entity.SiteSnapshot;
|
import com.par.core.entity.SiteSnapshot;
|
||||||
@@ -32,7 +33,7 @@ public class SnapshotController {
|
|||||||
/**
|
/**
|
||||||
* 获取站点当前生效配置快照(供 mediabot 使用)
|
* 获取站点当前生效配置快照(供 mediabot 使用)
|
||||||
* 支持 ETag (If-None-Match) 实现增量更新
|
* 支持 ETag (If-None-Match) 实现增量更新
|
||||||
* 优先从数据库读取,数据库不可用时回退到七牛云 CDN
|
* 优先从 Caffeine 缓存读取,缓存未命中查数据库并回退七牛云 CDN
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{siteId}")
|
@GetMapping("/{siteId}")
|
||||||
public ResponseEntity<?> getSnapshot(
|
public ResponseEntity<?> getSnapshot(
|
||||||
@@ -40,9 +41,9 @@ public class SnapshotController {
|
|||||||
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
|
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SiteSnapshot active = snapshotService.getActive(siteId);
|
String bundleJson = snapshotService.getActiveBundleJson(siteId);
|
||||||
|
|
||||||
if (active == null) {
|
if (bundleJson == null || bundleJson.isEmpty()) {
|
||||||
// 尝试从云存储 CDN 回退
|
// 尝试从云存储 CDN 回退
|
||||||
String cached = storageService.read("snapshots/" + siteId + ".json");
|
String cached = storageService.read("snapshots/" + siteId + ".json");
|
||||||
if (cached != null && !cached.isBlank()) {
|
if (cached != null && !cached.isBlank()) {
|
||||||
@@ -53,18 +54,18 @@ public class SnapshotController {
|
|||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ETag 检查
|
// ETag 检查:从 JSON 中提取 checksum
|
||||||
if (ifNoneMatch != null && ifNoneMatch.equals(active.getChecksum())) {
|
String etag = extractChecksum(bundleJson);
|
||||||
|
if (ifNoneMatch != null && etag != null && ifNoneMatch.equals(etag)) {
|
||||||
return ResponseEntity.status(304)
|
return ResponseEntity.status(304)
|
||||||
.header(HttpHeaders.ETAG, active.getChecksum())
|
.header(HttpHeaders.ETAG, etag)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
String json = toJson(active);
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.ETAG, active.getChecksum())
|
.header(HttpHeaders.ETAG, etag)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(json);
|
.body(bundleJson);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取快照失败 siteId={}", siteId, e);
|
log.error("获取快照失败 siteId={}", siteId, e);
|
||||||
@@ -99,39 +100,13 @@ public class SnapshotController {
|
|||||||
return ApiResponse.success(result);
|
return ApiResponse.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toJson(SiteSnapshot snapshot) {
|
/** 从 bundle JSON 中提取 checksum 字段,用于 ETag 比较 */
|
||||||
|
private String extractChecksum(String bundleJson) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> bundle = new LinkedHashMap<>();
|
JsonNode node = om.readTree(bundleJson);
|
||||||
bundle.put("siteId", snapshot.getSiteId());
|
return node.has("checksum") ? node.get("checksum").asText() : null;
|
||||||
bundle.put("generatedAt", snapshot.getPublishedAt() != null
|
|
||||||
? snapshot.getPublishedAt().toString()
|
|
||||||
: java.time.LocalDateTime.now().toString());
|
|
||||||
|
|
||||||
if (snapshot.getScript() != null) {
|
|
||||||
bundle.put("script", snapshot.getScript());
|
|
||||||
bundle.put("scriptVersion", snapshot.getScriptVersion());
|
|
||||||
bundle.put("scriptSource", snapshot.getScriptSource());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.getPages() != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var pages = om.readValue(snapshot.getPages(), java.util.List.class);
|
|
||||||
bundle.put("pages", pages);
|
|
||||||
bundle.put("pagesVersion", snapshot.getPagesVersion());
|
|
||||||
bundle.put("pagesSource", snapshot.getPagesSource());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.getRules() != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var rules = om.readValue(snapshot.getRules(), java.util.List.class);
|
|
||||||
bundle.put("riskRules", rules);
|
|
||||||
bundle.put("riskRulesVersion", snapshot.getRulesVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
return om.writeValueAsString(bundle);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("序列化快照失败 siteId={}", snapshot.getSiteId(), e);
|
return null;
|
||||||
return "{}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ public interface SiteSnapshotService {
|
|||||||
*/
|
*/
|
||||||
SiteSnapshot getActive(String siteId);
|
SiteSnapshot getActive(String siteId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取站点当前生效快照的预序列化 JSON(带 Caffeine 缓存)
|
||||||
|
* 供 SnapshotController 使用,避免重复序列化
|
||||||
|
* @return JSON 字符串,或 null 表示无快照
|
||||||
|
*/
|
||||||
|
String getActiveBundleJson(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 checksum 判断是否需要更新
|
* 按 checksum 判断是否需要更新
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
private final CachedStorageService storageService;
|
private final CachedStorageService storageService;
|
||||||
private static final ObjectMapper om = new ObjectMapper();
|
private static final ObjectMapper om = new ObjectMapper();
|
||||||
|
|
||||||
/** 已发布快照本地缓存:key=siteId, value=SiteSnapshot */
|
/** 已发布快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
|
||||||
private final Cache<String, SiteSnapshot> activeSnapshotCache = Caffeine.newBuilder()
|
private final Cache<String, String> bundleJsonCache = Caffeine.newBuilder()
|
||||||
.expireAfterWrite(Duration.ofMinutes(10))
|
.expireAfterWrite(Duration.ofMinutes(10))
|
||||||
.maximumSize(500)
|
.maximumSize(500)
|
||||||
.build();
|
.build();
|
||||||
@@ -193,12 +193,12 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
draft.setPublishedAt(LocalDateTime.now());
|
draft.setPublishedAt(LocalDateTime.now());
|
||||||
snapshotMapper.updateById(draft);
|
snapshotMapper.updateById(draft);
|
||||||
|
|
||||||
// 刷新本地缓存
|
// 序列化并刷新缓存
|
||||||
activeSnapshotCache.put(siteId, draft);
|
String bundleJson = toBundleJson(draft);
|
||||||
|
bundleJsonCache.put(siteId, bundleJson);
|
||||||
|
|
||||||
// 上传到七牛云 CDN 作为灾备
|
// 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON)
|
||||||
try {
|
try {
|
||||||
String bundleJson = toBundleJson(draft);
|
|
||||||
String cdnPath = "snapshots/" + siteId + ".json";
|
String cdnPath = "snapshots/" + siteId + ".json";
|
||||||
storageService.store(cdnPath, bundleJson);
|
storageService.store(cdnPath, bundleJson);
|
||||||
// 也保存版本化副本
|
// 也保存版本化副本
|
||||||
@@ -217,9 +217,19 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SiteSnapshot getActive(String siteId) {
|
public SiteSnapshot getActive(String siteId) {
|
||||||
return activeSnapshotCache.get(siteId, key -> {
|
return snapshotMapper.selectLatestPublished(siteId);
|
||||||
log.debug("缓存未命中,查询数据库 siteId={}", siteId);
|
}
|
||||||
return snapshotMapper.selectLatestPublished(siteId);
|
|
||||||
|
@Override
|
||||||
|
public String getActiveBundleJson(String siteId) {
|
||||||
|
return bundleJsonCache.get(siteId, key -> {
|
||||||
|
SiteSnapshot active = snapshotMapper.selectLatestPublished(key);
|
||||||
|
if (active == null) {
|
||||||
|
log.debug("站点 {} 无 published 快照", key);
|
||||||
|
return ""; // 空字符串表示无数据,Caffeine 会缓存
|
||||||
|
}
|
||||||
|
log.debug("缓存未命中,序列化 siteId={}", key);
|
||||||
|
return toBundleJson(active);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +263,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
try {
|
try {
|
||||||
Map<String, Object> bundle = new LinkedHashMap<>();
|
Map<String, Object> bundle = new LinkedHashMap<>();
|
||||||
bundle.put("siteId", snapshot.getSiteId());
|
bundle.put("siteId", snapshot.getSiteId());
|
||||||
|
bundle.put("checksum", snapshot.getChecksum());
|
||||||
bundle.put("generatedAt", snapshot.getPublishedAt() != null
|
bundle.put("generatedAt", snapshot.getPublishedAt() != null
|
||||||
? snapshot.getPublishedAt().toString() : LocalDateTime.now().toString());
|
? snapshot.getPublishedAt().toString() : LocalDateTime.now().toString());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user