幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣了钱,流水记录也变成了两条,再或者新增用户表单注册时,用户反复提交表单.
简而言之:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是对数据库的影响只能是一次性的,不能重复处理
产生『重复数据或数据不一致』(假定程序业务代码没问题),绝大部分就是发生了重复的请求,重复请求是指『同一个请求因为某些原因被多次提交』。导致这个情况会有几种场景:
接口的幂等性实际上就是『接口可重复调用』,在调用方多次调用的情况下,接口『最终得到的结果是一致的』。
以『增删改查』四大操作来看,『删除』和『查询』操作天然是幂等的,没有(或不在乎)重复提交/重复请求问题。因为不管用户点击多少次删除操作或者是查询操作,也就是重复去调用查询接口或者是删除接口都不会有问题。因此,幂等需求通常是用在『新增』和『修改』类型的业务上。如用户注册表单的重复提交问题
而『修改』类型的业务通过 SQL 改造和 last_upated_at 字段的结合,也可以实现幂等,而无需下述的 token 和去重表方案。
因此,幂等性的处理重点集中在『新增』型业务上。
上述方案适用绝大部分场景。主要思想:
其实,这里的 token 起到的就是全局唯一 ID 的作用。
这里的重点在于:要先删除 token ,再执行业务代码 。
因为『后删除 token』的缺陷太致命:如果进行业务处理成功后,删除 redis 中的 token 失败了,那么 token 仍存在于 Redis 中,这时如果发起了第二次请求,那么因为 token 的存在,会认为该操作未被执行过,这样就导致了有可能会发生重复请求。
当然,『先删除 token』也有缺点,如果先删除 token 成功,而随后执行业务逻辑失败,那么需要再返回信息中告知请求方,在重新获得 token,而不能/无法重复利用之前的 token 。
业务代码如下:
1、导入相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
2、相关配置 application.yml
server:
port: 8080
spring:
application:
name: distribute-redis
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/k15?serverTimezone=UTC
redis:
host: 127.0.0.1
port: 6379
thymeleaf:
cache: true
check-template: true
check-template-location: true
content-type: text/html
enabled: true
encoding: UTF-8
excluded-view-names: ''
mode: HTML5
prefix: classpath:/templates/
suffix: .html
3、启动类
@SpringBootApplication
@MapperScan("com.woniu.outlet.mybatis")
public class DistributeRedisApplication {
public static void main(String[] args) {
SpringApplication.run(DistributeRedisApplication.class, args);
}
}
4、页面素材
resources/templates下,添加部门页面add-department.html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Hellotitle>
head>
<body>
<form th:action="@{/add}">
<p><input name="name" value="test-name"/>p>
<p><input name="location" value="test-location">p>
<p><input type="hidden" name="departmentToken" th:value="${token}">p>
<p>
<button type="submit">提交button>
p>
form>
body>
html>
success.html和失败页面略
5、controller相关代码
@Controller
public class DepartmentController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 当用户请求进入首页的时候 生成token,返回给页面,页面可以隐藏
* 同时存到redis 中
*当用户 提交 执行add方法时,先去redis查是否有,然后做完操作就删除,这样下次再
* 提交时,redis就没有该数据了 但是这样有什么问题呢 ?
* @return
*/
@RequestMapping("/addpage")
public String addDepartmnetPage(Model model) {
//生成一个0-1000的随机数
int random = new Random().nextInt(1000);
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("department-token" + random, random + "");
model.addAttribute("token", random + "");
return "add-department";
}
@RequestMapping("/add")
public String addDepartment(String name, String departmentToken) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
//1、先查token 是否存在
String redisToken = ops.get("department-token" + departmentToken);
if (!StringUtils.isEmpty(redisToken)) {
try {
System.out.println("保存到数据库");
//2、删除token
redisTemplate.delete("department-token" + departmentToken);
return "success";
} catch (Exception exception) {
return "failure";
}
} else {
return "failure";
}
}
}
上面的addDepartment()方法方式实现有没有什么问题呢?
当一个用户快速的点击两下提交,前一次进请求add和后一次进请求add 相差时间很短,那么第一个请求还没来得及删除的时候,第二个请求也能查到,那么又会往数据库保存一次。这还不是没有解决表单的重复提交问题吗?
改进措施:就是进请求的时候 就删除,不执行查询,即使删除失败也是返回false
@RequestMapping("/add")
public String addDepartment(String name, String departmentToken) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
//1、先查token 是否存在
if(redisTemplate.delete("department-token" + departmentToken)){
System.out.println("保存到数据库");
return "success";
}
return "failure";
}
另外在脚手架前后端分离的项目中,也可以当加载某个vue页面时,可以通过生命周期的钩子函数 如created 给服务器发请求,生成token返回给客户端浏览器