8种方案解决重复提交问题!
【摘要】 2)使用Post/Redirect/Get模式
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
1.什么是幂等

在我们编程中常见幂等
§
§
§
§
§
2.产生原因
§
§
§
§
§
§
§
3.解决方案
①配置注解
importjava.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceResubmit {/***延时时间在延时多久后可以再次提交** @return Time unit is one second*/int delaySeconds()default20;}
②实例化锁
importcom.google.common.cache.Cache;importcom.google.common.cache.CacheBuilder;importlombok.extern.slf4j.Slf4j;importorg.apache.commons.codec.digest.DigestUtils;importjava.util.Objects;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ScheduledThreadPoolExecutor;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;/*** @author lijing*重复提交锁*/@Slf4jpublicfinalclassResubmitLock{privatestaticfinalConcurrentHashMap<String, Object> LOCK_CACHE =newConcurrentHashMap<>(200);privatestaticfinalScheduledThreadPoolExecutor EXECUTOR =newScheduledThreadPoolExecutor(5,newThreadPoolExecutor.DiscardPolicy());// private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()//最大缓存100个// .maximumSize(1000)//设置写缓存后5秒钟过期// .expireAfterWrite(5, TimeUnit.SECONDS)// .build();privateResubmitLock(){}/***静态内部类单例模式** @return*/privatestaticclassSingletonInstance{privatestaticfinalResubmitLock INSTANCE =newResubmitLock();}publicstaticResubmitLockgetInstance(){returnSingletonInstance.INSTANCE;}publicstaticStringhandleKey(String param){returnDigestUtils.md5Hex(param ==null?"": param);}/***加锁putIfAbsent是原子操作保证线程安全** @param key对应的key* @param value* @return*/publicbooleanlock(finalString key, Object value) {returnObjects.isNull(LOCK_CACHE.putIfAbsent(key, value));}/***延时释放锁用以控制短时间内的重复提交** @param lock是否需要解锁* @param key对应的key* @param delaySeconds延时时间*/publicvoidunLock(finalbooleanlock,finalString key,finalintdelaySeconds) {if(lock) {EXECUTOR.schedule(() -> {LOCK_CACHE.remove(key);}, delaySeconds, TimeUnit.SECONDS);}}}
③AOP 切面
importcom.alibaba.fastjson.JSONObject;importcom.cn.xxx.common.annotation.Resubmit;importcom.cn.xxx.common.annotation.impl.ResubmitLock;importcom.cn.xxx.common.dto.RequestDTO;importcom.cn.xxx.common.dto.ResponseDTO;importcom.cn.xxx.common.enums.ResponseCode;importlombok.extern.log4j.Log4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.stereotype.Component;importjava.lang.reflect.Method;/*** @ClassName RequestDataAspect* @Description数据重复提交校验* @Author lijing* @Date 2019/05/16 17:05**/@Log4j@Aspect@ComponentpublicclassResubmitDataAspect{privatefinalstatic String DATA ="data";privatefinalstatic Object PRESENT = new Object();@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")publicObject handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();//获取注解信息Resubmitannotation= method.getAnnotation(Resubmit.class);int delaySeconds =annotation.delaySeconds();Object[] pointArgs = joinPoint.getArgs();String key ="";//获取第一个参数Object firstParam = pointArgs[0];if(firstParam instanceof RequestDTO) {//解析参数JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());JSONObjectdata= JSONObject.parseObject(requestDTO.getString(DATA));if(data!=null) {StringBuffer sb = new StringBuffer();data.forEach((k, v) -> {sb.append(v);});//生成加密参数使用了content_MD5的加密方式key = ResubmitLock.handleKey(sb.toString());}}//执行锁boolean lock =false;try{//设置解锁keylock = ResubmitLock.getInstance().lock(key, PRESENT);if(lock) {//放行returnjoinPoint.proceed();}else{//响应重复提交异常returnnew ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);}}finally{//设置解锁key和解锁时间ResubmitLock.getInstance().unLock(lock, key, delaySeconds);}}}
④注解使用案例
@ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")@PostMapping("/posts/save")@Resubmit(delaySeconds = 10)publicResponseDTO<BaseResponseDataDTO> saveBbsPosts(@RequestBody@ValidatedRequestDTO<BbsPostsRequestDTO> requestDto) {returnbbsPostsBizService.saveBbsPosts(requestDto);}
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>
属性配置 在 application.properites 资源文件中添加 redis 相关的配置项
spring.redis.host=localhostspring.redis.port=6379spring.redis.password=123456
packagecom.battcn.interceptor;importcom.battcn.annotation.CacheLock;importcom.battcn.utils.RedisLockHelper;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.lang.reflect.Method;importjava.util.UUID;/*** redis方案** @author Levin* @since 2018/6/12 0012*/@Aspect@ConfigurationpublicclassLockMethodInterceptor{@AutowiredpublicLockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {this.redisLockHelper = redisLockHelper;this.cacheKeyGenerator = cacheKeyGenerator;}privatefinalRedisLockHelper redisLockHelper;privatefinalCacheKeyGenerator cacheKeyGenerator;@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")publicObject interceptor(ProceedingJoinPoint pjp) {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();CacheLock lock = method.getAnnotation(CacheLock.class);if(StringUtils.isEmpty(lock.prefix())) {thrownew RuntimeException("lock key don't null...");}finalString lockKey = cacheKeyGenerator.getLockKey(pjp);String value = UUID.randomUUID().toString();try{//假设上锁成功,但是设置过期时间失效,以后拿到的都是falsefinalboolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());if(!success) {thrownew RuntimeException("重复提交");}try{returnpjp.proceed();}catch(Throwable throwable) {thrownew RuntimeException("系统异常");}}finally{// TODO如果演示的话需要注释该代码;实际应该放开redisLockHelper.unlock(lockKey, value);}}}
RedisLockHelper 通过封装成 API 方式调用,灵活度更加高
packagecom.battcn.utils;importorg.springframework.boot.autoconfigure.AutoConfigureAfter;importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisStringCommands;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.types.Expiration;importorg.springframework.util.StringUtils;importjava.util.concurrent.Executors;importjava.util.concurrent.ScheduledExecutorService;importjava.util.concurrent.TimeUnit;importjava.util.regex.Pattern;/***需要定义成Bean** @author Levin* @since 2018/6/15 0015*/@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)publicclassRedisLockHelper{privatestaticfinalString DELIMITER ="|";/***如果要求比较高可以通过注入的方式分配*/privatestaticfinalScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);privatefinalStringRedisTemplate stringRedisTemplate;publicRedisLockHelper(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/***获取锁(存在死锁风险)** @param lockKey lockKey* @param value value* @param time超时时间* @param unit过期单位* @return true or false*/publicboolean tryLock(finalString lockKey,finalString value,finallong time,finalTimeUnit unit) {returnstringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));}/***获取锁** @param lockKey lockKey* @param uuid UUID* @param timeout超时时间* @param unit过期单位* @return true or false*/publicboolean lock(String lockKey,finalString uuid, long timeout,finalTimeUnit unit) {finallong milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);if(success) {stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);}else{String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);finalString[] oldValues = oldVal.split(Pattern.quote(DELIMITER));if(Long.parseLong(oldValues[0]) +1<= System.currentTimeMillis()) {returntrue;}}returnsuccess;}/*** @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>*/publicvoid unlock(String lockKey, String value) {unlock(lockKey, value,0, TimeUnit.MILLISECONDS);}/***延迟unlock** @param lockKey key* @param uuid client(最好是唯一键的)* @param delayTime延迟时间* @param unit时间单位*/publicvoid unlock(finalString lockKey,finalString uuid, long delayTime, TimeUnit unit) {if(StringUtils.isEmpty(lockKey)) {return;}if(delayTime <=0) {doUnlock(lockKey, uuid);}else{EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);}}/*** @param lockKey key* @param uuid client(最好是唯一键的)*/privatevoid doUnlock(finalString lockKey,finalString uuid) {Stringval= stringRedisTemplate.opsForValue().get(lockKey);finalString[] values =val.split(Pattern.quote(DELIMITER));if(values.length <=0) {return;}if(uuid.equals(values[1])) {stringRedisTemplate.delete(lockKey);}}}
redis的提交参照
原创作者:方志朋
方志朋简介:SpringCloud 中国社区联合创始人,博客访问量突破一千万,爱好开源,热爱分享,活跃于各大社区,保持着非常强的学习驱动力,终身学习践行者,终身学习受益者。目前就职于国内某家知名互联网保险公司,担任DEVOPS 工程师,对微服务领域和持续集成领域研究较深,精通微服务框架SpringCloud。
【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)