分享

java开发中 防止重复提交的几种方案

 hncdman 2022-12-21 发布于湖南

暴力小熊

已于 2022-07-01 16:13:00 修改

3624

 收藏 25

分类专栏: redis java 数据库 文章标签: java 前端 面试

版权

redis

同时被 3 个专栏收录

4 篇文章1 订阅

订阅专栏

java

25 篇文章2 订阅

订阅专栏

数据库

8 篇文章0 订阅

订阅专栏

开场白:老铁们对于文章有错误、不准确,或需要补充的请留言讨论 ,大家共同学习。如果觉得还不错的请关注、留言、点赞 、收藏。 创作不易,且看且珍惜

一、产生原因

对于重复提交的问题,主要由于重复点击或者网络重发请求, 我要先了解产生原因几种方式:

点击提交按钮两次;

点击刷新按钮;

使用浏览器后退按钮重复之前的操作,导致重复提交表单;

使用浏览器历史记录重复提交表单;

浏览器重复的HTTP请;

nginx重发等情况;

分布式RPC的try重发等点击提交按钮两次;

等… …

二、幂等

对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。

幂等:F(F(X)) = F(X)多次运算结果一致;简单点说就是对于完全相同的操作,操作一次与操作多次的结果是一样的。

在开发中,我们都会涉及到对数据库操作。例如:

select 查询天然幂等

delete 删除也是幂等,删除同一个多次效果一样

update 直接更新某个值(如:状态 字段固定值),幂等

update 更新累加操作(如:商品数量 字段),非幂等

(可以采用简单的乐观锁和悲观锁 个人更喜欢乐观锁。

乐观锁:数据库表加version字段的方式;

悲观锁:用了 select…for update 的方式,* 要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。

这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)

insert 非幂等操作,每次新增一条 重点 (数据库简单方案:可采取数据库唯一索引方式;这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)

三、解决方案

1. 方案对比

序号 前端/后端 方案 优点 缺点 代码实现

1) 前端 前端js提交后禁止按钮,返回结果后解禁等 简单 方便 只能控制页面,通过工具可绕过不安全

2) 后端 提交后重定向到其他页面,防止用户F5和浏览器前进后退等重复提交问题 简单 方便 体验不好,适用部分场景,若是遇到网络问题 还会出现

3) 后端 在表单、session、token 放入唯一标识符(如:UUID),每次操作时,保存标识一定时间后移除,保存期间有相同的标识就不处理或提示 相对简单 表单:有时需要前后端协商配合; session、token:加大服务性能开销

4) 后端 ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一标识(如:用户ID+请求路径+参数) 相对简单 适用于单机部署的应用 见下

5) 后端 redis 是线程安全的,可以实现redis分布式锁。设置唯一标识(如:用户ID+请求路径+参数)当做key ,value值可以随意(推荐设置成过期的时间点),在设置key的过期时间 单机、分布式、高并发都可以决绝 相对复杂需要部署维护redis 见下

2. 代码实现

4). google cache 代码实现 注解方式 Single lock

pom.xml 引入

<dependency>

   <groupId>com.google.guava</groupId>

    <artifactId>guava</artifactId>

    <version>28.2-jre</version>

</dependency>

1

2

3

4

5

配置文件 .yml

resubmit:

  local:

    timeOut: 30

1

2

3

实现代码

import java.lang.annotation.*;

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

public @interface LocalLock {

}

1

2

3

4

5

6

7

8

9

import com.alibaba.fastjson.JSONObject;

import com.example.mydemo.common.utils.IpUtils;

import com.example.mydemo.common.utils.Result;

import com.example.mydemo.common.utils.SecurityUtils;

import com.example.mydemo.common.utils.sign.MyMD5Util;

import com.google.common.cache.Cache;

import com.google.common.cache.CacheBuilder;

import lombok.Data;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.context.request.RequestAttributes;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.Method;

import java.security.NoSuchAlgorithmException;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.TimeUnit;

/**

 * @author: xx

 * @description: 单机放重复提交

 */

@Data

@Aspect

@Configuration

public class LocalLockMethodInterceptor {

    @Value("${spring.profiles.active}")

    private String springProfilesActive;

    @Value("${spring.application.name}")

    private String springApplicationName;

    private static int expireTimeSecond =5;

    @Value("${resubmit:local:timeOut}")

    public void setExpireTimeSecond(int expireTimeSecond) {

        LocalLockMethodInterceptor.expireTimeSecond = expireTimeSecond;

    }

    //定义缓存,设置最大缓存数及过期日期

    private static final Cache<String,Object> CACHE =

            CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(expireTimeSecond, TimeUnit.SECONDS).build();

    @Around("execution(public * *(..))  && @annotation(com.example.mydemo.common.interceptor.annotation.LocalLock)")

    public Object interceptor(ProceedingJoinPoint joinPoint){

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        Method method = signature.getMethod();

//        LocalLock localLock = method.getAnnotation(LocalLock.class);

        try{

        String key = getLockUniqueKey(signature,joinPoint.getArgs());

        if(CACHE.getIfPresent(key) != null){

            return Result.fail("不允许重复提交,请稍后再试");

        }

        CACHE.put(key,key);

            return joinPoint.proceed();

        }catch (Throwable throwable){

            throw new RuntimeException(throwable.getMessage());

        }finally {

        }

    }

