侧边栏壁纸
  • 累计撰写 264 篇文章
  • 累计收到 2 条评论

为了做 SaaS 多业务线把系统重构为多租户架构(Multi-Tenancy / 动态数据源),搜索收录与各租户关键词排名却断崖式暴跌?排查无状态爬虫未带租户头全量命中默认空库快照、ThreadLocal 泄漏致跨租户快照串号投毒、CDN 边缘缓存缺失隔离与 Canonical 跨租户漂移我踩过的几个深坑

2026-11-9 / 0 评论 / 12 阅读
AI 摘要由 AI 生成

重构SaaS多业务线为多租户架构后,搜索收录和关键词排名暴跌,问题源于爬虫无状态访问、ThreadLocal泄漏、CDN缓存缺失等。文章详细分析了这些问题及解决方案。

上个月我们把原本单体独立的 B2B 行业门户全面重构成了多租户 SaaS 架构。初衷很明确,为了同时支撑旗下自营的几个行业垂直站,顺带给十几家大型加盟客户提供独立域名绑定的白标站点,技术团队在底层做了动态数据源切换(基于 Spring 的 AbstractRoutingDataSource 与 HikariCP 连接池),前端统一采用 Next.js 服务端渲染,前置网关用 Nginx 配合 Cloudflare CDN 做反向代理。

在内网联调与灰度测试阶段,所有业务功能表现得很顺畅。租户之间的数据严格按库隔离,登录后台只能看到自己租户的订单与文章,不同二级域名的企业主题样式和 Logo 也切分得清清楚楚。

然而全量切流两周后,监控群里的告警打破了平静。

全站数十万存量索引的自然搜索展现量暴跌了 70% 以上,不仅新入驻租户发布的上千篇行业资讯完全不被收录,连几个运营了三四年的老牌自营垂直站,核心关键词排名也大面积跌出前五页。Google Search Console 集中爆出了上万个“重复网页,用户未选择规范网页(Duplicate without user-selected canonical)”和“软 404(Soft 404)”,百度搜索资源平台的抓取诊断里也密密麻麻全是快照异常与耗时飙升。

当我点开各大搜索引擎的网页快照库时,背脊阵阵发凉:租户 B 的机械设备产品页,百度快照里显示的居然是租户 A 的电子元器件企业介绍;一大半二级域名租户的行业文章,在爬虫快照里竟然全是空空如也的骨架或“暂无数据”;更离谱的是,所有租户页面底部的 Canonical 规范链接,全部指向了 SaaS 平台的公共主域名。

把一个单租户系统改成多租户支撑 SaaS 并不算难,但搜索引擎爬虫是完全无状态的访客。爬虫不会按业务前端的规矩携带自定义 HTTP 标头,也不会老老实实配合你在浏览器端存 Cookie。一旦底层的租户解析、线程上下文管理、边缘缓存与规范标签有一处设计脱节,搜索引擎就会把整套 SaaS 站群当成劣质复制品或大面积死链。

复盘排查整整四天,我把这次重构中踩到的几个致命深坑与最终的生产修复方案详细整理出来。


坑一:爬虫无状态直连,未带 Tenant-ID 标头被默认路由进空白库(软 404 灾难)

我们这套多租户系统的租户识别最初是由前端脚手架团队主导设计的。为了让单页应用与 API 交互更纯粹,团队约定客户端每次请求都通过 Axios 拦截器在 HTTP 标头里塞入 X-Tenant-Id: tenant_1001,如果是已登录用户,也可以从 JWT Token 或 Cookie 中解析出租户 ID。

后端的 TenantInterceptor 拦截逻辑写得很符合典型的业务流转:

public class TenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = request.getHeader("X-Tenant-Id");

        if (StringUtils.isBlank(tenantId)) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if ("tenant_id".equals(cookie.getName())) {
                        tenantId = cookie.getValue();
                        break;
                    }
                }
            }
        }

        // 如果都没取到,给一个默认租户兜底
        if (StringUtils.isBlank(tenantId)) {
            tenantId = "default_platform";
        }

        TenantContextHolder.setTenantId(tenantId);
        return true;
    }
}

