重定向类型

一个看似简单的选择

重定向逻辑写完了,但我盯着代码里的一个数字发呆:301

return redirect(long_url, code=301)  # 还是 302?

我搜了半天,发现这个选择的影响比想象中大得多。301 还是 302,看起来只是改个数字,但会影响统计数据、用户体验,甚至 SEO。

HTTP 重定向状态码

HTTP 协议定义了多种重定向状态码:

状态码名称含义
301Moved Permanently永久重定向
302Found临时重定向
303See Other查看其他位置
307Temporary Redirect临时重定向(保持方法)
308Permanent Redirect永久重定向(保持方法)

最常用的是 301302。我需要理解它们的区别。

301 永久重定向

浏览器会缓存

HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/original-long-url
Cache-Control: max-age=31536000

301 表示资源已永久移动到新位置。浏览器会缓存这个重定向关系。

我做了个实验:

首次访问

用户访问 short.url/abc123

请求我的服务器

返回 301 + 目标 URL

浏览器跳转,并缓存这个关系

再次访问

用户访问 short.url/abc123

浏览器发现缓存中有这个短链接的重定向

直接跳转,不请求我的服务器!

好处与坏处

好处是快——用户第二次访问时,直接从浏览器缓存跳转,延迟为 0。

但坏处也很明显:我统计不到点击了

用户访问 100 次,我的服务器只收到第 1 次请求。剩下的 99 次都是浏览器直接跳转。我的点击统计完全失效了。

更大的问题:无法修改

还有一个更严重的问题:如果用户想修改短链接的目标 URL 呢?

# 第 1 天:用户创建了短链接
short.url/abc123 → blog.example.com/post-1

# 第 30 天:用户修改了目标 URL
short.url/abc123 → shop.example.com/product-2

如果我用 301,浏览器已经缓存了旧的重定向关系。用户修改了目标 URL,但访问短链接时还是跳到旧的地址。

用户访问 short.url/abc123

浏览器缓存命中

直接跳转到 blog.example.com/post-1(旧地址!)

修改无效!用户看不到新目标

301 的语义是”永久”,浏览器会认真对待这个承诺。

302 临时重定向

HTTP/1.1 302 Found
Location: https://www.example.com/original-long-url
Cache-Control: no-cache

302 表示资源临时从不同位置访问。浏览器不应缓存这个重定向关系。

每次访问

用户访问 short.url/abc123

请求我的服务器

返回 302 + 目标 URL

跳转(但不缓存)

下次访问时,流程一样,会再次请求我的服务器。

好处:完整的控制权

使用 302,每次访问都会经过我的服务器。这给了我完整的控制权:

def redirect(short_code):
    url_record = db.get_record(short_code)

    # 检查链接是否被禁用
    if url_record.status == 'disabled':
        return render_error('该链接已被禁用')

    # 检查是否过期
    if url_record.expires_at and url_record.expires_at < now():
        return render_error('该链接已过期')

    # 检查访问权限
    if url_record.password and not verify_password():
        return redirect('/login')

    # 记录点击统计
    db.click_count += 1
    save_click_log(
        ip=request.ip,
        referer=request.headers.get('Referer'),
        user_agent=request.headers.get('User-Agent'),
        ...
    )

    return redirect(url_record.long_url, code=302)

这些功能用 301 是无法实现的——浏览器缓存后根本不请求服务器。

坏处:多了一次往返

每次访问都要请求我的服务器,多了一次网络往返。性能上比 301 差一些。

指标301302
首次响应~50ms~50ms
再次访问0ms(缓存)~50ms
服务器压力

SEO 权重传递

研究了 SEO 之后,我才知道 301 和 302 还有一个重要区别:权重传递

301 会传递 SEO 权重

搜索引擎看到 301 时,会将原始 URL 的 SEO 权重传递给目标 URL。

short.url/abc123(权重:100)
    ↓ 301 重定向
example.com/target(获得权重:100)

这适用于网站迁移场景:旧域名 → 新域名,权重跟着转移。

302 不会传递权重

搜索引擎看到 302 时,认为这是临时的,权重保留在原始 URL。

short.url/abc123(权重:100)
    ↓ 302 重定向
example.com/target(权重:0)

short.url/abc123 仍然保留权重 100

我的短链接服务需要 SEO 吗?

对于短链接服务,权重应该留在哪里?

我思考了一下:

  1. 短链接域名本身不需要 SEO:用户是通过其他渠道(社交媒体、短信)看到短链接的,不是通过搜索引擎搜索”短链接”找到的。

  2. 目标 URL 应该获得权重:如果有人分享了短链接指向他的博客,权重应该传递给博客,而不是留在短链接域名。

从这个角度,似乎应该用 301?

但我又想到另一个问题:短链接服务本身不是内容生产者。搜索引擎爬虫访问短链接时,应该让它跳转到目标内容,而不是把权重留在短链接域名。

不过,大多数短链接的目标 URL 本身就有自己的 SEO,不需要通过短链接获得权重。而且,很多短链接指向的是临时内容、活动页面,这些页面本来就不需要长期 SEO。

权衡之下,我决定不把 SEO 作为主要考虑因素。

我的选择:302

综合考虑后,我选择了 302。原因如下:

原因 1:需要准确的点击统计

短链接服务的重要功能就是点击统计。用户创建短链接后,想知道有多少人点击了,从哪里点击的。

