山海万灵 HarmonyOS 文化知识实战(19):MySQL、Redis、OpenSearch 与 MinIO 的内容发布同步

📅 2026/8/9 2:07:45
山海万灵 HarmonyOS 文化知识实战(19):MySQL、Redis、OpenSearch 与 MinIO 的内容发布同步
文化知识应用里的“发布”不是把一条记录改成已发布那么简单。山海万灵把内容事实、读缓存、检索投影和媒体对象拆到不同基础设施后真正需要守住的是一次发布发生后读者打开图鉴、搜索神兽、读取插画时不能看见彼此矛盾的版本。本文拆解项目中的内容发布同步链路MySQL 保存主数据与发布版本Redis 只缓存可重建的读模型OpenSearch 保存搜索投影MinIO 提供受控媒体 URL。它们各有职责也各有失败时的回退方式。图中记录的是一次隔离环境中的真实回读内容目录、Redis 缓存失效、OpenSearch 索引、MinIO 资源和发布事件返回结果均按同一条链路核对。先确定谁拥有事实内容服务不让缓存和索引反向成为事实来源。神兽条目、来源标注、发布状态和版本快照都以 MySQL 为准发布事件只在主数据事务成立后驱动后续读模型。层级保存内容可以重建吗发布后的职责MySQL神兽主档、来源标注、版本快照、发布状态否记录唯一的新版本与可发布条件Redis图鉴列表、单条详情、首页投影是删除受影响缓存让下一次读取回到主库重建OpenSearch可搜索的神兽投影是用当前已发布条目刷新索引MinIO封面、插画等媒体对象对象可重新上传生成受控的媒体访问地址这条边界解决了两个常见问题缓存过期不会丢失内容搜索索引短暂滞后也不会改写主数据。ArkTS 页面仍然通过 Repository 请求稳定的内容接口而不是分别访问数据库、缓存和对象存储。发布事件先过权限和版本校验内容服务把生命周期事件放在内部接口中并要求调用方携带内部令牌。无令牌请求会得到CONTENT.EVENT_UNAUTHORIZED不会写入发布状态这让普通客户端无法伪造发布、下线或回滚动作。事件中至少要有事件编号、类型、实体类型、实体编号、版本与发生时间。版本比较在事务内完成较旧事件不会覆盖更新版本。{ eventId: content-publish-20260808-001, eventType: content.published, entityType: BEAST, entityId: beast_baize, contentVersion: 1, publishedAt: 2026-08-08T05:45:00Z }if (!repository.record(event)) { return new EventConsumptionResult(true, false, false); } if (!publicationStateRepository.applyBeastVisibility(event, visibility)) { return new EventConsumptionResult(false, false, false); }record(event)让同一个事件编号具备幂等性applyBeastVisibility再比较内容版本和发生时间。即使投递方因为网络超时重复发送已经处理过的事件也只返回重复结果不再触发第二次状态写入。来源条件拦住“未审校即发布”内容条目不是只要存在就能公开。发布时会检查关联来源是否具有完整的校注版本、页码、审核人和审核时间并且来源核验状态必须达到ANNOTATED_VERIFIED。条件缺失时事件会被拒绝缓存与搜索不会被刷新。if (!publicationStateRepository.publishVerifiedBeastSource(event.entityId())) { throw new EventConsumptionException( CONTENT.SOURCE_NOT_PUBLISHABLE, published beast requires verified source evidence); }这一步把文化内容的真实性约束放在服务端事务里。页面上的“发布”按钮、后台任务和重放机制都走同一条判断避免某个入口绕开来源标注。发布服务把状态写入、来源条件、版本快照和读模型动作集中在一个事务入口。这里最重要的不是代码长度而是每个返回分支都对应一类可观察结果重复事件不重复写入过期事件不移动可见状态来源不合格时不刷新任何读模型。Transactional public EventConsumptionResult consume(ContentLifecycleEvent event) { validate(event); if (!publicationStateRepository.beastExists(event.entityId())) { throw new EventConsumptionException( CONTENT.EVENT_ENTITY_NOT_FOUND, published beast does not exist); } if (!repository.record(event)) { return new EventConsumptionResult(true, false, false); } ContentVisibility visibility content.offline.equals(event.eventType()) ? ContentVisibility.OFFLINE : ContentVisibility.PUBLISHED; if (!publicationStateRepository.applyBeastVisibility(event, visibility)) { return new EventConsumptionResult(false, false, false); } if (visibility ContentVisibility.PUBLISHED !publicationStateRepository.publishVerifiedBeastSource(event.entityId())) { throw new EventConsumptionException( CONTENT.SOURCE_NOT_PUBLISHABLE, verified source is required); } boolean cacheInvalidated cache.invalidatePublishedBeast(event.entityId()); boolean searchIndexed searchService.refreshPublishedBeasts(); return new EventConsumptionResult(false, cacheInvalidated, searchIndexed); }Redis 只做加速不保存发布结论图鉴列表、详情和首页投影都有独立缓存键。发布事件成功后服务只删除与该条目相关的缓存键列表、首页和单条详情会在下一次读取时从 MySQL 重建。ListString keys new ArrayList(List.of( BEASTS_KEY, HOME_PROJECTION_KEY, beastKey(beastId) )); redisTemplate.delete(keys);如果 Redis 临时不可用缓存删除返回失败但发布主事务不回滚。后续读取仍可直接访问 MySQL缓存恢复后再按 TTL 重建。这种降级把“性能层故障”和“内容事实故障”分开处理。OpenSearch 刷新的是已发布投影搜索服务不会把草稿直接送进索引。content.published成功后刷新已发布神兽content.offline则移除对应条目。隔离环境回读中OpenSearch 集群为 green索引能够返回beast_baize与发布事件的searchIndexedtrue结果一致。boolean searchIndexed visibility ContentVisibility.OFFLINE ? searchService.removeBeastFromIndex(event.entityId()) : searchService.refreshPublishedBeasts();索引失败时不把旧索引伪装成新结果。内容接口仍从 MySQL 提供已发布条目运维侧可以根据 Outbox 记录重试投递待索引恢复后重新建立投影。MinIO 把媒体访问与内容主档解耦媒体表只保存对象桶、对象键、MIME 类型、尺寸、版权类型与发布状态不把二进制内容塞进内容主表。内容接口依据这些元数据返回可访问 URL隔离环境中资源900001的 URL 已成功回读。场景返回策略对象存在且资源已发布返回受控媒体 URL 与资源元数据对象暂时不可读返回明确的资源降级结果不制造空 URL条目下线不继续作为已发布资源出现在内容接口中缓存或索引异常媒体元数据仍由 MySQL 的发布状态约束这样插画替换、缩略图补齐或对象迁移不会要求修改神兽正文媒体系统也不能脱离发布状态独自暴露草稿资源。public record ContentAssetFile( String assetId, String bucketName, String objectKey, String mimeType, String status ) {}不把同步逻辑散落到页面如果由 ArkTS 页面在“保存成功”后分别调用清缓存、刷新搜索和拼接媒体地址任何一个请求超时都会留下半完成状态详情已经更新搜索仍是旧名称正文已经可读插画链接却失效。把这些动作留在服务端事件处理器中前端只关心稳定的内容响应和加载、空态、错误态即可。这也让移动端的状态恢复更简单。应用重新进入前台时Repository 再次读取图鉴或搜索接口得到的是服务端已收束后的结果而不是根据本地按钮点击历史推测某一项同步是否完成。ArkTS 的类型和状态模型适合承接这种明确的接口边界页面不需要了解 Redis 键名、索引名称或对象桶路径。ArkTS 开发入门 也强调以清晰类型和模块边界组织应用逻辑。对于后台任务Outbox 记录提供了另一层保护主事务成功后才有待投递事件投递失败可以按事件编号重放接收方的幂等记录又阻止重复生效。这样即使 CMS 与内容服务短暂断连也不会靠“再点一次发布”解决问题。一次可回读的发布动作本次运行按以下顺序完成启动 MySQL、Redis、OpenSearch 与 MinIO读取内容目录以建立缓存发送无令牌事件确认权限拒绝补齐可发布的来源标注后发送content.published回读事件结果、OpenSearch 文档和 MinIO 资源 URL。回读项结果内容目录成功返回 18 条内容无令牌事件返回CONTENT.EVENT_UNAUTHORIZED已授权发布事件duplicatefalse缓存失效与索引刷新均成功OpenSearch回读到beast_baize文档MinIO健康接口可用资源 URL 可返回这组回读覆盖了发布同步最容易断裂的边界主数据、权限、缓存、搜索和媒体。后续新增 CMS 发布入口时仍然应通过 Outbox 投递同一种生命周期事件而不是在页面层分别清缓存和改索引。public record EventConsumptionResult( boolean duplicate, boolean cacheInvalidated, boolean searchIndexed ) {}小结山海万灵把“发布”落实为一条可验证的数据链MySQL 先确认内容和来源事件记录保证幂等Redis 失效避免旧读模型OpenSearch 重建搜索投影MinIO 按发布状态提供媒体地址。读者看到的是一致的图鉴、搜索与插画结果服务端保留的是可回放、可排错的同步边界。参考资料Spring Data Redis 文档、OpenSearch 文档、MinIO Java SDK 文档。