幂等操作大概讲解

幂等操作

一、幂等概念

1、幂等的数学概念

如果在一元运算中,x 为某集合中的任意数,如果满足 f(x) = f(f(x)) ,那么该 f 运算具有幂等性。

绝对值运算 abs(a) = abs(abs(a)) 就是幂等性函数

如果在二元运算中,x 为某集合中的任意数,如果满足 f(x,x) = x,前提是 f 运算的两个参数均为 x,那么我们称 f 运算也有幂等性。

求大值函数 max(x,x) = x 就是幂等性函数 

2、幂等的业务概念

就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

  • 场景1:支付场景

    用户购买商品使用支付宝支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了。因此需要对于每一笔订单,操作多次,也只能扣一次钱。

  • 场景2:一键三连

    小破站有一个一键三连的功能,长按可以对up主进行激励,每个人对每个视频只有一个一键三连的机会。就算再喜欢某个视频,多次操作,也只能有一键三连一次。

  • 场景3:统计DAU/MAU

    DAU/MAU,又叫日活/月活,是用于反映网站、互联网应用或网络游戏的运营情况的统计指标。所以一个用户当天或者当月登录多次(或者达到某种活跃用户判断机制多次),也只能看作一个活跃用户,不能重复计算。

在增删改查4个操作中,尤为注意就是增加和修改,
(1) 查询对于结果是不会有改变的,
(2) 删除只会进行一次,用户多次点击产生的结果一样,
(3) 修改在大多场景下结果一样
(4) 增加在重复提交的场景下会出现

3、幂等概述

生产环境经常出现过重复的数据?在排查问题的时候,数据又是正常的。这个是何解呢?怎么会出现这种情况,而且还很难排查问题。

原因 :产生重复数据或数据不一致(假定程序业务代码没问题),绝大部分就是发生了重复的请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:(本质上:多次请求)

1)微服务场景,在我们传统应用架构中调用接口,要么成功,要么失败。但是在微服务架构下,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。
2)用户交互的时候多次点击。如:快速点击按钮多次。
3)MQ消息中间件,消息重复消费。
4)第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调。
5)其他中间件/应用服务根据自身的特性,也有可能进行重试。

接口幂等:接口的幂等性实际上就是 接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。更准确的讲:多次调用对系统的产生的影响是一样的,即对资源的作用是一样的,但是返回值允许不同。

4、幂等场景

  • 查询,select * from user where xxx,不会对数据产生任何变化,具备幂等性

  • 新增,insert into user(userid, name) values(1, ‘a’)

    如 userid 为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性
    如 userid 不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性
    
  • 修改,区分直接赋值和计算赋值

    直接赋值,update user set point = 20 where userid = 1,不管执行多少次,point都一样,具备幂等性
    计算赋值,update user set point = point + 20 where userid = 1,每次操作 point 数据都不一样,不具备幂等性
    
  • 删除,delete from user where userid = 1,多次操作,结果一样,具备幂等性

上面场景中,我们发现新增没有唯一主键约束的数据,和修改计算赋值型操作都不具备幂等性

在这里插入图片描述

二、使用乐观锁实现幂等

1、锁的定义

不管是互斥锁、自旋锁、重入锁、读写锁、行锁、表锁等等等等这些概念,我把他们都归纳为两种类型,乐观锁和悲观锁。

  • 乐观锁

    乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。

  • 悲观锁

    悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁

在这里插入图片描述

