spring boot:用redis+lua实现表单接口的幂等性(spring boot 2.2.0)
一,什么是幂等性?
1,幂等:
幂等操作:不管执行多少次,所产生的影响都和一次执行的影响相同。
幂等函数或幂等方法:可以使用相同的参数重复执行,并能获得相同的结果的函数/方法。
这些函数/方法不用担心重复执行会对系统造成改变。
2,幂等操作的一些例子:
前端重复提交相同的数据,后台只产生对应这个数据的一个相同的反应结果
发送验证码短信:应该只发一次,相同的验证短信不能多次发送。
生成订单:一个业务请求只能创建一个订单,不能重复创建相同的订单
用户付款:只能扣用户一次钱,不能重复扣费
3,实现幂等操作的一些方法:
unique索引
悲观锁
乐观锁
token机制
...
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/22/springboot-yong-redislua-shi-xian-biao-dan-jie-kou-de-mi-deng-xing-springboot220/
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔 邮箱: 371125307@qq.com
二,关于演示代码的说明:
1,项目的原理:
我们这里的演示的是表单提交时要避免重复提交相同的内容,
前端在用户点击提交按钮后,需要在后端返回结果之前,禁止用户再次点击提交按钮,
假如有请求绕过了前端的控制,直接向后端发送重复的相同请求,
后端如何避免?
在用户打开表单时,后端会生成一个token字符串,保存在redis后,传递给表单,
当表单提交时,这个字符串会再次提交到后端接口,
后端接口需要判断这个字符串是否在redis中存在?
如果不存在不允许提交,如果存在删除时能成功删除,允许提交,
删除时报错:表示已被其他进程删除,也不能允许提交.
2,项目在github的地址:
https://github.com/liuhongdi/idempotent
3,代码结构截图:
三,lua代码的说明:
checkidem.lua
local current = redis.call('GET', KEYS[1]) if current == false then --redis.log(redis.LOG_NOTICE,KEYS[1]..' is nil ') return '-1' end local isdel = redis.call('DEL', KEYS[1]) if isdel == 1 then --redis.log(redis.LOG_NOTICE,' del '..KEYS[1]..' success') return '1'; else --redis.log(redis.LOG_NOTICE,'del '..KEYS[1]..' failed') return '0'; end
如果当前token在redis中不存在,返回 -1
如果token存在,删除成功,返回1
删除失败,返回0
说明:为什么使用lua脚本?
redis上的lua脚本的执行是原子性的,不存在多个线程的并发问题,
使用lua脚本能保证不会出现重复的提交
四,java代码的说明:
1,RedisLuaUtil
@Service public class RedisLuaUtil { @Resource private StringRedisTemplate stringRedisTemplate; /* run a lua script luaFileName: lua file name,no path keyList: list for redis key return 0: delete fail -1: no this key 1: delete success */ public String runLuaScript(String luaFileName,List<String> keyList) { //System.out.println("redis script begin"); DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName))); redisScript.setResultType(String.class); String argsone = "none"; //System.out.println("execute begin"); String result = stringRedisTemplate.execute(redisScript, keyList,argsone); System.out.println("lua result:"+result); return result; } }
说明:
DefaultRedisScript:负责封装lua脚本
luaFileName: lua文件名
keyList: redis中的key列表,我们只需要传递token即可
stringRedisTemplate:负责执行脚本
argsone:值参数,我们传一个空字串即可
2,TokenServiceImpl.java中对redisLuaUtil类的调用
@Override public void checkToken(HttpServletRequest request) { String token = request.getHeader(TOKEN_NAME); if (StringUtils.isBlank(token)) {// header中不存在token token = request.getParameter(TOKEN_NAME); if (StringUtils.isBlank(token)) {// parameter中也不存在token //System.out.println("-----no token"); throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } } //System.out.println("runlua begin"); List<String> keyList = new ArrayList(); keyList.add(token); String res = redisLuaUtil.runLuaScript("checkidem.lua",keyList); if (res.equals("1")) { ServerResponseUtil.success("success"); } else { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } }
五,测试幂等性的检测是否生效?
提交表单前,先得到token
访问:
http://127.0.0.1:8080/order/gettoken
返回:
{"status":0,"msg":"8IgWPMtotKyzO13pnxCS9pc4","data":null}
用ab测试表单:
#-c:指定请求的并发数量
#-n:指定请求的总数量
[root@localhost etc]# ab -c 10 -n 10 http://127.0.0.1:8080/order/addorder?form_token=8IgWPMtotKyzO13pnxCS9pc4
查看代码中system.out.println的打印输出:
lua result:1 lua result:-1 lua result:-1 lua result:-1 lua result:-1 lua result:-1 lua result:-1 lua result:-1 lua result:-1 lua result:-1
可以看到只有一个是提交成功,其他的请求均给出了报错
六,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)