幂等性
幂等(idempotent、idempotence)是一个数学与计算机
所谓的幂等性,是分布式环境下的一个常见问题,一般是指我们在进行多次操作时,所得到的结果是一样的,即多次运算结果是一致的。
学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
也就是说,用户对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的,不会因为多次点击而产生副作用。
一句话,幂等就是一个执行操作,无论执行多少次,产生的效果和返回的结果都是一样的。
幂等性,是开发人员在日常开发中必须要考虑的,尤其是转账、支付等涉及金额交易的场景,如果出现幂等性的问题,造成的后果是非常严重的。
常见幂等性操作
我们进行代码实现时,常见的请求有如下几种,他们的幂等性如下:
- select查询天然幂
- delete删除也是幂等,删除同一个数据多次其效果一样;
- update直接更新某个值时,幂等;
- update更新累加操作的的结果,非幂等;
- insert操作会每次都新增一条,非幂等;
什么情况下会产生重复提交(非幂等性)
以下几种情况会导致非幂等性的结果出现:
- 连续点击提交两次按钮;
- 点击刷新按钮;
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
- 使用浏览器历史记录重复提交表单;
- 浏览器重复地HTTP请求等。
- 超时重试,引起接口重复调用
- 定时任务设置不合理,导致数据重复处理
- 使用消息队列时,消息重复消费
为什么要实现幂等性?
如今随着互联网技术快速发展,业务越来越复杂,系统的高并发和关键数据的场景越来越多。
在分布式系统中,机器宕机和消息丢失也是需要重点关注的问题,其中的一个典型就是幂等性问题。
想想看,一个对外暴露的接口会面领很多次请求,如果不能保证幂等性会带来什么样的后果?
微信进行一次扣款操作,应该只扣用户一次钱,当遇到网络故障
或系统 bug,如果没有实现幂等性扣多了你会不会直接 “C 语言” 投诉?
当然,有些接口是天然保证幂等性的,比如查询操作、删除操作。有些对数据的修改是一个常量,无其他操作,也是具有幂等性的。修改操作可能幂等可能不幂等。
SELECT col1 FROM tab1 WHERE col2 = 2 UPDATE tab1 SET col1 = 1 WHERE col2 = 2 UPDATE tab1 SET col1 = col1 + 1 WHERE col2 = 2
这三个 sql 只有第三个不是幂等的。
POST 请求天生就不是一个幂等操作,每次调用都会在系统中产生新的资源,想要幂等就必须在业务中实现。
需要避免的是,幂等性和并发安全不是一回事。当同一笔订单即使你不停的提交支付,如果扣了不止一次钱,就说明该操作不幂等。
而有多笔订单同时进行支付,最后扣除的金额不是这么多笔金额的总和,说明该操作有并发安全问题。这是两个维度的问题,应该分开讨论解决。
如何保证幂等性
1.前端js提交禁止按钮可以用一些js组件
2.使用Post/Redirect/Get模式
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。
这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
3.借助数据库操作
insert唯一索引,保证插入的数据只有一条。另外也可以基于悲观锁或者乐观锁,先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求;如果没有存在,就证明是第一次进来,直接放行。
4.session机制(后台服务端)
在服务器端,生成一个唯一的标识符,将它存入session,同时将它写入表单的隐藏字段中,然后将表单页面发给浏览器,用户录入信息后点击提交。
另外在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,如果相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除,如果不相等即重复提交。
5.Redis token机制
每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证。如果验证通过删除token,下次请求再次判断token是否相等,如果不相等即重复提交。
Redis token的代码实现
接下来我就把Redis token方式的实现代码列出,如下所示:
package com.qfjy.project.meeting.util; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @ClassName RedisRepeatUtil * @Description TODO Redis解决重复提交 * @Author guoweixin * @Date 2022/4/19 * @Version 1.0 */ @Slf4j @Component public class RedisRepeatUtil { /**Redis中间件 @Autowired private RedisTemplate<String,Object> redisTemplate; /** * 会议发布页面KEY */ public static String MEETING_MEMETING_PUB_ADD_KEY="meeting:meetingPub:pageToken:"; /** * 会议抢单页面(进行页面)KEY */ public static String MEETING_MEETING_GRAB_ADD_KEY="meeting:meetingGrab:add:pageToken:"; /** * TODO 进入页面 生成token * 1、命令是根据进入页面的名称+用户ID * 2、过期时间是30分钟(为了避免产生无效的内存数据浪费) * @param key redis key * @return */ public String generToken(String key){ String uuid= UUID.randomUUID().toString(); log.info("uuid:"+uuid); //为期设置过期时 redisTemplate.opsForValue().set(key, uuid); redisTemplate.expire(key,30, TimeUnit.MINUTES); return uuid; } /** * TODO(解决重复提交) * 前端 token和后端redis token进行比较判断 * @param tokenUUID * @return */ public boolean compareToken(String key,String tokenUUID){ //删除key boolean flag=tokenUUID.equals((String) redisTemplate.opsForValue().get(key)); redisTemplate.delete(key); return flag; } }