业务人员在浏览器里访问或者通过前端界面点选,请求头都带有完备的租户标识,后端根据这个标识切到具体的客户数据库,数据读写十分顺畅。

可搜索引擎的蜘蛛(Baiduspider、Googlebot、Bingbot)根本不是我们的前端应用。爬虫抓取网页时,发起的是原生的无状态 HTTP GET 请求,既不会主动携带私有的 X-Tenant-Id,也不会保留上一跳写入的会话 Cookie。

当 Googlebot 顺着外链抓取 https://machinery.example.com/article/5892.html 时,请求抵达网关,后端拦截器压根找不到任何自定义 Header 和 Cookie。拦截器执行了兜底逻辑,把当前线程的租户标识硬生生赋成了 default_platform。

default_platform 是初始化系统时建的一个公共空库,里面除了一两篇演示公告,所有的租户业务表都是空的。服务端的 SSR 渲染器拿着这个空库的数据库连接去查文章 ID 为 5892 的记录,结果自然是返回 null。

更致命的是前端模板的异常处理。模板没有直接抛出 404 响应,而是渲染出了一个带有公共导航栏、内容区显示“文章已被删除或不存在”的兜底静态页,并且 HTTP 状态码返回了清一色的 200 OK。

爬虫连续爬了上万个不同的租户文章 URL,拿到的全是状态码为 200、正文一模一样且内容不足百字的空模板。百度和谷歌的质量算法在三天内迅速将这批 URL 标记为“软 404”与“低质薄内容”,抓取配额直接腰斩,全网历史收录遭遇断崖式清理。

改造方案:网关层强制 URL 维度物理识别,彻底剥离对客户端 Header 的依赖

公开供搜索引擎抓取的页面,租户身份必须与 URL 强绑定,绝对不能依赖请求头或 Cookie。

我们在 Nginx 接入层前置了租户提取逻辑,支持二级子域名模式(tenant.example.com)和自定义独立域名模式(www.tenant-brand.com),直接在网关内部完成映射并改写上游请求头:

map $http_host $extracted_tenant {
    default "";
    # 匹配二级子域名
    ~^(?<subdomain>[a-zA-Z0-9-]+)\.example\.com$ $subdomain;
    # 映射外部独立绑定域名
    "www.machinery-brand.com" "tenant_machinery";
    "portal.electronics-pro.com" "tenant_elec";
}

server {
    listen 80;
    listen 443 ssl http2;
    server_name ~^(.*)\.example\.com$ www.machinery-brand.com portal.electronics-pro.com;

    location / {
        # 如果域名无法解析出任何有效租户,直接在接入层返回 404,阻断软 404
        if ($extracted_tenant = "") {
            return 404 "Tenant Not Found";
        }

        proxy_pass http://saas_backend_upstream;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 网关层强制向下游服务透传提取出的真实租户标识
        proxy_set_header X-Tenant-Id $extracted_tenant;
    }
}

后端 TenantInterceptor 同步重构,废黜盲目回退到默认空库的做法。如果请求来自公开页面路由且无法定位租户,直接在拦截器中抛出 TenantNotFoundException,让全局异常处理器准确返回 404 Not Found 状态码,坚决不让软 404 污染快照。


坑二:Tomcat 线程池复用引发 ThreadLocal 泄漏,租户快照严重串号投毒

解决了爬虫识别租户的问题后,另一个更诡异的现象出现了。

运营人员在抽检搜索快照时发现,某些明明属于租户 A 的页面,在百度的快照里显示的标题和正文居然是租户 B 的内容。刷新两三次,有时候看到的是 A,有时候刷新出来又是 B。

追踪日志后发现,罪魁祸首是底层极其常见的 ThreadLocal 变量泄漏。

在 Spring Web 体系中,多租户上下文通常保存在一个静态的 ThreadLocal<String> 容器里,以供当前请求链路上的 Service、DAO 和 AbstractRoutingDataSource 随时读取:

public class TenantContextHolder {
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        CONTEXT.set(tenantId);
    }

    public static String getTenantId() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