​ 举个例子,有时候我们上公共厕所的时候要排队。如果你蹲马桶的时候开着门,外面有人排着队看着你。你会这么做吗?当然,如果在自己家里,有可能会这么干,这就是乐观锁。虽然,能进到房间,但是有人占着坑位,该排队还是得排队。比如数据库提供的类似于write_condition机制,Java API 并发工具包下面的原子变量类就是使用了乐观锁的CAS来实现的。
​ 悲观锁就不同了,就相当于是进房间之后,第一件事就是把门锁上,那在门外排队等候的人不知道里面发生了什么,又着急但是又只能干等着,这就是悲观锁。比如行锁、表锁、读锁、写锁,都是在操作之前先上锁,Java API中的synchronized和ReentrantLock等独占锁都是悲观锁思想的实现。

  • 锁的应用场景

    根据前面对两种锁的介绍,总结一下两种锁的应用场景:
    	乐观锁,它适用于写少读多的情况,也就是说减少操作冲突,这样可以省去锁竞争的开销,提高系统的吞吐量。
    	悲观锁,它适用于写多读少的情况。因为,如果还使用乐观锁的话,会经常出现操作冲突,这样会导致应用层会不断地Retry,反而会降低系统性能。
    
    

2、使用乐观锁实现幂等

  • 在要进行添加和修改的表中增加一个版本号列
    在这里插入图片描述

  • 进行修改操作

    在执行update命令前,需要先将要修改的数据对象查出来(不是点击列表中的查询按钮,在进后页面后自动查一次),将查到的版本号做为修改的条件。

    查询代码略

    update users set money=money+300,version=version+1 where id=5 and version=1  -- version=1中的1为查询出来的版本号
    

    加上了版本号后,就让此计算赋值型业务,具备了幂等性

缺点:就是在操作业务前,需要先查询出当前的version版本。

三、使用token+redis实现幂等【日常使用】

token + redis 的幂等方案,适用于绝大部分场景。token 模式主要是为了防重的。需要前后端进行一定程度的交互来完成。需要利用到 Redis。

在这里插入图片描述

具体流程步骤:
 a、客户端会先发送一个请求去获取token,服务端会生成一个全局唯一的ID作为token保存在redis中,同时把这个ID返回给客户端客户端
 b、第二次调用业务请求的时候必须携带这个token
 c、服务端会校验这个token,如果校验成功,则执行业务,并删除redis中的token
 d、如果校验失败,说明redis中已经没有对应的token,则表示重复操作,直接返回指定的结果给客户端

在这里插入图片描述在这里插入图片描述

1、案例实现

a、创建项目

创建一个页面,用来模拟修改数据的过程

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>

    <h3>提交修改后的数据</h3>
    <form action="/update" method="get">
        <p>姓名:<input type="text" name="username" value="tom"> </p>
        <p>性别:<input type="text" name="sex" value="sex"> </p>
        <!--实际开发中将返回的唯一id保存到隐藏表单中-->
        <p>获得的token:<input type="text" id="token" name="token" value=""> </p>
        <button type="submit">提交修改数据</button>
    </form>

<script type="text/javascript">
    //修改页面一刷新就请求一次token
        axios.get('/getToken',{})
            .then(result=>{
                document.getElementById("token").value=result.data.msg;
        }).catch(error=>{
            console.info(error);
        })
</script>
</body>
</html>
b、java代码
@RestController
public class UserController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 生成唯一id
     * @return
     */
    @GetMapping("/getToken")
    public ResponseResult<String> getUserById(){
        //生成唯一id
        String powerid= UUID.randomUUID().toString();
        //将唯一id保存到redis
        ValueOperations<String, String> operations =redisTemplate.opsForValue();
        operations.set(powerid, powerid,2,TimeUnit.MINUTES); //键保存2分钟
        //将id返回到页面
        return new ResponseResult<>(200, powerid);
    }

    /**
     * 根据id查询用户信息
     * @return
     */
    @GetMapping("/update")
    public ResponseResult<String> updateUser(@RequestParam("username") String username,
                                             @RequestParam("sex") String sex,
                                             @RequestParam("token") String token){

        //判断redis中是否有表单提交中的id
        if(!redisTemplate.hasKey(token)){
            //为true说明是第一次提交,唯一id还在redis中
            //为false说明不是第一次提交,唯一id已在redis中被删除了
            return new ResponseResult<>(1001, "不能进行重复提交");
        }

        //删除当前token
        redisTemplate.delete(token);

        //执行修改流程

        //返回结果
        return  new ResponseResult<>(200, "修改成功");
    }
}
        return new ResponseResult<>(1001, "不能进行重复提交");
    }

    //删除当前token
    redisTemplate.delete(token);

    //执行修改流程

    //返回结果
    return  new ResponseResult<>(200, "修改成功");
}

}


