一、前言
今天本来应该学习netty基础传输的相关内容,但是由于对基础知识掌握的不足,出现学习的瓶颈,先学习一下幂等性压压惊,晚上再梳理一下netty的相关内容,认认真真学习,争取明晚可以完成netty基础传输相关内容,今晚就看一下幂等性吧!
二、什么是幂等性?
百度百科的解释
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。
理解
所谓幂等性,就是无论执行多少次,都会只执行一次,多次的影响和一次是一样的,比如我们新增用户,无论执行多少次,都只会保存一次,避免了重复提交
幂等性运用的范围也是很多,比如以下几个:
- 保存用户信息,前端重复提交相同的数据,后端接口对于这个数据只会保存一次,无论重新提交多少次,也只会完成一次
- 用户支付,无论提交多少次,他只能有一次成功,只能扣一次钱
- 验证码,相同的的验证码只能发送一次,不能重复发送 等等
三、实现幂等性
1、实现的方式
- mysql的唯一索引,如果索引存在,就会抛出异常,也就保证了重复提交问题
- 悲观锁
- 乐观锁
- redis实现,将生成的token保存在redis中,提交之后,将token删除,重复发起请求,获取不到redis存储的token就无法重复提交,直接提示用户:重复保存
2、redis和lua实现幂等性
我们这里使用redis和Lua的方式实现,为什么使用lua?
因为lua是原子操作,原子操作从开始就要一直到执行结束,中间不会有线程切换,所以可以解决高并发的问题
2.1 不使用幂等性存在的问题
我们先看一下用户保存操作:
代码语言:javascript复制controller
@Log("用户创建")
@ApiOperation(value = "用户创建",notes = "用户创建")
@PostMapping("/createUser")
public GoflyResponse createUser(@Valid UserDto userDto) throws GoflyException {
User user = new User();
BeanUtils.copyProperties(userDto,user);
iUserService.createUser(user);
return new GoflyResponse().code(HttpStatus.OK).message("成功");
}
postman多次提交用户保存操作
提交三次之后,新增了同样的三个用户,所以这个是错误的,需要修改
代码语言:javascript复制| USER_ID | USERNAME | PASSWORD | NAME
| 9 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 杨羽茉
| 10 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 杨羽茉
| 11 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 杨羽茉
2.2 redis和lua解决问题
lua脚本----> checkidem.lua
放在resources目录下
代码语言:javascript复制//获取token----> redis: get token
local current = redis.call('GET',KEYS[1])
//如果不存在返回-1
if current == false then
return '-1'
end
//访问一次之后,删除token
local isdel = redis.call('DEL',KEYS[1])
//如果删除成功就返回'1'
if isdel == 1 then
return '1'
else
//删除失败,说明token不存在
return '0'
end
redis工具类------> RedisLuaUtil.java
用于运行lua脚本
代码语言:javascript复制/**
* redis解析lua工具类
*/
@Component
public class RedisLuaUtil {
//注入stringRedisTemplate
@Resource
private StringRedisTemplate stringRedisTemplate;
//运行lua脚本
public String runLuaScript(String luaFileName, List<String> keys){
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
//读取lua脚本文件
redisScript.setScriptSource(
new ResourceScriptSource(
new ClassPathResource(luaFileName)
)
);
//lua返回值类型
redisScript.setResultType(String.class);
//值参数
String argson = "none";
//执行lua脚本,获取返回值,lua返回的内容主要看
String result = stringRedisTemplate.execute(redisScript, keys, argson);
return result;
}
/**
* 校验token
* @param request
*/
public String checkToken(HttpServletRequest request) {
//获取token,从request中获取
String token = request.getParameter("token");
//将token存储到list中
List<String> keys = new ArrayList<>();
keys.add(token);
//调用运行lua脚本的方法,并且传入lua文件的名称及token集合
String result = this.runLuaScript("checkidem.lua", keys);
//根据自定义的lua脚本,会返回三种返回值:
//1. GET token 不存在时返回-1
//2. DEL token 成功时,返回1
//3. DEL token 不成功,返回0
if(result.equals("1")){
return "保存成功";
}else{
return "请勿重复提交";
}
}
}
代码语言:javascript复制生成token、模拟保存用户操作、测试
//自动注入redisTemplate,用于存储token到redis中
@Autowired
private StringRedisTemplate stringRedisTemplate;
//自动注入自定义的Redis Lua工具类
@Autowired
private RedisLuaUtil redisLuaUtil;
(1) 生成token
代码语言:javascript复制@GetMapping("/getToken")
public String getToken(){
//生成UUID,作为token
String result = UUID.randomUUID().toString();
//将生成的token保存到redis中
stringRedisTemplate.opsForValue().set(result,result);
//返回给用户
return result;
}
(2) 模拟保存用户操作
代码语言:javascript复制@GetMapping("/saveUser")
public String saveUser(HttpServletRequest request){
//执行自定义的RedisLuaUtil类,用于校验是否为重复保存
String result = redisLuaUtil.checkToken(request);
//返回给用户
return result;
}
(3) 测试
代码语言:javascript复制第一:首先调用/getToken操作,生成token
request:
http://localhost:8080/getToken
respones:
91a23e2b-52ab-4867-90bc-737f31bb7ac3
代码语言:javascript复制保存用户
request:
http://localhost:8080/saveUser?token=91a23e2b-52ab-4867-90bc-737f31bb7ac3
将生成的token作为参数传到后端
第一次提交:
respones:
保存成功
第二次提交:
respones:
请勿重复提交
四、运行流程
- 第一步:调用/getToken 生成UUID,保存到redis中,并且返回给用户 @GetMapping("/getToken") public String getToken(){ //生成token String result = UUID.randomUUID().toString(); //保存到redis stringRedisTemplate.opsForValue().set(result,result); return result; } 复制代码
- 第二步:将token作为参数,调用/saveUser 首先,调用redisLuaUtil中的checkToken方法,判断token是否存在 @GetMapping("/saveUser") public String saveUser(HttpServletRequest request){ //验证token是否存在redis中 String result = redisLuaUtil.checkToken(request); return result; } 复制代码 如果存在,lua会对redis中的token删除 local current = redis.call('GET',KEYS[1]) //如果不存在,返回-1,如果存在执行下面DEL操作 if current == false then return '-1' end local isdel = redis.call('DEL',KEYS[1]) if isdel == 1 then return '1' else return '0' end 复制代码 如果不存在,直接返回-1 如果删除成功,返回1 如果删除失败,返回0 然后,将结果返回给用户(后端根据返回-1 0 1,对用户做出响应)
总结起来,就是用户在访问其他接口的时候,需要先生成token并保存到redis中,返回将token作为参数传递到目标方法,如果redis中有这个token,返回可以访问的标志(比如1),并且删除redis中的这个token,当第二次访问的时候,redis就查询不到该token,就返回重复提交错误(比如 -1)