301 的问题

首次访问:请求服务器 → 301 重定向 → 浏览器缓存
再次访问:[不请求服务器] → 直接从缓存跳转 ❌ 统计丢失

302 的方案

每次访问:请求服务器 → 记录统计 → 302 重定向 ✅ 统计完整

原因 2:原始 URL 可能会变

用户可能需要修改短链接的目标 URL:

  • 商品链接换了
  • 博客迁移了
  • 活动页面更新了

用 302,我可以随时更新目标 URL,用户立即生效。用 301,浏览器缓存会导致修改无效。

原因 3:需要权限控制

短链接可能需要:

  • 禁用功能(违规链接)
  • 过期时间(临时链接)
  • 密码保护(私密链接)

这些都需要每次请求都经过服务器验证。301 的缓存机制无法实现。

实现代码

Flask 实现

from flask import Flask, redirect, abort, request

app = Flask(__name__)

@app.route('/<short_code>')
def redirect_url(short_code):
    # 查询数据库
    record = db.query(
        "SELECT long_url, status FROM url_mapping WHERE short_code = %s",
        (short_code,)
    )

    if not record:
        abort(404)

    if record.status == 'disabled':
        abort(403)

    # 异步记录点击
    click_log.delay(
        short_code=short_code,
        ip=request.remote_addr,
        referer=request.headers.get('Referer'),
        user_agent=request.headers.get('User-Agent')
    )

    # 302 重定向
    return redirect(record.long_url, code=302)

Go 实现

func redirectHandler(w http.ResponseWriter, r *http.Request) {
    shortCode := r.URL.Path[1:]  // 去掉开头的 /

    // 查询数据库
    longURL, err := db.GetLongURL(shortCode)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    // 异步记录点击
    go recordClick(shortCode, r)

    // 302 重定向
    http.Redirect(w, r, longURL, http.StatusFound)
}

添加缓存控制头

为了防止中间代理缓存 302 响应,我添加了明确的缓存控制头:

@app.route('/<short_code>')
def redirect_url(short_code):
    long_url = get_long_url(short_code)
    
    if not long_url:
        abort(404)
    
    response = redirect(long_url, code=302)
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    
    return response

这样即使有 CDN 或代理服务器,也不会缓存我的重定向响应。

本章小结

特性301302
语义永久重定向临时重定向
浏览器缓存✅ 会缓存❌ 不缓存
SEO 权重传递给目标保留在原始
服务器请求仅首次每次
短链接适用性❌ 不推荐✅ 推荐

短链接服务选择 302 的理由

  1. URL 可能需要修改
  2. 需要准确的点击统计
  3. 需要权限控制和状态检查

练习题

练习 1

如果用户反馈说修改了目标 URL 但短链接还是跳转到旧地址,可能是什么原因?如何解决?

参考答案

可能原因

  1. 使用了 301 重定向

    • 浏览器缓存了 301 重定向
    • 再次访问时直接从缓存跳转,不请求服务器
  2. CDN 缓存了重定向

    • CDN 节点缓存了 301 响应
    • 用户从 CDN 获取缓存的重定向
  3. 中间代理缓存

    • 公司/学校网络的代理服务器缓存了响应

解决方案

  1. 改用 302 重定向

    return redirect(long_url, code=302)  # 而不是 301
  2. 添加缓存控制头

    response = redirect(long_url, code=302)
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response
  3. 清除 CDN 缓存

    • 在 CDN 控制台刷新缓存
    • 配置 CDN 不缓存 302 响应
  4. 通知用户清除浏览器缓存

    • 临时方案:强制刷新(Ctrl+F5)
    • 或等待缓存自然过期

练习 2

如何在使用 302 的同时优化性能?

参考答案

优化策略

import redis
from flask import Flask, redirect, request

app = Flask(__name__)
redis_client = redis.Redis()

@app.route('/<short_code>')
def short_url_redirect(short_code):
    cache_key = f"url:{short_code}"

    # 1. 先查缓存(~1ms)
    long_url = redis_client.get(cache_key)

    if long_url is None:
        # 2. 缓存未命中,查数据库(~50ms)
        result = db.query(
            "SELECT long_url FROM url_mapping WHERE short_code = %s",
            (short_code,)
        )

        if not result:
            abort(404)

        long_url = result['long_url']

        # 3. 写入缓存(1 小时过期)
        redis_client.setex(cache_key, 3600, long_url)
    else:
        long_url = long_url.decode()

    # 4. 异步记录点击(不阻塞响应)
    record_click.delay(short_code, request)

    # 5. 返回 302(但响应很快,因为走了缓存)
    return redirect(long_url, code=302)

性能对比

方案平均响应时间数据库 QPS
直接查库~50ms10000
Redis 缓存~5ms100(99% 降低)

关键优化点

  1. Redis 缓存热点数据
  2. 点击统计异步处理
  3. 缓存 1 小时,平衡性能和灵活性

悬念:缓存能两全其美吗?

302 虽然好,但每次都要查询数据库。如果我的短链接服务每天有 1000 万次访问,数据库能承受吗?

我想到一个方案:用 Redis 缓存短链接映射。这样既保留了 302 的灵活性(可以修改、可以统计),又能获得接近 301 的性能。

但这又带来新问题:缓存和数据库之间如何保持一致?如果我修改了目标 URL,缓存什么时候更新?

下次我再深入探讨缓存方案。