从数据库到缓存、消息与搜索:围绕访问需求演进数据链路
按商品编号读取详情与按价格筛选列表,需要考虑不同的查询路径。数据库索引可以减少扫描;反复读取相同结果时,缓存又能减少重复取数。按描述检索商品时,还可能需要分词和相关度排序;按类别汇总则会用到聚合。提交后发送通知可以借助消息,把后续处理与原请求的等待分开。
技术选择从这些访问模式开始。一个查询增加合适索引后已经满足需求,就可以停在这里;缓存、消息和搜索没有必然的引入顺序。新增派生副本时,还要安排取源、允许的延迟、删除处理和丢失后的重建。
从访问模式和真实执行计划开始
区分需要改变的访问路径
| 当前需求 | 可以先检查的方案 | 引入后的主要维护工作 |
|---|---|---|
| 精确过滤、关联和排序变慢 | SQL、索引、统计信息、分页方式 | 索引写放大、计划变化、锁和资源等待 |
| 大量重复读取相同结果 | 结果缓存、应用内缓存、CDN | 键设计、失效、穿透和热点、允许的陈旧时间 |
| 读取占满主库,而查询仍适合关系模型 | 读副本、报表库或预计算结果 | 复制延迟、读己之写、故障时的路由 |
| 请求要等待可延后完成的任务 | 队列、任务状态、消费者 | 积压、重试、重复、毒消息与最终结果查询 |
| 需要分词、相关度、模糊匹配和聚合 | 专门搜索索引 | Mapping、分析器、写入可见性与重建 |
| 数据规模超出单个存储单元的合理范围 | 分区、归档或分片 | 路由、跨片查询、全局约束与迁移 |
先记录实际查询条件、返回行数、访问频率、数据分布和用户可接受的结果时效。一个详情接口在数据库中只花几毫秒,却在连接池等待较长时间,优化 SQL 本身可能收益有限;导出百万行即使使用索引,也仍要读取、编码和发送大量数据。应沿等待位置分别观察连接池、数据库执行、网络和应用处理。
结果稳定性同样需要先定义。展示商品描述可以接受短暂落后,扣减库存时则要在保存库存的地方重新检查可用数量;搜索列表中的“有货”不能直接替代下单事务里的库存条件。
启动四个独立的教学服务
下载数据访问与派生副本实验工程,在 Linux Bash 中进入解压后的 evolution-data-lab。需要 Docker Engine、Compose v2、curl,以及可运行 Docker 的普通用户。先构建,再启动服务,预留约 4 GiB 的实验内存;首次下载镜像还需要访问各产品的官方注册表。
实验使用 PostgreSQL 18.6、Redis 8.10.1、RabbitMQ 4.3.5 和 OpenSearch 3.8.0。搜索命令和输出均针对 OpenSearch,不把其他兼容产品的文档当作该版本的运行结果。Java 源码目标为 17,构建与默认 CLI 镜像为 Maven 3.9.12 / Temurin 17,实际 Java 为 17.0.18+8;工程也支持 Maven 的 Temurin 25 镜像。
set -euo pipefail
export LAB_PROJECT=evo25data
bash build.sh
docker compose -p "$LAB_PROJECT" up -d --wait
lab() {
docker compose -p "$LAB_PROJECT" run --rm \
--user "$(id -u):$(id -g)" lab "$@"
}
lab preflight构建缓存位于解压目录 .m2,构建和 CLI 都显式使用宿主 UID/GID。PostgreSQL 初始化管理员创建实验账号,应用以 evo25app 登录,仅在专用 Schema 中进行教学建表与查询;它没有超级用户或 BYPASSRLS 权限。这里为了运行建表实验授予了 Schema 管理权限,生产业务账号通常应与迁移账号分开。
PostgreSQL、Redis 和 RabbitMQ 没有宿主端口。OpenSearch 的安全插件仅在这个本地单节点实验中关闭,并且只映射 127.0.0.1:29200。固定口令都是公开演示值,不能用于其他系统。
preflight 读取所有服务的实际版本、数据库角色、队列和本组索引数量,不创建或清空业务数据。未创建队列时数量为 -1。先确认这些参与者都可达,再进入具体场景;Maven 的 3 个局部测试只检查值对象、摘要和事件解析,不替代后面的数据库与消息实验。
用相同结果比较索引前后
lab plan该命令在真实 PostgreSQL 中生成十万行固定商品数据,比较这条查询的执行计划:
SELECT id
FROM plan_product
WHERE NOT deleted
AND price_cents BETWEEN 50000 AND 50100
ORDER BY price_cents, id
LIMIT 20;初始表只有主键索引,按价格筛选需要扫描并排序。随后建立与过滤及排序相匹配的部分复合索引:
CREATE INDEX evo25_plan_price
ON plan_product(price_cents, id)
WHERE NOT deleted;
ANALYZE plan_product;CLI 输出三份 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON):没有复合索引、建立索引后,以及返回大部分有效商品的查询。两次实际 JDK 对照中,选择性查询从 Seq Scan 加排序变为 Index Only Scan;返回大部分商品的查询仍选择 Seq Scan。索引前后取得的 20 个有序 ID 完全相同,最后删除并重建这个实验索引,再次确认结果不变。
读取计划时,从下层扫描节点向上看过滤、排序和限制。比较估计行数与实际行数,观察循环次数、过滤掉的行以及缓存页访问。cost 是优化器估算单位,不能当成毫秒;ANALYZE 会实际执行语句,检查修改类语句时尤其要控制副作用。PostgreSQL EXPLAIN 文档解释了这些字段。
本次 Index Only Scan 的第一轮仍有 Heap Fetches=20。覆盖了查询列以后,PostgreSQL 仍可能为可见性检查访问堆;是否能够避免这些访问还与可见性映射有关。索引仅扫描文档说明了条件。
返回大部分表内容时,逐项走索引再取数据可能比顺序扫描更昂贵。选择率之外,列顺序、排序方向和写入成本也影响索引的价值。实验保留优化器对顺序扫描的选择,不设固定加速倍数;数据分布与统计信息变化后,重新读计划才能判断收益。
读副本、分区和分片改变不同对象
读副本承接仍然适合 SQL 的读取,复制延迟却可能使刚提交的数据暂时不可见。需要读己之写的路径,可以继续读主库,或用适合该复制系统的进度条件等待;时间固定的“写后几秒走主库”只是一种经验窗口,无法覆盖所有延迟变化。PostgreSQL 热备文档也说明了恢复过程对查询的影响。
分区把一张逻辑表分成可管理的部分,常用于裁剪扫描范围和分批保留数据;它仍需要与查询条件匹配。分片则把数据路由到多个存储单元,进一步引入跨片查询、全局唯一约束、再均衡和跨片事务问题。按租户分片便于组织级搬迁,但超大租户可能成为单片热点;按其他业务键散列可以分散负载,却会改变关联查询成本。分区的具体限制见PostgreSQL 分区文档。
缓存命中之外,还要控制回填和失效
常用缓存方式与键的范围
Cache-Aside 由应用先查缓存,未命中再查源存储并回填;写操作通常先提交源数据,再推进失效。Read-Through 将取源逻辑封装在缓存访问层。Write-Through 把写入经过缓存协调层,但仍需知道源库与缓存怎样处理部分失败。Write-Behind 先接受缓存中的写入,随后异步保存,必须额外处理数据丢失、积压和恢复,不能直接承接原本承诺立即持久化的业务操作。
键需要包含会改变结果的维度:租户、实体 ID、版本或参数;缓存已经做过权限裁剪的响应时,还要考虑用户授权范围。TTL 表示可保留多久,不表达源状态何时变化。热点键可以使用请求合并、提前刷新和有限的过期抖动,避免大量请求同时回源;不存在的资源可以短时缓存明确的空值,但创建该资源后也要失效。
应用内一级缓存能减少网络访问,却把副本数量增加到每个进程。进程重启、广播失效丢失或升级期间的新旧值格式,都需要独立处理。缓存击穿、穿透与雪崩的详细配置见缓存失效与热点保护;在引入新副本之前,先确定它失效或不可用时请求怎样回到源存储,并为回源设置容量保护。
精确复现旧读回填
lab cache命令使用两个真实数据库连接和受控等待,固定下列顺序:
A 读取源库 v1,暂不回填
↓
B 提交源库 v2,并删除缓存值
↓
A 恢复执行,把先前读到的 v1 写进缓存
↓
源库 v2 / 缓存 v1这解释了“更新数据库后删除缓存”仍可能产生的旧值窗口:删除完成时,另一个读请求手里已经拿到了旧版本。把两条语句简单调换顺序,会出现其他部分失败和并发窗口,无法从根本上消除两个系统之间的协调问题。
工程分别运行朴素回填、保留版本下界和丢失下界三种独立场景:
cache-naive old_read=1 source_after_write=2 fill=NAIVE_FILLED repaired_next_version=3
cache-guard old_read=1 source_after_write=2 fill=STALE_REJECTED repaired_next_version=3
cache-lost old_read=1 source_after_write=2 fill=FLOOR_MISSING repaired_next_version=3朴素路径从 Redis 读到 v1 时,源库已经是 v2。后两条路径拒绝了旧回填。三个场景随后各自从当前源状态恢复,新的提交和读取都到达 v3。
把版本下界与缓存值分开保存
受保护路径为同一实体保存 floor 和 value 两个键。B 提交 v2 后,用一个 Lua 脚本推进下界并删除旧值;A 回填前,另一个脚本原子比较下界与现存值。Redis 脚本的原子执行范围见官方脚本说明。
evo25data:{cache-guard:p1}:floor = 2 无 TTL
evo25data:{cache-guard:p1}:value = 完整状态 TTL 60 秒
回填输入版本 < floor → STALE_REJECTED
floor 不存在 → FLOOR_MISSING
现存 value 同版本但内容摘要不同 → CONTENT_CONFLICT
满足比较条件 → 保存 value两个键采用相同 Redis hash tag,为需要同槽执行的场景保留一致的键定位。版本被显式限制在 1..1,000,000,000,与该 Lua 数字比较方式的精度范围相容;不能把相同写法直接推广到任意 64 位整数版本。
下界缺失时不能把“查不到版本”解释为接受任意旧值。本例用精确 DEL 丢失下界,回填返回 FLOOR_MISSING 并保持缓存为空;恢复时读取当前源行,在受控的共享行锁下建立下界与值,再继续后续写入。实际服务可以临时回源或关闭该缓存路径,避免在信息不足时继续填旧数据。
下界只有版本,没有永久保存每个版本的摘要。同版本内容冲突只在现存 value 还可比较时拒绝;value 过期或被删除后,单靠 floor 无法识别同版本不同载荷。源系统需要保证版本对应的内容稳定,不能把可失效的缓存当作完整的历史冲突账本。
当前 Redis 实验关闭持久化并采用 noeviction,重启会丢掉下界。即使启用了持久化,源库提交与 Redis 推进下界之间仍存在窗口。对库存、余额或权限等必须即时检查的条件,应在负责保存该状态的系统中完成判断;该缓存方案只缩小特定回填窗口,不提供跨库线性一致。Redis 持久化方式及丢失条件见官方持久化说明。
用持久事件连接异步任务和搜索状态
一次源更新保存哪些信息
异步任务适合通知、索引更新和可延后完成的处理。请求返回“已接受”时,客户端还需要操作 ID、当前状态和结果查询入口。若任务在响应前已经完成,则可以返回完成结果;进入队列时应继续标记为等待处理。
将源数据保存和消息发送分成两个普通调用,会留下源库已提交但消息未发出的窗口。当前工程在同一个 PostgreSQL 事务中保存商品状态与 Outbox 记录:
product
├─ scope + id:独立实验场景中的商品身份
├─ version:该商品单调增加的源版本
├─ description / price_cents:业务内容
└─ deleted:业务删除标记
outbox
├─ event_id:本次变更的消息身份
├─ scope + product_id + source_version:对应源状态
├─ state:包含 deleted 与内容摘要的完整状态
└─ sent:发布器是否已经登记发送结果源更新使用期望版本作为条件,更新成功后才插入事件。事务提交使这两项共同保存;回滚则都不保留。event_id 区分变更,version 比较同一商品的新旧程度,payload_hash 检查完整内容是否一致。
这里发送完整状态,例如“价格现在为 120 分,版本为 2”。它允许消费者在已有 v3 时跳过迟到 v2。若消息表达的是“余额增加 20”或“库存减少 1”,跳过旧消息会漏掉业务动作,应使用符合增量语义的顺序、去重和缺口处理。选择事件格式会直接改变恢复算法。
Outbox 可以由轮询发布器读取,也可以通过 CDC 把变更接到日志流;两种方式都需要保存可恢复进度、处理坏记录,并保证业务写入确实走过已纳入捕获的路径。当前附件采用受控发布命令,不提供通用后台 CDC 服务。
发布确认以后仍可能重复
保持在前面的 Bash 终端,执行指定故障:
set +e
lab outbox-gap
exit_code=$?
set -e
test "$exit_code" -eq 23
lab inspectoutbox-gap 提交商品 v1 和 Outbox,向真实 RabbitMQ 队列发送持久消息并取得 publisher confirm,随后在登记 sent 之前退出。退出码必须为 23;其他非零退出属于启动、连接或实现错误,不应继续按这个故障解释。
inspect 此时应显示源版本 1、sent=false 和队列中 1 条消息。消息已经到达 Broker,发布器的本地记录却仍表示需要发送。再次发送可以补偿未知结果,也会产生重复,因此消费者必须能识别重复内容。
lab outbox-recover新 JVM 使用原来的 event_id、版本和内容重发,队列先变为两条。消费者第一次更新 OpenSearch 后主动关闭真实通道,不发送 ack;RabbitMQ 随后重新投递这条未确认消息。输出中的关键部分为:
consumer_write=APPLIED ack=false; closing actual channel
deliveries=3 redeliveries=1 same_event_ids=1 decisions=[APPLIED, DUPLICATE, DUPLICATE]
publisher_confirm=true basic_return=312 NO_ROUTE前三次处理分别是一条首次应用和两条相同版本、相同内容的重复。redeliveries 来自真实消息 envelope;两次发布和一次消费重投由不同原因形成。发布确认与消费者确认的职责见 RabbitMQ Confirms 文档。
路由失败、消费者失败和确认顺序
恢复命令还实际创建 v2,并故意发往不存在的 routing key。发布使用 mandatory=true,因此收到 basic.return=312 NO_ROUTE;Broker 仍可能给出 publisher confirm。当前发布器只有在确认成功且没有 return 时才登记 sent。修正路由后重新发送,搜索中查到 v2,队列最终为零。
发布确认表示 Broker 已按相应队列语义处理发布,不说明消费者已完成业务,也不保证消息被路由到预期队列。消费 ack 应放在业务副作用成功之后;先 ack 再处理,进程消失时消息可能已经无法重投。单节点 durable classic queue 加持久消息也没有验证多副本灾难恢复,队列类型与复制策略需要按正式可靠性要求选择。
源业务 + Outbox 提交 → 发布到 Broker
├─ 发布器:confirm / return 检查 → 登记 sent
└─ 消费者:更新目标 → consumer ack发布端登记和消费端处理各自推进,消费者可能在发布器完成 sent 登记之前就开始执行。每个箭头之间都可能断开。消费者已经写入目标而 ack 丢失时,重新投递需要重取目标状态来判断;多写一个本地“已处理表”,如果它与搜索写入不在同一事务中,也会出现同样的先后窗口。这里直接依靠搜索目标中的严格版本和完整内容比较控制重复。
临时连接失败可以有限重试;格式错误、Mapping 拒绝和同版本不同内容应保留原消息并转入明确的检查流程,避免立即重入同一队列形成热循环。积压要同时观察数量、最旧等待时间和处理速率;单个大任务可能比大量小消息更占资源。
当前 outbox-recover 只接受上述已完成 gap 的具体前置状态。若它中途再次失败,sent、队列数量或索引可能已经改变,原入口会拒绝继续并保留数据。应先使用 inspect 和具体队列、文档状态检查;不能把这个教学恢复入口当作任意中断点可自动重入的生产发布器。
搜索 Mapping、版本与内容冲突
搜索索引是按查询需要组织的派生数据。精确筛选字段通常使用 keyword 或数值类型,全文检索字段使用适合语言的分析方式;字段类型和分析器决定能够执行哪些查询。当前工程显式创建 dynamic=strict Mapping,商品 ID 为 keyword,描述为 text,价格和版本为 long,并关闭数字强制转换。OpenSearch 创建索引 API提供了设置与 Mapping 的入口。
lab search该场景在 OpenSearch 中依次写入 v1、v2,重放旧 v1 和相同 v2,再发送同版本但不同价格的内容。真正的索引请求采用:
PUT /evo25data-version-v1/_doc/p1?version=2&version_type=external&refresh=wait_for严格外部版本只接受比已有版本更高的写入;相等或更低会返回版本冲突。external_gte 的相等版本可写语义不同,这里不使用它。具体参数见 OpenSearch Index Document API。
收到 409 后,客户端先确认错误类型,再从具体索引读取当前文档:
| 当前状态与输入的关系 | 当前消费者决定 |
|---|---|
| 当前版本高于输入版本 | 完整旧状态已被覆盖,记为 STALE |
| 版本相等,全部业务字段、删除标记及摘要相同 | DUPLICATE,接受为同内容重投 |
| 版本相等但内容不同,或其他无法解释的关系 | VERSION_CONTENT_CONFLICT,保留错误 |
场景中的冲突价格 999 没有覆盖已经保存的 v2。接着将字符串写入价格字段,OpenSearch 实际返回 400 mapper_parsing_exception,该坏文档不存在;随后合法 v3 写入和查询成功。连接异常或任意其他 409 都不会被统一吞成重复。
refresh=wait_for 让这组写入在后续搜索可见时再继续,便于检查。正式高吞吐写入应结合批量大小、刷新频率和查询时效调整,不必对每条写入强制刷新。写入响应、按 ID 取文档和搜索查询的可见性要求应分别考虑。OpenSearch Refresh API说明了刷新与搜索的关系。
删除也要进入版本和重建
如果直接物理删除搜索文档,之后的旧事件可能在目标端缺少持续版本记录时重新创建它。当前模型把删除保存为更高源版本的业务墓碑:仍有商品 ID、版本和 deleted=true,查询时过滤掉它。
search 场景保存 v4 墓碑后,普通搜索得到零个有效商品,直接按 ID 读取仍可检查 v4;迟到 v1、v3 都被拒绝。把包含墓碑的源状态回填到第二个索引后,重放旧 v1 仍无法让商品复活。重新上架则由源系统产生 v5,而不是把旧消息改成“再次有效”。
墓碑保存哪些字段、保存多久,应结合事件最大保留和重放时间、审计需求及数据删除要求决定。摘要只是内容一致性比较,不能替代安全签名;清理敏感业务字段时,也要同步调整墓碑及摘要规则。
重建查询副本,并为切换保留返回路径
快照和增量怎样交错
更换 Mapping、分析器或索引布局时,通常需要创建新索引再回填。源数据在回填期间仍可能更新,因此先读到的快照值与后来的事件会交错到达。新索引如果按到达顺序无条件覆盖,就可能被最后回填的旧快照降回旧版本。
lab rebuild场景创建旧索引 evo25data-rebuild-v1、新索引 evo25data-rebuild-v2 和查询别名 evo25data-products。两个商品先处于 v1。随后在 PostgreSQL REPEATABLE READ 事务中读取有限快照,另一连接把 p1 更新为 v2、把 p2 删除为 v2;原快照再次读取仍是两个 v1。快照行为见PostgreSQL 事务隔离文档。
源快照:p1 v1 / p2 v1
新变更:p1 v2 / p2 v2 deleted
↓ 先写新索引
新索引:p1 v2 / p2 v2 deleted
↑ 再到达的 v1 快照被严格版本拒绝接着逐项重放这次有限实验保存的四个事件,并比较源与两个索引中的全部字段,包括删除标记和内容摘要。普通查询虽然只返回 p1,p2 的墓碑仍参加重建核对。两边“可见商品都是一条”不足以发现 p2 是否丢失删除状态。
这个过程有一个明确限制:生产者与事件集合受控,四个事件可以逐项列出并确认完成。生产 CDC 需要确定快照对应的日志起点、分区或流进度,以及缺口处理方式。最大事件 ID 可能越过尚未提交或尚未处理的较小 ID;它不能自动成为连续完成水位。对持续更新的数据源,切换前还需要明确的追平条件或短暂协调窗口。
别名切换只改变查询入口
两边数据核对完成后,工程通过一次 _aliases 请求移除旧目标并添加新目标,检查返回确认和实际别名指向。这样的多个别名动作可以原子执行,具体 API 见 OpenSearch Manage Aliases。
{
"actions": [
{
"remove": {
"index": "evo25data-rebuild-v1",
"alias": "evo25data-products",
"must_exist": true
}
},
{
"add": {
"index": "evo25data-rebuild-v2",
"alias": "evo25data-products"
}
}
]
}原子的是别名配置变更,数据同步在之前完成;已经发出的查询也不会因此被取消。查询别名、消费者实际写入的具体索引,以及可选的写别名要分别管理。一个查询名字切到新索引,并不会自动让所有消费者同时开始写两个索引。
用真实新写入检查回切资格
实验切到新索引后,暂时停止向旧索引投影,再提交 p1 v3。此时新侧是 v3、旧侧仍是 v2。尝试核对旧侧时得到 PROJECTION_MISMATCH,工程保留当前别名,不进行回切。
切新前:旧索引 p1 v2 / 新索引 p1 v2
切新后:旧索引 p1 v2 / 新索引 p1 v3
↓
回切检查:旧索引缺少新写入,拒绝切回
↓
补齐旧索引 v3 → 全字段核对 → 别名切回旧索引
↓
再提交 v4,两侧更新,别名查询返回 v4最终可以通过回环 HTTP 检查当前查询入口:
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 3 --max-time 15 \
'http://127.0.0.1:29200/_alias/evo25data-products'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 3 --max-time 15 \
-H 'Content-Type: application/json' \
-d '{"query":{"term":{"deleted":false}},"track_total_hits":true}' \
'http://127.0.0.1:29200/evo25data-products/_search'别名应指向 evo25data-rebuild-v1,有效商品为 p1,版本 4、价格 140 分。搜索响应还要检查 timed_out、失败分片数和实际命中字段;CLI 已逐项检查这些信息,并拒绝截断的结果集合。多节点或大量数据时,要使用合适的分页与全量核对方式,不能把一次默认大小的搜索结果当作全集。
保留旧索引只是回切的起点。观察期间可以持续双投影,或者保留足够事件供回切前补齐;两种方式都要承担资源与延迟成本。旧查询完全退役、恢复窗口结束且不再需要其状态后,才删除已确认的具体索引。
让每一种失败有自己的处理入口
| 现象 | 先检查的对象 | 可采取的处理与后续验证 |
|---|---|---|
| SQL 仍然慢 | 实际扫描、排序、连接等待、返回行数 | 调整适配查询的索引或访问方式,再比较相同结果 |
| 缓存反复出现旧值 | 源版本、回填顺序、失效和下界是否丢失 | 暂时回源,重建可信下界,再发一次新更新 |
| Outbox 未发送但队列已有消息 | confirm 与 sent 之间的中断记录 | 按同一事件身份补发,检查目标重复处理 |
| confirm 成功但消费者没消息 | mandatory return、exchange、binding、routing key | 修复路由后重发,确认目标新版本和队列变化 |
| 消费不断失败 | 原消息、目标类型错误、版本内容冲突 | 隔离可疑记录,修复格式或目标配置后受控重放 |
| 搜索比源库旧 | Outbox、投影进度、refresh、实际写目标 | 修复失败环节,检查具体实体版本而非只看总数 |
| 重建后删除对象重新出现 | 源删除记录、墓碑回填、旧事件到达顺序 | 补齐删除版本,重放迟到事件确认没有复活 |
| 回切后丢失新结果 | 旧索引是否继续同步、回切前的字段差异 | 停止回切,补齐旧侧,核对后再切并验证新写入 |
inspect 按场景输出源状态与事件。只有 outbox 场景经过 RabbitMQ 发布并更新 sent;缓存、搜索和重建场景保留的完整事件用于各自对照,没有通用后台发布器。看到它们的 sent=false 时,不应把这些独立实验记录混成同一个队列的积压。
lab inspect
docker compose -p "$LAB_PROJECT" stop停止保留数据卷。某一场景再次初始化会拒绝 SCENARIO_EXISTS 并保留现场;不要把旧数据导致的拒绝当作版本不兼容。完整 Java 25 对照使用新的解压目录和新的 Compose 项目名,构建设置 BUILD_IMAGE=maven:3.9.12-eclipse-temurin-25,运行设置 LAB_IMAGE=maven:3.9.12-eclipse-temurin-25。先停止旧服务,避免争用回环端口 29200。
计算新增副本的长期代价
缓存需要内存和回源容量,消息需要存储积压与重试,搜索需要索引、段合并和重建空间。一次全量重建可能让旧新索引、快照与保留事件同时存在,还会与正常业务争用数据库读取和网络带宽。容量评估应包含这种过渡状态。
上线前应实际从现有源数据重新生成副本,确认所需时间和权限。缓存不可用时的主库保护、消息滞留时的任务查询、搜索故障时可继续使用的功能,都要落实到具体接口。当前访问需求用更少组件就能满足时,可以继续保留较简单的方案。
权威资料与规范地址
查询执行、缓存脚本、投递确认与搜索切换的精确规则可按下列入口查阅:
