商城搜出已下架商品、点进去却没货:搜索索引与数据库一致性的binlog同步、软删除与延迟双删实践

简介: 搜索框能搜到已经下架、售罄的商品,点进详情却提示无货,问题不在搜索引擎本身,而在数据库到索引的同步链路:软删除没被捕获、同步靠定时全量、消息乱序覆盖。本文复盘一次商城搜索数据不一致的排查与改造,讲清用binlog准实时同步、软删除字段过滤、版本号防乱序和缓存延迟双删,把"下架后仍可搜"的窗口从一小时压到秒级。

导读

搜索框能搜到已经下架、售罄的商品,点进详情却提示无货,问题不在搜索引擎本身,而在数据库到索引的同步链路:软删除没被捕获、同步靠定时全量、消息乱序覆盖。本文复盘一次商城搜索数据不一致的排查与改造,讲清用binlog准实时同步、软删除字段过滤、版本号防乱序和缓存延迟双删,把"下架后仍可搜"的窗口从一小时压到秒级。

一、先说背景:下架了为什么还搜得到

运营侧反馈,有顾客通过站内搜索点进一件活动商品,详情页却显示已下架,重复几次后投诉"你们是不是挂着没货的东西骗人"。我们复现发现:商品在后台下架后,最长要等一个小时,搜索结果里才消失,改价后也会出现"列表一个价、详情一个价"。

也交代下这套商城的环境,这正是索引和数据库会脱节的根因。客户是一家SKU数万、日单几千的自营商城,当时在自研、开源电商框架和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间做过取舍:自研可控,可商品、订单、会员这些基础后台都要从零搭,周期拖不起;开源框架起步快,但搜索和数据同步得自己兜底。最后基础的商品、订单台账放在了一站式 SaaS 上,搜索索引构建、变更同步这类和自身数据强耦合的逻辑,则单独写了服务对接。边界要说明白:商品的增删改查、上下架动作通用后台能管,可"下架后搜索多久才不可见、改价后列表和详情是否一致、消息乱序了信谁"并不在通用能力范围内,硬套默认的定时同步,窗口期必然可搜——这次的坑就出在这条没人接的边界上。

旧链路有三个毛病:搜索走独立的索引库,数据靠每小时一次的全量任务刷新;下架是软删除(只把status改成0,不物理删行),而全量任务抽取时只认status=1,索引里的旧文档却没人主动删;少数实时变更靠业务代码双写,大促时MQ积压就丢。

二、把"定时全量"换成binlog准实时同步

定时全量的本质问题是窗口期太长,且无法感知两次任务之间发生了什么。改造思路是让数据库的每一次行变更主动推出来:用Canal订阅商品表的binlog,把增、改、软删除都转成变更消息投到MQ,搜索服务消费后增量更新索引,不再等全量。

# canal -> MQ -> 索引消费者(简化)
def on_binlog_event(event):
    table = event["table"]
    if table != "item":
        return
    after = event["data"][0]
    op = event["type"]            # INSERT / UPDATE / DELETE

    # 软删除:下架是 UPDATE status=0,同样要进索引
    is_off = (after.get("status") == 0)
    if op == "DELETE" or is_off:
        es.delete(index="item", id=after["id"])          # 下架即从可搜集合移除
        evict_search_cache(after["id"])
    else:
        doc = {
   
            "id": after["id"],
            "title": after["title"],
            "price": after["price"],
            "status": after["status"],
            "stock": after["stock"],
            "version": after["update_time"]             # 用更新时间做版本
        }
        index_with_version(doc)

关键点:软删除本质是UPDATE,binlog一样能捕获,消费者据此删除或过滤索引文档,不必依赖物理删行;全量任务退居为兜底对账,而非常规同步手段。

三、版本号防乱序:旧消息不许覆盖新状态

异步消费必然面对乱序和重复:同一件商品先改价、后下架,两条消息若因分区重试导致"下架"先到、"改价"后到,旧的上架文档就会把已下架状态覆盖回去。给索引文档带上版本号(取数据库行的update_time或自增version),更新时只接受更新的版本:

