重定向类型
一个看似简单的选择
重定向逻辑写完了,但我盯着代码里的一个数字发呆:301。
return redirect(long_url, code=301) # 还是 302?我搜了半天,发现这个选择的影响比想象中大得多。301 还是 302,看起来只是改个数字,但会影响统计数据、用户体验,甚至 SEO。
HTTP 重定向状态码
HTTP 协议定义了多种重定向状态码:
| 状态码 | 名称 | 含义 |
|---|---|---|
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 303 | See Other | 查看其他位置 |
| 307 | Temporary Redirect | 临时重定向(保持方法) |
| 308 | Permanent Redirect | 永久重定向(保持方法) |
最常用的是 301 和 302。我需要理解它们的区别。
301 永久重定向
浏览器会缓存
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/original-long-url
Cache-Control: max-age=31536000301 表示资源已永久移动到新位置。浏览器会缓存这个重定向关系。
我做了个实验:
首次访问:
用户访问 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-cache302 表示资源临时从不同位置访问。浏览器不应缓存这个重定向关系。
每次访问:
用户访问 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 差一些。
| 指标 | 301 | 302 |
|---|---|---|
| 首次响应 | ~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 吗?
对于短链接服务,权重应该留在哪里?
我思考了一下:
短链接域名本身不需要 SEO:用户是通过其他渠道(社交媒体、短信)看到短链接的,不是通过搜索引擎搜索”短链接”找到的。
目标 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 或代理服务器,也不会缓存我的重定向响应。
本章小结
| 特性 | 301 | 302 |
|---|---|---|
| 语义 | 永久重定向 | 临时重定向 |
| 浏览器缓存 | ✅ 会缓存 | ❌ 不缓存 |
| SEO 权重 | 传递给目标 | 保留在原始 |
| 服务器请求 | 仅首次 | 每次 |
| 短链接适用性 | ❌ 不推荐 | ✅ 推荐 |
短链接服务选择 302 的理由:
- URL 可能需要修改
- 需要准确的点击统计
- 需要权限控制和状态检查
练习题
练习 1
如果用户反馈说修改了目标 URL 但短链接还是跳转到旧地址,可能是什么原因?如何解决?
可能原因:
使用了 301 重定向
- 浏览器缓存了 301 重定向
- 再次访问时直接从缓存跳转,不请求服务器
CDN 缓存了重定向
- CDN 节点缓存了 301 响应
- 用户从 CDN 获取缓存的重定向
中间代理缓存
- 公司/学校网络的代理服务器缓存了响应
解决方案:
改用 302 重定向
return redirect(long_url, code=302) # 而不是 301添加缓存控制头
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 缓存
- 在 CDN 控制台刷新缓存
- 配置 CDN 不缓存 302 响应
通知用户清除浏览器缓存
- 临时方案:强制刷新(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 |
|---|---|---|
| 直接查库 | ~50ms | 10000 |
| Redis 缓存 | ~5ms | 100(99% 降低) |
关键优化点:
- Redis 缓存热点数据
- 点击统计异步处理
- 缓存 1 小时,平衡性能和灵活性
悬念:缓存能两全其美吗?
302 虽然好,但每次都要查询数据库。如果我的短链接服务每天有 1000 万次访问,数据库能承受吗?
我想到一个方案:用 Redis 缓存短链接映射。这样既保留了 302 的灵活性(可以修改、可以统计),又能获得接近 301 的性能。
但这又带来新问题:缓存和数据库之间如何保持一致?如果我修改了目标 URL,缓存什么时候更新?
下次我再深入探讨缓存方案。