平时用户登录量平稳时,Tomcat 内部的 Worker 线程池复用周期较长,且大部分业务请求都会重新触发 TenantInterceptor.preHandle 覆盖 ThreadLocal,问题没有完全暴露。

然而当百度蜘蛛发起并发批量抓取时,短时间内几千个请求涌入。以下两个场景直接引发了线程上下文污染:

场景一是异常中断与静态资源旁路请求。某个请求在执行过程中抛出了未捕获的运行时异常(比如数据库瞬时超时),导致执行流跳过了后续逻辑。原有的拦截器把清理逻辑写在了 postHandle 里,而 Spring 框架规定,一旦 preHandle 之后的业务代码抛出异常,postHandle 根本不会被执行,只有 afterCompletion 才会保证触发。这导致抛出异常的 Worker 线程带着租户 A 的 ID 回到了 Tomcat 线程池。

紧接着,当该线程被分配去处理百度蜘蛛对租户 B 的页面请求时,如果中间某个过滤器发生阻断或直接利用了旧线程的上下文,数据库查询就会直接指向租户 A 的库,渲染出租户 A 的文章正文。

场景二是异步线程池脱节。为了加速首屏渲染,我们在控制器里使用了 CompletableFuture.supplyAsync 并行加载文章的“相关推荐”和“侧边栏热门标签”。默认情况下,标准 ThreadLocal 的值无法传递给异步线程池中的子线程。异步线程拿到的上下文是空的,甚至偶尔拿到的是子线程池中上次残留的租户 C 的标识。

这就导致了一个极其荒谬的现象:文章的主体是租户 B 的,而底部的推荐链接和侧边栏分类全是租户 C 的。搜索引擎判定该页面存在大面积内容冲突与欺骗性重定向倾向,严重影响权重评估。

改造方案:强制 afterCompletion 兜底清理与 TaskDecorator 异步上下文继承

首先,重写拦截器生命周期,必须在 afterCompletion 中无条件执行 clear(),确保无论成功、业务异常还是客户端主动掐断连接,回收进池的线程永远是干净的:

public class StrictTenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = request.getHeader("X-Tenant-Id");
        if (StringUtils.isBlank(tenantId)) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return false;
        }
        TenantContextHolder.setTenantId(tenantId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) {
        // 必须在 afterCompletion 中彻底移除,防止线程复用污染
        TenantContextHolder.clear();
    }
}

针对异步调用,不要手动在每一个业务方法里拷贝上下文。通过自定义 Spring 的 TaskDecorator,在任务提交和执行边界处建立自动拷贝与销毁机制:

@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {

    @Bean("saasAsyncExecutor")
    public Executor saasAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(16);
        executor.setMaxPoolSize(64);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("saas-async-");

        // 配置上下文装饰器
        executor.setTaskDecorator(runnable -> {
            // 抓取当前主线程的租户标识
            String tenantId = TenantContextHolder.getTenantId();
            return () -> {
                try {
                    // 绑定到子线程
                    TenantContextHolder.setTenantId(tenantId);
                    runnable.run();
                } finally {
                    // 执行完毕后立即清空子线程上下文
                    TenantContextHolder.clear();
                }
            };
        });

        executor.initialize();
        return executor;
    }
}

上线后通过压测脚本对不同租户的公开接口做交叉并发压测,核对响应中的租户签名和数据库连接日志,上下文串号现象彻底归零。


坑三:CDN 边缘缓存忽略 Host 维度,租户 A 快照被缓存投毒给租户 B

在静态化与性能优化阶段,运维团队为了提升全站响应速度、降低源站 SSR 的 CPU 消耗,在 Cloudflare 和自建边缘 Nginx 节点上开启了全页面的边缘 HTML 缓存(Cache Everything)。

为了追求更高的缓存命中率,运维在配置缓存键(Cache Key)规则时犯了一个致命经验主义错误。他们认为所有租户底层的页面 URL 规范都是一致的(比如都是 /article/1001.html),于是在缓存规则中直接把 Cache Key 简写成了:

# 致命错误配置:只包含了请求 URI 和参数,完全丢掉了 Host 区分
proxy_cache_key "$uri$is_args$args";