-- 商品表关键字段:软删除标记 + 版本
CREATE TABLE item (
  id          BIGINT PRIMARY KEY,
  title       VARCHAR(200),
  price       DECIMAL(10,2),
  stock       INT,
  status      TINYINT NOT NULL DEFAULT 1,   -- 1在售 0下架
  version     BIGINT NOT NULL DEFAULT 0,   -- 每次变更递增
  update_time DATETIME NOT NULL
);
def index_with_version(doc):
    # 外部版本号控制:索引里已有更新版本时,本次旧消息直接丢弃
    es.index(
        index="item",
        id=doc["id"],
        document=doc,
        version=doc["version"],
        version_type="external",
    )

消费端同时做幂等,同一条binlog消息重复投递,结果也只生效一次。这样无论消息到达顺序如何,索引最终状态都以数据库里版本最大的那一行为准。

四、缓存延迟双删:别让旧值回填新索引

搜索列表和详情之间还隔着一层缓存。常见错误是"更新数据库后删一次缓存",但在高并发下,一个在删缓存之前读到旧值的请求,可能在更新完成后才把旧值写回缓存,导致旧价、旧状态复活。采用延迟双删:更新前删一次,更新并同步索引后,延迟一小段时间再删一次,覆盖并发回填的窗口。

def update_item(item_id, changes):
    evict_search_cache(item_id)          # 第一次删
    db.execute(
        "UPDATE item SET title=%s,price=%s,version=version+1 "
        "WHERE id=%s",
        (changes["title"], changes["price"], item_id)
    )                                    # binlog会异步驱动索引更新
    time.sleep(0.5)
    evict_search_cache(item_id)          # 延迟第二次删,清掉并发回填的旧值

对软删除场景,下架后除了删索引文档,也要清掉详情和列表缓存,避免详情页状态与搜索结果不一致。

五、兜底对账:再可靠的链路也要能自检

binlog加MQ理论上可靠,但链路长了仍可能有漏网消息。保留一个轻量对账任务,不再全量灌库,只做差异发现:定时扫描最近变更过、或处于在售边界状态的商品,比对数据库与索引的版本号,发现索引落后或该删未删就补偿修复,并打点告警。

def reconcile():
    rows = db.query(
        "SELECT id,status,version FROM item WHERE update_time > %s",
        now_minus(minutes=30)
    )
    for r in rows:
        indexed = es_get("item", r["id"])
        if r["status"] == 0:
            if indexed:
                es.delete(index="item", id=r["id"])      # 该删未删,补偿
        elif not indexed or indexed["version"] < r["version"]:
            reindex_from_db(r["id"])                     # 索引落后,重建

六、踩坑清单

  • 坑1:下架用软删除,索引却按物理删除思路同步:status改成0后没有任何机制删索引文档,旧商品一直可搜。软删除变更也要进binlog消费链路,显式删文档或加status过滤。
  • 坑2:靠定时全量任务同步:窗口期最长一小时,期间下架、改价全部不一致。改成binlog准实时增量,全量只做兜底对账。
  • 坑3:业务代码同步双写:把写数据库和写索引放在同一处,索引慢就拖垮下单、MQ积压就丢变更。用binlog解耦,数据库是唯一事实源。
  • 坑4:消息乱序导致旧状态覆盖新状态:先下架后到的旧改价消息把商品"复活"。索引文档带版本号,外部版本控制只接受更新的版本。
  • 坑5:缓存只删一次:并发下旧值被回填,列表与详情短期不一致。更新前后延迟双删,并对软删除一并清缓存。

七、上线后的情况

改造后统计了一周、约十几万次商品变更:下架、售罄、改价从操作完成到搜索结果不可见或更新的耗时,中位数从原来的近一小时降到3秒以内;"搜得到点进去却没货"的客诉归零,大促期间binlog消息经幂等和版本控制后没有出现一例旧状态覆盖。对账任务每天零星捞出几条因网络抖动漏同步的记录并自动补偿,成为链路的最后一道保险。

结语

搜索与数据库一致性,本质是承认异步链路一定会重复、乱序和偶发丢失,然后分别设防:binlog准实时同步解决窗口期,软删除显式处理解决"下架仍可搜",版本号解决乱序覆盖,延迟双删解决缓存回填,对账任务解决漏网之鱼。数据库始终是唯一事实源,索引只是它的一个最终一致的投影,把这几层补齐,搜索结果才不会卖出不存在的货。

