接口幂等性

目录

什么是接口幂等性

场景一:唯一索引保证幂等

实现思路

表结构设计

场景二:乐观锁控制并发修改

实现思路

表结构设计

代码演示


什么是接口幂等性

接口幂等性指:

无论调用多少次接口,产生的效果都是一样的,不会因为多次调用而导致副作用的叠加。换句话说:调用一次和调用多次的结果相同,系统状态不变。

通常来说,实现接口幂等性的常见方法主要有这两种:

1、唯一索引保证幂等

给关键业务字段(比如订单号、请求ID、用户ID+操作类型等)设置数据库唯一索引,数据库层面自动防止重复写入。

如果重复请求到来,由于唯一索引冲突,会报错或直接拒绝,保证接口调用的幂等性。

这是最简单且常用的幂等实现方式,尤其适合“写”操作。

唯一索引更适合保证“同一资源唯一创建”,防止重复插入。

2、乐观锁控制并发修改

在数据表中加版本号(version)字段,每次更新时带上版本号条件,比如 WHERE id=? AND version=?。

更新成功后版本号+1,如果版本号不匹配说明已有其他修改,当前更新失败。

适合并发高且需要避免重复操作或者修改覆盖的场景。

常用来防止重复支付、重复修改等问题。

乐观锁适合防止并发修改时数据冲突,保证操作的正确顺序和一致性

场景一:唯一索引保证幂等

用户注册账号时,重复点击注册按钮等情况

实现思路

前端调用注册接口,传入用户名、邮箱、密码。

后端先不额外查询是否存在,而是直接尝试插入新用户数据。

由于用户名和邮箱字段有唯一约束,数据库会自动防止重复插入。

如果插入成功,返回注册成功。

如果因唯一约束冲突导致插入失败,捕获异常,返回“用户名或邮箱已被注册”提示。

这样保证接口幂等,避免重复注册。

    表结构设计

    from sqlalchemy import Column, Integer, String, DateTime, UniqueConstraint
    from sqlalchemy.orm import declarative_base
    from datetime import datetime
    
    Base = declarative_base()
    
    class User(Base):
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True, autoincrement=True)
        username = Column(String(50), nullable=False, unique=True)   # 用户名唯一
        email = Column(String(100), nullable=False, unique=True)     # 邮箱唯一
        password_hash = Column(String(255), nullable=False)          # 密码加密存储
        created_at = Column(DateTime, default=datetime.utcnow)
    

    场景二:乐观锁控制并发修改

    用户下单后点击“支付”按钮,系统会调用后端接口从其账户余额中扣除相应金额用于支付该订单。
    设定场景如下:

    用户余额:¥900,某一笔未支付订单金额:¥100
    如果该支付接口没有实现幂等性控制,可能会导致用户余额被重复扣除:

            假设用户原本余额为 ¥900,提交一次消费请求消费 ¥100,预期剩余为 ¥800。由于网络卡顿或重试机制,同一个订单的扣款请求被执行了两次。系统没有幂等性控制,结果被重复扣款,余额从 900 → 800 → 700

    实现思路

    在接口幂等性中,核心目标就是:识别重复请求,并“去重”,避免业务逻辑重复执行。本质是通过“唯一标识(id)”+“判断是否已执行(version)”来控制流程,从而达到幂等性。

    并发请求场景下的乐观锁处理流程示意图:

    接口幂等性_第1张图片

    表结构设计

    from sqlalchemy import Column, Integer, String, Float, DateTime, UniqueConstraint
    from sqlalchemy.orm import declarative_base
    from datetime import datetime
    
    Base = declarative_base()
    
    class User(Base):
        __tablename__ = 'user'
        id = Column(Integer, primary_key=True)
        balance = Column(Float, default=0)
    
    class Order(Base):
        __tablename__ = 'orders'
        id = Column(Integer, primary_key=True, autoincrement=True)
        order_id = Column(String(64), unique=True, nullable=False)
        user_id = Column(Integer, nullable=False)
        amount = Column(Float, nullable=False)
        status = Column(String(20), default="pending")  # pending / paid
        version = Column(Integer, default=0)  # 乐观锁字段
    
        __table_args__ = (
            UniqueConstraint('order_id', name='uq_order_id'),
        )
    

    代码演示

    from fastapi import FastAPI, HTTPException
    from sqlalchemy.orm import Session
    from sqlalchemy.exc import SQLAlchemyError
    from models import User, Order
    from database import get_db
    from pydantic import BaseModel
    
    app = FastAPI()
    
    class PayRequest(BaseModel):
        order_id: str
        user_id: int
    
    @app.post("/pay")
    def pay(req: PayRequest, db: Session = get_db()):
        try:
            # 查询订单
            order = db.query(Order).filter_by(order_id=req.order_id).first()
            if not order or order.user_id != req.user_id:
                raise HTTPException(status_code=404, detail="订单不存在")
    
            if order.status != "pending":
                return {"message": "订单已处理,重复请求"}
    
            # 查询用户余额
            user = db.query(User).filter_by(id=req.user_id).first()
            if not user:
                raise HTTPException(status_code=404, detail="用户不存在")
    
            if user.balance < order.amount:
                raise HTTPException(status_code=400, detail="余额不足")
    
            # 幂等关键步骤:乐观锁更新订单状态
            rows = db.query(Order).filter_by(order_id=req.order_id, version=order.version, status="pending")\
                .update({
                    "status": "paid",
                    "version": order.version + 1
                })
    
            if rows == 0:
                raise HTTPException(status_code=409, detail="订单可能已被并发处理")
    
            # 扣款操作(和订单状态更新在同一事务中)
            user.balance -= order.amount
    
            db.commit()
            return {"message": "支付成功,已扣除余额 ¥%.2f" % order.amount}
    
        except SQLAlchemyError:
            db.rollback()
            raise HTTPException(status_code=500, detail="系统异常,支付失败")
    

    你可能感兴趣的:(python,数据库,python,并发,乐观锁,幂等性)