这导致了灾难性的跨租户缓存投毒。

当某个真实访客首次访问了租户 A 的产品列表 https://elec.example.com/products,边缘节点向源站请求页面,源站返回了租户 A 的电子元器件列表,CDN 节点根据 URI /products 将这份 HTML 在边缘硬盘上缓存了 2 小时。

五分钟后,Baiduspider 顺着外链去抓取租户 B 的机械设备产品列表 https://machinery.example.com/products。

请求到达 CDN 边缘节点。CDN 检查 Cache Key,发现请求的 URI 同样是 /products,而且缓存处于有效期内。于是 CDN 直接从内存中调出刚才租户 A 的页面,毫不犹豫地作为一个 200 HIT 响应返回给了百度蜘蛛。

百度蜘蛛抓下了这个带有租户 A 信息的快照,存入了租户 B 的索引库中。等到下午爬虫再去抓租户 C 的 /products,如果缓存正好被租户 B 的新请求冲刷过,爬虫又会拿到租户 B 的内容。

在搜索引擎的算法模型里,几百个不同独立域名或子域名下的页面内容完全雷同,并且还在以数小时为周期随机跳动。这不仅触发了搜索引擎对“镜像站群”与“泛站作弊”的严厉惩罚,也直接造成各大租户域名的信任度被集体降权。

改造方案:强制收敛 Cache Key 维度,并在源站显式声明 Vary 响应头

边缘缓存必须将域名(Host)和通信协议(Scheme)作为最基础的分区维度。

在边缘 Nginx 节点上,重构 proxy_cache_key,彻底杜绝多租户共享同一个缓存桶:

# 正确的多租户缓存键配置:协议 + 独立 Host + URI + 参数
proxy_cache_key "$scheme$http_host$uri$is_args$args";

# 针对公开页面启用缓存,但对爬虫保留精确头
proxy_cache_valid 200 301 302 30m;
proxy_cache_valid 404 1m;

# 强制要求下游及 CDN 在响应缓存中区分 Host 和语言编码
proxy_hide_header Vary;
add_header Vary "Host, Accept-Encoding" always;

同时在源站 Spring Boot 的拦截器或全局过滤器中,对于公开渲染页面,一律在 HTTP 响应头里增加缓存控制细化标头:

response.setHeader("Cache-Control", "public, max-age=1800, s-maxage=3600");
response.setHeader("Vary", "Host, Accept-Encoding");

配置更新完成后,通过 cURL 命令模拟两个不同 Host 发送相同 URI 请求:

curl -I -H "Host: elec.example.com" https://cdn.example.com/products
curl -I -H "Host: machinery.example.com" https://cdn.example.com/products

比对两次响应的 X-Cache-Status 和 HTML 实体哈希,确保两个域名各自独立经历 MISS 到 HIT 的生命周期,完全互不影响。


坑四:动态 Canonical 标签未隔离,各租户页面权重全部跨站倒流给主站

Canonical(规范网址标签)是现代技术 SEO 中极其核心的防重复指令。它的作用是告诉搜索引擎:“不管用户是通过什么参数或别名访问进来的,这个页面的唯一权重归属目标是指定的规范 URL”。

单体系统时代,我们在全局模板头部写死了一段脚本:

<!-- 单体时代遗留下来的老代码 -->
<link rel="canonical" th:href="'https://www.example.com' + ${requestUri}" />

重构成多租户架构后,负责前端模板迁移的同学注意到了这里不能写死 www.example.com,于是改成了读取配置项中的 platform.domain。

然而他们忘记了一个根本事实:在多租户体系下,每个租户都有自己的独立主权域名。比如租户 1 是 machinery.example.com,租户 2 甚至绑定了自己的独立域名 www.tenant-brand.com。

但配置文件里的 platform.domain,填写的依然是 SaaS 平台自己的营销官网 www.example.com。

这就造成了一个极其荒唐的局面:

租户 A 的优质原创文章 https://machinery.example.com/article/108.html 渲染出来的 HTML 里,头部赫然写着:

<link rel="canonical" href="https://www.example.com/article/108.html" />