    /**

     * 获取唯一标识key

     *

     * @param methodSignature

     * @param args

     * @return

     */

    private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {

        //请求uri, 获取类名称,方法名称

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        HttpServletRequest request = servletRequestAttributes.getRequest();

//        HttpServletResponse responese = servletRequestAttributes.getResponse();

        //获取用户信息

        String userMsg = SecurityUtils.getUsername(); //获取登录用户名称

        //1.判断用户是否登录

        if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip

            userMsg = IpUtils.getIpAddr(request);

        }

        String hash = "";

        List list = new ArrayList();

        if (args.length > 0) {

            String[] parameterNames = methodSignature.getParameterNames();

            for (int i = 0; i < parameterNames.length; i++) {

                Object obj = args[i];

                list.add(obj);

            }

            hash = JSONObject.toJSONString(list);

        }

        //项目名称 + 环境编码 + 获取类名称 + 方法名称 + 唯一key

        String key = "locallock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();

        if (StringUtils.isNotEmpty(key)) {

            key = key + ":" + hash;

        }

        key = MyMD5Util.getMD5(key);

        return key;

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

使用:

@LocalLock

    public void save(@RequestBody User user) {

    }

1

2

3

4

5)redis

pom.xml 引入

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

1

2

3

4

.yml文件 redis 配置

spring:

  redis:

    host: localhost

    port: :6379

    password: 123456

1

2

3

4

5

import java.lang.annotation.*;

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

public @interface RedisLock {

    int expire() default 5;

}

1

2

3

4

5

6

7

8

9

10

11

import com.alibaba.fastjson.JSONObject;

import com.google.common.collect.Lists;

import com.heshu.sz.blockchain.utonhsbs.common.utils.MyMD5Util;

import com.heshu.sz.blockchain.utonhsbs.common.utils.SecurityUtils;

import com.heshu.sz.blockchain.utonhsbs.common.utils.ip.IpUtils;

import com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock;

import com.heshu.sz.blockchain.utonhsbs.framework.system.domain.BaseResult;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.data.redis.core.script.RedisScript;

import org.springframework.web.context.request.RequestAttributes;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.Method;

import java.security.NoSuchAlgorithmException;

import java.util.ArrayList;

import java.util.List;

/**

 * @author :xx

 * @description:

 * @date : 2022/7/1 9:41

 */

@Slf4j

@Aspect

@Configuration

public class RedisLockMethodInterceptor {

    @Value("${spring.profiles.active}")

    private String springProfilesActive;

    @Value("${spring.application.name}")

    private String springApplicationName;

    @Autowired

    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock)")

    public void point() {

    }

    @Around("point()")

    public Object doaround(ProceedingJoinPoint joinPoint) {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        Method method = signature.getMethod();

        RedisLock localLock = method.getAnnotation(RedisLock.class);

        try {

            String lockUniqueKey = getLockUniqueKey(signature, joinPoint.getArgs());

            Integer expire = localLock.expire();

            if (expire < 0) {

                expire = 5;

            }

            ArrayList<String> keys = Lists.newArrayList(lockUniqueKey);

            String result = stringRedisTemplate.execute(setNxWithExpireTime, keys, expire.toString());

            if (!"ok".equalsIgnoreCase(result)) {//不存在

                return BaseResult.error("不允许重复提交,请稍后再试");

            }

            return joinPoint.proceed();

        } catch (Throwable throwable) {

            throw new RuntimeException(throwable.getMessage());

        }

    }

    /**

     * lua脚本

     */

    private RedisScript<String> setNxWithExpireTime = new DefaultRedisScript<>(

            "return redis.call('set', KEYS[1], 1, 'ex', ARGV[1], 'nx');",

            String.class

    );

    /**

     * 获取唯一标识key

     *

     * @param methodSignature

     * @param args

     * @return

     */

    private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {

        //请求uri, 获取类名称,方法名称

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        HttpServletRequest request = servletRequestAttributes.getRequest();

//        HttpServletResponse responese = servletRequestAttributes.getResponse();

        //获取用户信息

        String userMsg = SecurityUtils.getUsername(); //获取登录用户名称

        //1.判断用户是否登录

        if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip

            userMsg = IpUtils.getIpAddr(request);

        }

        String hash = "";

        List list = new ArrayList();

        if (args.length > 0) {

            String[] parameterNames = methodSignature.getParameterNames();

            for (int i = 0; i < parameterNames.length; i++) {

                Object obj = args[i];

                list.add(obj);

            }

            String param = JSONObject.toJSONString(list);

            hash = MyMD5Util.getMD5(param);

        }

        //项目名称 + 环境编码 + 获取类名称 + 加密参数

        String key = "lock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();

        if (StringUtils.isNotEmpty(key)) {

            key = key + ":" + hash;

        }

        return key;

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

使用

@RedisLock

    public void save(@RequestBody User user) {

    }

1

2

3

4

文章知识

————————————————

版权声明:本文为CSDN博主「暴力小熊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多