暴力小熊 已于 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版权协议,转载请附上原文出处链接及本声明。
|
|