租户 B 的同类文章页面里,头部同样写着指向 https://www.example.com 的 Canonical 链接。

Googlebot 和各大搜索爬虫解析到这行标签后,忠实地执行了搜索引擎的标准规范:将所有子站点和租户独立域名的页面权重,全额集中合并到 Canonical 指定的目标网址。

而 SaaS 平台的主站 www.example.com 根本就没有这些文章的具体内容,访问过去直接是 404。

结果就是:各个租户站点的页面被搜索引擎从索引库中逐渐剔除(因为它们自己声明自己不是规范版本),而主站又接不住这批根本不存在的内容,导致几十万页面的搜索权重全部白白蒸发在虚空中。

改造方案:基于租户元数据动态渲染租户专属的绝对 Canonical URL

Canonical 标签必须根据当前租户绑定的首选规范域名动态计算,且必须是包含协议头的绝对完整路径。

我们在租户管理微服务中为每个租户定义了 canonical_base_url(支持配置为租户自定义域名或平台二级域名),在服务端渲染控制器中通过模型直接注入:

@Controller
public class ArticleDetailController {

    @Autowired
    private TenantMetadataService tenantMetadataService;

    @GetMapping("/article/{id}.html")
    public String detail(@PathVariable("id") Long id, Model model, HttpServletRequest request) {
        String tenantId = TenantContextHolder.getTenantId();
        TenantConfig config = tenantMetadataService.getConfig(tenantId);

        Article article = articleService.getByIdAndTenant(id, tenantId);
        if (article == null) {
            throw new ResourceNotFoundException("Article not found");
        }

        // 动态拼装该租户的唯一权威规范地址
        String canonicalUrl = config.getCanonicalBaseUrl() + "/article/" + id + ".html";

        model.addAttribute("article", article);
        model.addAttribute("canonicalUrl", canonicalUrl);
        return "article_detail";
    }
}

前端 HTML 模板严格绑定该变量,彻底杜绝跨租户指向主站的可能:

<head>
    <title th:text="${article.title} + ' - ' + ${tenantConfig.siteName}">页面标题</title>
    <!-- 动态绑定的规范 URL,必须属于当前租户自身域名体系 -->
    <link rel="canonical" th:href="${canonicalUrl}" />
    <!-- 同时修正 OpenGraph 协议中的 URL 属性 -->
    <meta property="og:url" th:content="${canonicalUrl}" />
</head>

坑五:爬虫集中巡检打爆连接池,动态数据源懒加载引发批量 500 / 504

在多租户底层,我们采用了典型的“分库(Database-per-tenant)”隔离模型。系统配置了一个 DynamicRoutingDataSource,继承自 Spring 的 AbstractRoutingDataSource:

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContextHolder.getTenantId();
    }
}

为了节约服务器内存,一开始没有在服务启动时初始化所有租户的数据库连接池,而是采用了按需懒加载机制:当某个租户的第一个请求到达时,动态读取该租户的 JDBC 配置,现场初始化一个最小容量为 5、最大容量为 20 的 HikariCP 连接池并塞入 Map 中。

在日常平稳访问时,这套逻辑跑得好好的。因为普通访客通常只在某一个具体租户的站点内游览,连接池建好后便一直稳定复用。

但在技术 SEO 领域,各大搜索引擎的网络爬虫具有强烈的批量爬取特性。

比如百度爬虫每天凌晨会针对整个站群进行集中例行抓取,几十只蜘蛛并发爬取,短时间内横跨了 80 多个不同租户的域名。

那一瞬间,后端应用服务器同时收到了来自几十个冷门租户的并发请求。系统同时尝试为这几十个租户动态创建 HikariCP 连接池。

灾难接踵而至:

首先,每个 HikariCP 初始化时都需要与后端的 MySQL 集群进行三次握手与认证鉴权,几十个连接池瞬间向 MySQL 发起了数百个连接握手,瞬时打满了数据库服务器的 max_connections 上限,触发了大量的 Too many connections 报错。

其次,数据源初始化的临界区包含了互斥同步锁,导致工作线程严重阻塞。后来的爬虫请求在队列里等待获取数据库连接超时,直接抛出 CannotGetJdbcConnectionException。

