飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • HTTP 基础

    • 认识HTTP协议与应用层定位
    • HTTP消息格式与报文结构
    • HTTP请求方法与幂等性
    • HTTP状态码详解
  • 连接与缓存

    • HTTP持久连接与版本演进
    • HTTP缓存机制
  • 状态与协商

    • Cookie与Session状态管理
    • HTTP重定向与内容协商
    • HTTP条件请求与范围请求
  • 安全与加密

    • HTTP认证机制
    • HTTPS与TLS握手
  • 协议演进

    • HTTP2核心特性
    • HTTP3与QUIC
  • 架构与实战

    • HTTP代理服务器与Web缓存
    • HTTP常见攻击与防御
    • HTTP实践工具与抓包分析

答案:HTTP请求方法与幂等性

为什么银行转账接口通常用 POST 而不是 PUT?(提示:考虑幂等性,如果转账请求超时重发会怎样?)

核心原因:POST 非幂等,PUT 幂等,而转账业务天然不适合幂等语义。

先理解幂等性:幂等操作是指执行一次和执行多次效果相同。比如 PUT 更新用户昵称为"波比",执行 1 次和 100 次,结果都是昵称变成"波比",这就是幂等。

但转账完全不同。假设鸣哥要从飞翔公司账户给供应商转账 10 万元:

  • 执行 1 次:账户扣 10 万,供应商收到 10 万 ✓
  • 执行 2 次:账户扣 20 万,供应商收到 20 万 ✗(灾难!)

如果转账接口用 PUT(幂等),网络超时后客户端自动重试,钱就被转了两遍。虽然可以用"请求唯一 ID + 服务端去重"来 hack,但这违背了 PUT 的幂等语义,属于用错误的 HTTP 方法表达业务。

POST 是非幂等的,语义就是"提交一次就处理一次"。超时重发时,服务端如果已经处理过第一次请求,第二次会再次处理——这正好符合银行的风控逻辑:每笔转账必须有独立的流水号,重复提交会被识别为重复交易并拒绝(通过业务层幂等,而非 HTTP 层幂等)。

# 银行转账接口设计(POST)
@app.route('/transfer', methods=['POST'])
def transfer():
    req_id = request.headers.get('X-Request-Id')  # 唯一请求 ID
    amount = request.json.get('amount')
    
    # 业务层幂等:检查 req_id 是否已处理
    if redis.exists(f"transfer:{req_id}"):
        return jsonify({"code": 0, "message": "已处理", "duplicate": True})
    
    # 执行转账
    result = bank_service.transfer(amount)
    redis.setex(f"transfer:{req_id}", 3600, "done")  # 标记已处理
    return jsonify({"code": 0, "message": "转账成功"})

凌叔总结得好:"HTTP 方法的幂等性是语义约束,不是技术保证。银行用 POST 是因为转账业务本身非幂等,然后他们在业务层自己做幂等控制——这是架构的分层思想。"


靓晴要设计一个"点赞"功能,用 POST 还是 PUT 更合适?点赞两次应该算两次赞还是一次赞?

应该用 POST,但点赞两次应该算一次赞(即取消点赞),这需要业务层设计,不是 HTTP 方法能单独决定的。

先分析两种设计思路:

方案 A:POST(创建点赞记录)

POST /api/moments/9527/like HTTP/1.1

每次调用都创建一条点赞记录。点两次 = 两条记录 = 两个赞。这在某些场景下合理(比如雁姐发的年会照片,云吞和波比都可以点,每人只能点一次,但多人可以累积)。

方案 B:PUT(更新点赞状态)

PUT /api/moments/9527/like HTTP/1.1

{"liked": true}

幂等设计:无论调用多少次,最终状态都是"已点赞"。再发一个 {"liked": false} 就是取消点赞。

靓晴的实际选择:

飞翔公司朋友圈的点赞功能,靓晴最终选择了 POST + 业务层去重 的方案:

  1. 用户第一次点击,POST 创建点赞记录,图标变红,计数 +1。
  2. 用户第二次点击(同一用户、同一内容),服务端检测到已存在记录,删除该记录,图标变灰,计数 -1。