<think>好的,我现在需要帮助用户理解HTTP请求的等性。首先,我得回忆一下等性的定义。根据用户提供的引用[1],HTTP/1.1中定义的等性是指一次或多次请求同一资源会产生同样的结果,除了可能的网络问题外。也就是说,第一次请求可能会有副作用,但后续的多次请求不会对资源产生额外的影响。 接下来,我应该解释这个概念,并用例子来说明。比如,GET方法是等的,因为它只是获取数据,不会改变服务器状态。而POST方法通常不是等的,比如提交表单多次可能会创建多个资源。但是PUT和DELETE是等的,因为多次调用PUT更新同一资源结果是一样的,DELETE删除一次后资源就不存在了,再删除结果也一样。 然后,用户可能想知道如何解决非等性操作的问题。根据引用[2],银联的付款接口使用source和seq作为唯一索引,防止重复处理。这给了我一个思路,解决方案可能包括唯一标识符、版本号控制、使用等方法等。需要将这些方法详细说明,比如唯一标识符如何生成,如何在后端处理,版本号如何防止覆盖旧数据等。 另外,用户提到grpc的超时和重试机制,可能这也是解决等性的一种方式,比如在超时后重试时确保请求不会重复处理。不过这可能更多涉及重试策略而非等性本身,但可以简要提及。 还要注意用户提供的格式要求,行内数学公式用$...$,独立公式用$$...$$,不过目前内容中可能暂时用不到数学公式,但如果有需要的话需要正确应用。比如在讲解版本号时,可以举例版本号的格式,但可能不需要数学表达式。 最后,生成相关问题,需要围绕等性展开,比如定义、应用场景、解决方案、与重试机制的关系,以及实际案例。确保问题覆盖用户可能感兴趣的各个方面。 总结步骤: 1. 定义等性,引用[1]。 2. 举例不同HTTP方法的等性。 3. 解决方案:唯一标识、版本号、等方法、状态机、防重表,引用[2]的例子。 4. 相关问题生成,覆盖定义、示例、解决方案、应用场景等。</think>### HTTP请求等性解析 #### 一、定义 等性指**多次执行相同操作与执行一次的效果相同**。在HTTP协议中,若一个请求方法多次调用产生的副作用与单次调用相同,则称该方法具有等性[^1]。数学表达可写为: $$f(f(x)) = f(x)$$ #### 二、HTTP方法示例 | 方法 | 等性 | 示例说明 | |--------|--------|------------------------------| | GET | ✔️ | 多次获取商品详情不会改变数据 | | PUT | ✔️ | 重复更新同一资源最终状态一致 | | DELETE | ✔️ | 首次删除后资源已不存在 | | POST | ✖️ | 重复提交订单会创建多个实例 | #### 三、解决方案 1. **唯一标识符** 为请求添加唯一组合标识(如:$source + timestamp + seq$),通过数据库唯一索引拦截重复请求[^2]。 ```sql CREATE UNIQUE INDEX idx_payment ON transactions(source, seq); ``` 2. **版本号控制** 更新操作携带版本号,使用CAS(Compare and Set)机制: ```http PUT /resource/123?version=5 ``` 3. **状态机约束** 定义业务状态流转规则(如:已支付订单不可重复支付),拒绝非法状态变更。 4. **等方法设计** (1)查询代替更新 (2)增量式更新代替全量覆盖 (3)使用PUT替代POST进行更新 5. **防重表** 建立请求指纹记录表,采用异步队列处理降低并发冲突。 #### 四、典型应用场景 1. 支付系统重复付款拦截 2. 库存超卖预防 3. 分布式系统重试机制 4. 消息队列重复消费处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖成范德彪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值