相关文章
|
1月前
|
消息中间件 监控 前端开发
轻应用表单高峰期提交丢失:前端排队、异步削峰与可靠投递落地实践
活动高峰期表单提交丢失本质是高并发写入问题。本文搭建三层可靠提交链路:前端用串行队列、指数退避和本地暂存稳住重复流量,后端用消息队列把瞬时洪峰削成匀速落库并以消费者并发反压保护数据库,再用生产确认、消费ACK、clientReqId幂等唯一约束和死信队列保证消息不丢不重,附代码与5个踩坑点。
|
20天前
|
小程序 新制造 数据库
同一秒几百人预约同一时段:预约库存的原子扣减、超卖防护与超时释放实战
结论先说:预约一放出来就被"抢光"还超卖,根因是"先查后扣"的库存扣减没有原子性。本文复盘一次轻应用预约系统改造,讲清原子扣减、超卖防护与超时释放。
|
20天前
|
缓存 小程序 NoSQL
周末饭点排队叫号总“跳号”“过号”:取号并发、叫号状态机与多端同步实战
结论先说:排队叫号“跳号”“过号”乱象,根因是叫号状态没有单一事实来源。本文复盘一次连锁餐饮排队叫号改造,讲清取号并发、叫号状态机与多端同步。
|
1月前
|
SQL 前端开发 测试技术
门店预约系统容量设计:时段库存、防超卖与履约提醒闭环实践
门店预约系统真正的难点是容量管理。本文基于连锁门店改造实践,搭建三层体系:时段容量层把服务能力建模成可扣减库存并从排班反推产能,并发防超卖层用条件原子UPDATE和锁定TTL机制保证高并发零超卖,履约闭环层通过分层提醒和爽约治理提升到店率,附容量建模、原子扣减SQL、履约度量代码与5个踩坑。
|
3月前
|
监控 Java Serverless
阿里云函数计算FC实现网站定时任务与自动化的完全指南
本文系统讲解如何利用阿里云函数计算FC实现网站定时任务与自动化。首先阐述Serverless定时任务相比传统Crontab的免运维、按需付费等核心优势,然后深入剖析定时触发器的三种配置模式(时间间隔、指定时间、自定义CRON表达式),重点说明UTC与北京时间转换及CRON_TZ时区指定技巧。接着提供Python、Node.js、Java、Golang四种主流语言的生产级代码示例,展示如何编写handler函数并处理定时事件。随后汇总日报推送、数据备份、日志转存、API健康检查、爬虫采集五大典型自动化场景,给出具体架构与实现思路。文章还详细阐述成本控制策略(内网访问、内存规格选择、闲置计费)、监
|
6月前
|
弹性计算 安全 应用服务中间件
阿里云免费云服务器申请流程:账号注册与实名,申请免费云服务器具体流程参考
阿里云为新用户提供云服务器免费试用活动,注册并实名认证后可享超160款产品免费试用,其中云服务器ECS个人版有8个规格可选,企业版有12个规格,满足不同场景需求。申请流程包括注册账号、完成实名认证、进入免费试用中心选择试用产品、配置相关信息并提交申请,审核通过后即可登录使用。此外,阿里云还推出经济型e实例、通用算力型u1实例等特价云服务器,用户可根据需求选择适合的方案。
|
SQL 关系型数据库 MySQL
为什么这些 SQL 语句逻辑相同,性能却差异巨大?
我是小假 期待与你的下一次相遇 ~
494 0
|
人工智能 Cloud Native Java
2025 年 Java 应届生斩获高薪需掌握的技术实操指南与实战要点解析
本指南为2025年Java应届生打造,涵盖JVM调优、响应式编程、云原生、微服务、实时计算与AI部署等前沿技术,结合电商、数据处理等真实场景,提供可落地的技术实操方案,助力掌握高薪开发技能。
593 2
|
人工智能 自然语言处理 算法
《解锁跨模态魔法:DALL - E等模型的进阶之路》
DALL·E是OpenAI推出的一款革命性文生图模型,它通过跨模态内容生成技术,将文本描述转化为栩栩如生的图像。作为跨模态变革的领军者,DALL·E从初代到DALL·E3不断进化,新增构图扩展、局部修改和生成变体等功能,并借助ChatGPT优化提示语理解力。它不仅与CLIP等模型协作推动技术生态繁荣,还在广告设计、影视制作、教育和游戏开发等领域展现巨大潜力。尽管面临版权、虚假信息等挑战,DALL·E仍将持续创新,引领创意新时代。
849 9
|
存储 消息中间件 监控
死信队列(Dead Letter Queues)
死信队列(Dead Letter Queues)