java http重复提交_如何处理HTTP接口中的防重复提交

点击上方 IT牧场 ,选择 置顶或者星标

技术干货每日送达

00f2f480204ad768a4fca4a3de05396f.png

关于防重复提交

由于本人从事电商开发工作,项目中面对C端用户或多或少都会接触到提交保存或者修改的请求,例如创建订单,物流包裹签收,团员通知自提消息发送,这些接口因为涉及到数据库的保存或者修改,如果不做防重复提交,那么数据库要么增加无用的数据,或者出现错误的逻辑,要么消息重复发送造成用户骚扰这些不良后果。所以通用的防重复非常有必要,拦掉无效操作,也能避免程序出现错误。

关于防重复提交可以在前端做,也可以在后台做。本人从事后台开发工作,所以这里只讲后台如何防重复提交。

重复提交的定义

同一用户,同样参数,针对同一个http接口发起操作,由于网络抖动、弱网、用户误点,或者后端请求处理慢等造成服务端在同一时间收到用户的多次请求。

针对上述定义,我们可以有目的的设计我们的拦截规则。话不多说,下面撸实现,并配合jmeter来压测观察防重效果。

案例展示

项目环境准备springboot+redis来演示。

首先我们展示如果没有重复提交,demo会出现的问题。

@RestControllerpublic class OrderController{private Map map = new HashMap<>();@GetMapping("/createOrder/{userId}/{num}")public String createOrder(@PathVariable Integer userId,@PathVariable Integer num) throws InterruptedException {//这里模拟用户限购 假设每个用户只能买一个if (num > 1) {return "每个用户只能购买一份";}//通过map来模拟读库是否购买过if (map.get(userId) != null) {return "已经购买过了,无法再次购买";}//模拟创建订单逻辑 方便观察效果map.put(userId, num);return "创建订单成功,订单号为:" + new Random().nextInt(10000);}}

启动项目 jmeter压测 配置如下

1185dd9d2325ed6a2dc4f23176cfe49d.png

37644daf80b3429f504ac604aa6cb945.png

启动线程组测试

44cd7a1e082b32215297ddb807cfd20e.png

62c4575315792cbb1cbb2009b48da539.png

发现10个并发请求,第三和第四个请求都返回了订单号,其余都返回的是”已经购买过了,无法再次购买”说明在并发情况下,逻辑出现了问题,并不能保证对同一用户限购。

防重复提交逻辑开发

定义如下注解,如果有小伙伴们不知道怎么定义注解,这种请自行百度或者谷歌吧

@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface PreventDuplicateSubmission {}

为配合注解使用,我们可以自定义切面来实现防重逻辑

首先实现个工具类 redis单机分布式锁

@Componentpublic class RedisTemplateUtil {public static RedisTemplate staticRedisTemplate;@Autowiredprivate RedisTemplate redisTemplate;/*** 删除分布式锁lua脚本*/private final static String unlockScript = "if tostring(redis.call('get', KEYS[1])) == tostring(ARGV[1]) " +"then return redis.call('del', KEYS[1]) else return 0 end";@PostConstructpublic void postConstruct() {staticRedisTemplate = redisTemplate;}public static boolean lock(String lockkey, String certificate, int timeout) {return (boolean) (staticRedisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {Jedis jedis = (Jedis) connection.getNativeConnection();String result = jedis.set(lockkey, JSON.toJSONString(certificate), "nx", "ex", timeout);return "OK".equals(result) ? true : false;}}));}public static void unlock(String lockkey, String certificate) {staticRedisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {Jedis jedis = (Jedis) connection.getNativeConnection();return jedis.eval(unlockScript, Collections.singletonList(lockkey),Collections.singletonList(JSON.toJSONString(certificate)));}});}}

再来实现切面逻辑

@Aspect@Componentpublic class PreventDuplicateSubmissionAspect {@Autowiredprivate HttpServletRequest request;@Pointcut("execution (* com.iyd.demoapp.controller..*.*(..)) && @annotation(preventDuplicateSubmission)")public void preventDuplicateSubmissionAspectPointcut(PreventDuplicateSubmission preventDuplicateSubmission) {}@Around(value = "preventDuplicateSubmissionAspectPointcut(preventDuplicateSubmission)")public Object preventDuplicateSubmissionAspectAround(ProceedingJoinPoint invocation,PreventDuplicateSubmission preventDuplicateSubmission) throws Throwable {//获取切面传入参数 这里是http请求参数Object[] args = invocation.getArgs();//同一路径String lockKeyParam = request.getRequestURI();if (args != null) {List list = Arrays.asList(args);JSONArray jsonArray = new JSONArray(list);//同一参数 由于demo中请求的参数包含userId信息 这里同一参数中已经包含同一用户// 这里如果有自己的用户信息可以自己拼装来保证同一用户lockKeyParam += jsonArray.toJSONString();}String lockkey = SecureUtil.md5(lockKeyParam);String certificate = UUID.randomUUID().toString();boolean lock = RedisTemplateUtil.lock(lockkey, certificate, 120);if (!lock) {//没有获取到锁 请求正在处理中return "请求正在处理中";}try {return invocation.proceed();} finally {RedisTemplateUtil.unlock(lockkey, certificate);}}}

最后是在需要防重复提交的方法上加上注解

@RestControllerpublic class OrderController{private Map map = new HashMap<>();@PreventDuplicateSubmission@GetMapping("/createOrder/{userId}/{num}")public String createOrder(@PathVariable Integer userId,@PathVariable Integer num) throws InterruptedException {//这里模拟用户限购 假设每个用户只能买一个if (num > 1) {return "每个用户只能购买一份";}//通过map来模拟读库是否购买过if (map.get(userId) != null) {return "已经购买过了,无法再次购买";}//模拟创建订单逻辑 方便观察效果map.put(userId, num);return "创建订单成功,订单号为:" + new Random().nextInt(10000);}}

此时我们用jmeter来压测,不管你怎么压测,对于这个demo来说,只能有一个请求返回”创建订单成功,订单号XXX”。其余要么返回”请求正在处理中”,要么就是”已经购买过了,无法再次购买”,此时防重复提交已经很好的实现你的功能。

总结与思考

这里做几点思考,切面加锁时间如何考虑?如果请求参数没有用户信息,或者说请求参数不在请求体里面如何处理?比如保存收货地址这种,我们改如何考虑防重?对于创建订单来说,如果页面不返回上一级菜单,再次提交如何返回同一个订单号去拉起支付?下篇接着优化。。敬请期待!

干货分享

最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!

•001:《Java并发与高并发解决方案》学习笔记;•002:《深入JVM内核——原理、诊断与优化》学习笔记;•003:《Java面试宝典》•004:《Docker开源书》•005:《Kubernetes开源书》•006:《DDD速成(领域驱动设计速成)》•007:全部•008:加技术群讨论

近期热文

关注我

喜欢就点个"在看"呗^_^

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值