// 前端代码示意
async function toggleLike(momentId) {
  const res = await fetch(`/api/moments/${momentId}/like`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });
  const data = await res.json();
  // data.action 可能是 "liked" 或 "unliked"
  updateUI(data.action === 'liked');
}
# 后端代码示意
@app.route('/api/moments/<id>/like', methods=['POST'])
def toggle_like(id):
    user_id = get_current_user_id()
    existing = db.query("SELECT * FROM likes WHERE moment_id=? AND user_id=?", id, user_id)
    
    if existing:
        db.execute("DELETE FROM likes WHERE id=?", existing.id)
        return jsonify({"action": "unliked", "count": get_like_count(id)})
    else:
        db.execute("INSERT INTO likes (moment_id, user_id) VALUES (?, ?)", id, user_id)
        return jsonify({"action": "liked", "count": get_like_count(id)})

靓晴的设计哲学是:POST 表达"用户做了一个点赞动作",至于这个动作最终是创建还是删除,由业务层根据当前状态决定。这比单纯追求 HTTP 方法的幂等性更符合用户直觉——用户只想"点一下按钮",不关心底层是 INSERT 还是 DELETE。


假设凌叔写了一个自动重试机制:网络超时后自动重发请求。哪些方法是"重试安全"的?哪些需要额外处理?

重试安全的方法(幂等方法):

方法是否重试安全原因
GET✓ 安全获取资源,多次执行结果相同,不会修改数据。星宇查 10 次航班信息,数据库不会变。
HEAD✓ 安全和 GET 一样,只是不返回主体。
OPTIONS✓ 安全查询服务器支持的方法,纯查询。
PUT✓ 安全完整替换资源,多次替换结果相同。翼王更新公告标题为"年会通知",重试 100 次还是这个标题。
DELETE✓ 安全删除资源,第一次删除成功,后续返回 404 或 204,数据状态一致(都是已删除)。

非重试安全的方法(需要额外处理):

方法是否安全原因
POST✗ 不安全每次调用都可能创建新资源。波比重试报名,可能产生两条报名记录。
PATCH△ 视情况而定如果是"绝对值更新"(如 {"status": "approved"})则幂等;如果是"增量更新"(如 {"balance": +100})则非幂等。

凌叔的重试机制设计:

import time
import requests

# 幂等方法:直接重试,无需额外处理
IDEMPOTENT_METHODS = {'GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'}

def safe_request(method, url, **kwargs):
    max_retries = 3
    for attempt in range(max_retries):
        try:
            resp = requests.request(method, url, timeout=5, **kwargs)
            return resp
        except requests.Timeout:
            if method.upper() in IDEMPOTENT_METHODS:
                # 幂等方法:直接重试
                time.sleep(2 ** attempt)  # 指数退避
                continue
            else:
                # POST/PATCH 等非幂等方法:不能盲目重试
                raise NonIdempotentRetryError(
                    f"{method} 请求超时,为避免副作用,禁止自动重试。"
                    f"请检查服务端是否已处理,或使用唯一请求 ID 查询状态。"
                )
    return resp

# 使用示例
# GET 请求超时会自动重试
resp = safe_request('GET', 'http://oa.feixiang.net/api/flights')

# POST 请求超时不会自动重试,需要业务层处理
# 正确做法:先查询状态,确认未处理后再决定是否重试

额外处理策略(以 POST 为例):

  1. 唯一请求 ID:波比提交报名前生成 UUID,服务端记录"已处理的请求 ID 集合"。超时后波比用同一个 ID 重试,服务端返回"已处理"。
  2. 先查询再重试:POST 超时后,先 GET 查询报名列表,如果已存在则无需重试。
  3. 状态机:服务端设计"处理中"状态,客户端超时后查询状态,如果是"处理中"则轮询等待,而非直接重发 POST。

凌叔在飞翔公司的微服务架构中,给所有非幂等请求都强制加上了 X-Idempotency-Key 首部,这是从多次生产事故中总结出的铁律。