前端 Nginx 在等待上游响应 60 秒后超时,向百度蜘蛛大面积返回 504 Gateway Time-out。

爬虫连续数小时遇到服务器无响应和 504 错误,触发了搜索引擎内部的“源站不稳定熔断保护机制”。算法判定这批网站服务器性能恶劣,自动降低了全站的抓取频次与爬虫配额,使得存量快照长时间无法更新,新页面彻底失去曝光机会。

改造方案:数据源预热分级与共享租户 Schema 路由优化

对于 SaaS 系统中存在大量非高活租户的场景,不宜为每个冷租户开辟独立的物理连接池。

我们根据租户的流量体量与业务层级实行了分级存储策略:

  1. 核心大客户与自营主站保留独立的物理数据库与专属 HikariCP 连接池,且在 Spring Boot 容器启动时通过 ApplicationRunner 强制完成预热,禁止懒加载。
  2. 中小长尾租户收敛至共享的集群实例中,采用同库不同 Schema(或者同库共享表加 tenant_id 联合索引物理隔离)的模型,所有中小租户共用一套配置优良的弹性 HikariCP 连接池。路由切换时仅在连接层面调用 connection.setCatalog(tenantSchema),把毫秒级的连接池创建开销降为微秒级的轻量级会话指令。

此外,针对爬虫流量,在 Nginx 接入层设置专用的限速保护区域,避免蜘蛛并发突增冲垮源站:

# 识别常见合规爬虫,给予平滑速率控制
map $http_user_agent $is_spider {
    default 0;
    ~*(Baiduspider|Googlebot|bingbot|Bytespider) 1;
}

limit_req_zone $binary_remote_addr zone=spider_limit:20m rate=15r/s;

server {
    ...
    location / {
        # 对爬虫启用平滑请求排队,允许瞬时小突发,但限制平均速率
        if ($is_spider = 1) {
            limit_req zone=spider_limit burst=30 nodelay;
        }

        proxy_pass http://saas_backend_upstream;
        ...
    }
}

生产验证与上线排查核对规范

将上述五项重构全部部署到预发与生产环境后,我们执行了一整套针对多租户 SEO 场景的严格自动化核对流程。

通过编写自动测试脚本,对不同的租户域名进行无 Header、无 Cookie 的伪造蜘蛛抓取验证:

# 模拟百度蜘蛛访问租户 A,验证状态码、Canonical 及正文特征
curl -s -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
     -I "https://machinery.example.com/article/108.html" | grep -E "(HTTP/|Vary:|X-Tenant-Id:)"

# 检查返回的 HTML 源码,确认 Canonical 标签绝对地址与当前租户一致
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
     "https://machinery.example.com/article/108.html" | grep -i '<link rel="canonical"'

核对标准必须满足四项硬性指标:

  • 任何通过公网访问的合法子域名或独立域名,在不携带任何自定义 Request Header 和 Cookie 的前提下,均能稳定返回 HTTP 200,且正文包含完备的该租户数据,严禁出现软 404 空白骨架。
  • 响应头中必须包含 Vary: Host, Accept-Encoding,CDN 边缘缓存必须严格按照域名隔离存储,杜绝跨租户快照投毒。
  • 页面源码中的 <link rel="canonical" ... /> 必须精准指向当前租户的权威主域名,绝对不允许出现回退到平台主站的情况。
  • 后端服务在处理多租户请求时,线程复用必须彻底清除 ThreadLocal 状态,异步并发任务必须通过 TaskDecorator 规范化拷贝上下文。

全套方案上线修复三周后,Google Search Console 报告中的软 404 报警全部清零,上万条因规范网址混乱被剔除的页面陆续重新被收录,百度爬虫的抓取耗时稳定降至 120 毫秒以内,各垂直行业站点的自然搜索展现量恢复到了重构前的水平。

多租户架构在业务研发层面带来的是资源整合与降本提效,但在面对搜索引擎这类无状态访客时,系统的每一个细节都需要保持对 HTTP 协议与规范的敬畏。

评论一下?

OωO
取消