大家好,感谢邀请,今天来为大家分享一下揭秘8种高效处理重复提交的解决方案,面试难题轻松应对!的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!
1)Select查询天然是幂等的
2)删除也是幂等的。多次删除同一项目具有相同的效果。
3)update直接更新某个值,幂等
4)update更新累积操作不是幂等的。
5)插入是非幂等操作,每次添加一个新项
2. 原因
由于重复点击或网络重发eg:
1)点击提交按钮两次;
2)点击刷新按钮;
3)使用浏览器后退按钮重复之前的操作,导致表单重复提交;
4) 使用浏览器历史记录重复提交表单;
5)来自浏览器的重复HTTP请求;
6)nginx重发等情况;
7)分布式RPC尝试重传等;
三、解决方案
提交后执行页面重定向就是所谓的Post-Redirect-Get (PRG) 模式。
简而言之,当用户提交表单时,您执行客户端重定向并转到提交成功信息页面。
这样可以避免用户按F5导致的重复提交,浏览器表单也不会出现重复提交的警告。它还可以消除按浏览器的前进和后退按钮引起的相同问题。
在服务器端,生成唯一标识符,将其存储在会话中,并写入表单的隐藏字段中。然后表单页面被发送到浏览器。用户输入信息并点击提交。在服务器端,获取表单。将隐藏字段的值与会话中的唯一标识符进行比较。如果相等,说明是第一次提交,所以会处理这个请求,然后去掉session中的唯一标识。如果不相等,则说明是重复提交,因此不会再次处理。
相对复杂,不适合移动APP应用。这里我就不详细解释了。
插入使用唯一索引,更新使用乐观锁定版本方法
这种效率依赖于大数据量、高并发下的数据库硬件能力,可以针对非核心业务。
使用select . 进行更新,这与同步是相同的
加锁和先检查然后插入或更新是一样的,但是需要避免死锁,效率较低。
对于单个实体,请求并发量不大,可以推荐。
原理:使用ConcurrentHashMap并发容器的putIfAbsent方法和ScheduledThreadPoolExecutor定时任务。您还可以使用guava 缓存机制。还可以使用gauva 中缓存的有效时间生成密钥。 Content-MD5 Content-MD5是指Body的MD5值。只有当Body不是Form表单时才计算MD5。计算方式直接对参数和参数名称进行MD5加密。
MD5 被认为是唯一的,并且在某个类别范围内近似唯一。当然,低并发情况下就足够了。
当然,本地锁只适用于部署在单机上的应用程序。
配置注解
导入java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@有据可查
公共@接口重新提交{
/**
* 延迟时间:延迟多久后可以再次提交
*@returnTime 单位为一秒
*/
intdelaySeconds()默认20;
}
实例化锁
导入com.google.common.cache.Cache;
导入com.google.common.cache.CacheBuilder;
导入lombok.extern.slf4j.Slf4j;
导入org.apache.commons.codec.digest.DigestUtils;
导入java.util.Objects;
导入java.util.concurrent.ConcurrentHashMap;
导入java.util.concurrent.ScheduledThreadPoolExecutor;
导入java.util.concurrent.ThreadPoolExecutor;
导入java.util.concurrent.TimeUnit;
/**
*@authorlijing
* 重复提交锁定
*/
@Slf4j
公共最终类重新提交锁{
privatestaticfinalConcurrentHashMapLOCK_CACHE=newConcurrentHashMap(200);
privatestaticfinalScheduledThreadPoolExecutor EXECUTOR=newScheduledThreadPoolExecutor(5,newThreadPoolExecutor.DiscardPolicy());
//私有静态最终CacheCACHES=CacheBuilder.newBuilder()
//最大缓存100
//.maximumSize(1000)
//设置写缓存5秒后过期
//.expireAfterWrite(5, TimeUnit.SECONDS)
//。建造();
私有重新提交锁(){
}
/**
* 静态内部类单例模式
*@返回
*/
私有静态类SingletonInstance{
privatestaticfinalResubmitLock实例=newResubmitLock();
}
publicstaticResubmitLockgetInstance(){
返回SingletonInstance.INSTANCE;
}
publicstaticStringhandleKey(字符串参数){
returnDigestUtils.md5Hex(param==null?"": param);
}
/**
* 加锁putIfAbsent是原子操作,保证线程安全
*@paramkey对应的key
*@参数值
*@返回
*/
publicbooleanlock(finalString key, 对象值){
returnObjects.isNull(LOCK_CACHE.putIfAbsent(key, value));
}
/**
* 延迟释放锁用于控制短时间内重复提交
*@paramlock 是否需要解锁
*@paramkey对应的key
*@paramdelaySeconds 延迟时间
*/
publicvoidunLock(finalbooleanlock,finalString key,finalintdelaySeconds){
如果(锁定){
EXECUTOR.schedule(() -{
LOCK_CACHE.remove(key);
}、delaySeconds、TimeUnit.SECONDS);
}
}
}
AOP部分
导入com.alibaba.fastjson.JSONObject;
导入com.cn.xxx.common.annotation.重新提交;
importcom.cn.xxx.common.annotation.impl.ResubmitLock;
导入com.cn.xxx.common.dto.RequestDTO;
导入com.cn.xxx.common.dto.ResponseDTO;
导入com.cn.xxx.common.enums.ResponseCode;
导入lombok.extern.log4j.Log4j;
导入org.aspectj.lang.ProceedingJoinPoint;
导入org.aspectj.lang.annotation.Around;
导入org.aspectj.lang.annotation.Aspect;
导入org.aspectj.lang.reflect.MethodSignature;
导入org.springframework.stereotype.Component;
导入java.lang.reflect.Method;
/**
*@ClassNameRequestDataAspect
*@描述数据重复提交验证
*@作者lijing
*@日期2019/05/16 17:05
**/
@Log4j
@方面
@成分
公共类重新提交数据方面{
privatefinalstaticString DATA="数据";
privatefinalstaticObject PRESENT=newObject();
@Around("@annotation(com.cn.xxx.common.annotation.重新提交)")
publicObjecthandleResubmit(ProceedingJoinPoint joinPoint)throwsThrowable{
方法方法=((MethodSignature) joinPoint.getSignature()).getMethod();
//获取注解信息
重新提交注解=method.getAnnotation(Resubmit.class);
intdelaySeconds=注解.delaySeconds();
Object[] pointArgs=joinPoint.getArgs();
字符串键="";
//获取第一个参数
对象firstParam=pointArgs[0];
if(firstParaminstanceofRequestDTO) {
//解析参数
JSONObject requestDTO=JSONObject.parseObject(firstParam.toString());
JSONObject 数据=JSONObject.parseObject(requestDTO.getString(DATA));
如果(数据!=空){
StringBuffer sb=newStringBuffer();
data.forEach((k, v) -{
sb.append(v);
});
//使用content_MD5加密方式生成加密参数
key=ResubmitLock.handleKey(sb.toString());
}
}
//执行锁
布尔锁=假;
尝试{
//设置解锁密钥
锁=ResubmitLock.getInstance().lock(key, PRESENT);
如果(锁定){
//发布
returnjoinPoint.proceed();
}别的{
//响应重复提交异常
returnnewResponseDTO(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
}
}最后{
//设置解锁密钥和解锁时间
ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
}
}
}
注释用例
@ApiOperation(value="保存我的发帖界面",notes="保存我的发帖界面")
@PostMapping("/posts/save")
@重新提交(延迟秒=10)
公共ResponseDTOsaveBbsPosts(@RequestBody@ValidatedRequestDTOrequestDto) {
returnbbsPostsBizService.saveBbsPosts(requestDto);
}
上面是使用本地锁方式的幂等提交。 Content-MD5 用于加密。只要参数不变,参数加密值不改变,且密钥存在,提交就会被阻塞。
当然,你也可以使用一些其他的签名验证,在某次提交时生成固定的签名并提交给后端。统一签名会被后端解析为每次提交的验证token,并在缓存中处理。
只需在pom.xml中添加starter-web、starter-aop、starter-data-redis的依赖即可
org.springframework.bootgroupId
spring-boot-starter-webartifactId
依赖性
org.springframework.bootgroupId
spring-boot-starter-aopartifactId
依赖性
org.springframework.bootgroupId
spring-boot-starter-data-redisartifactId
依赖性
依赖关系
属性配置在application.properites资源文件中添加redis相关配置项:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
主要实现方法:熟悉Redis的朋友都知道,它是线程安全的。利用分布式锁的特性,我们可以轻松实现分布式锁,例如opsForValue().setIfAbsent(key, value)。它的作用是如果缓存中没有这样的锁。当前Key会被缓存并返回true,反之亦然;
缓存后,给key设置一个过期时间,防止因为系统崩溃而释放锁,造成死锁;那么我们是不是可以认为,当返回true时,我们就认为它已经获得了锁。当锁没有释放时,我们抛出异常.
packagecom.battcn.拦截器;
导入com.battcn.annotation.CacheLock;
导入com.battcn.utils.RedisLockHelper;
导入org.aspectj.lang.ProceedingJoinPoint;
导入org.aspectj.lang.annotation.Around;
导入org.aspectj.lang.annotation.Aspect;
导入org.aspectj.lang.reflect.MethodSignature;
导入org.springframework.beans.factory.annotation.Autowired;
导入org.springframework.context.annotation.Configuration;
导入org.springframework.util.StringUtils;
导入java.lang.reflect.Method;
导入java.util.UUID;
/**
* 雷迪解决方案
*@authorLevin
*@since2018/6/12 0012
*/
@方面
@配置
公共类LockMethodInterceptor{
@Autowired
publicLockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator){
this.redisLockHelper=redisLockHelper;
this.cacheKeyGenerator=cacheKeyGenerator;
}
privatefinalRedisLockHelper redisLockHelper;
privatefinalCacheKeyGenerator 缓存密钥生成器;
@Around("执行(public * *(.)) @annotation(com.battcn.annotation.CacheLock)")
publicObjectinterceptor(ProceedingJoinPoint pjp){
MethodSignature 签名=(MethodSignature) pjp.getSignature();
方法方法=signature.getMethod();
CacheLock 锁=method.getAnnotation(CacheLock.class);
if(StringUtils.isEmpty(lock.prefix())) {
thrownewRuntimeException("锁定键不为空.");
}
FinalString lockKey=cacheKeyGenerator.getLockKey(pjp);
字符串值=UUID.randomUUID().toString();
尝试{
//假设锁定成功,但是设置的过期时间失效,后续所有值都将为false。
Finalbooleansuccess=redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
如果(!成功){
thrownewRuntimeException("重复提交");
}
尝试{
returnpjp.proceed();
}catch(可抛出可抛出){
thrownewRuntimeException("系统异常");
}
}最后{
//TODO 如果是演示,需要注释掉这段代码;实际上,你应该保留它
redisLockHelper.unlock(lockKey, value);
}
}
}
RedisLockHelper的调用方式是封装成API,更加灵活。
packagecom.battcn.utils;
导入org.springframework.boot.autoconfigure.AutoConfigureAfter;
导入org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
导入org.springframework.context.annotation.Configuration;
导入org.springframework.data.redis.connection.RedisStringCommands;
导入org.springframework.data.redis.core.RedisCallback;
导入org.springframework.data.redis.core.StringRedisTemplate;
导入org.springframework.data.redis.core.types.Expiration;
导入org.springframework.util.StringUtils;
导入java.util.concurrent.Executors;
导入java.util.concurrent.ScheduledExecutorService;
导入java.util.concurrent.TimeUnit;
导入java.util.regex.Pattern;
/**
* 需要定义为Bean
*@authorLevin
*@since2018/6/15 0015
*/
@配置
@AutoConfigureAfter(RedisAutoConfiguration.class)
公共类RedisLockHelper{
privatestaticfinalString DELIMITER="|";
/**
* 如果要求比较高,可以通过注入的方式分配。
*/
privatestaticfinalScheduledExecutorService EXECUTOR_SERVICE=Executors.newScheduledThreadPool(10);
privatefinalStringRedisTemplate stringRedisTemplate;
publicRedisLockHelper(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}
/**
* 获取锁(有死锁的风险)
*@paramlockKey 锁键
*@paramvalue值
*@paramtime 超时时间
*@paramunit 过期单位
*@returntrue 或false
*/
publicbooleantryLock(finalString lockKey,finalString value,finallongtime,finalTimeUnit 单位){
returnstringRedisTemplate.execute((RedisCallback) 连接-connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(时间, 单位), RedisStringCommands.SetOption.SET_IF_ABSENT));
}
/**
* 获取锁
*@paramlockKey 锁键
*@paramuuid UUID
*@paramtimeout 超时时间
*@paramunit 过期单位
*@returntrue 或false
*/
publicbooleanlock(String lockKey,finalString uuid,longtimeout,finalTimeUnit 单位){
Finallongmilliseconds=Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
booleansuccess=stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + 毫秒) + DELIMITER + uuid);
如果(成功){
stringRedisTemplate.expire(lockKey, 超时, TimeUnit.SECONDS);
}别的{
String oldVal=stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + 毫秒) + DELIMITER + uuid);
FinalString[] oldValues=oldVal.split(Pattern.quote(DELIMITER));
if(Long.parseLong(oldValues[0]) +1=System.currentTimeMillis()) {
返回真;
}
}
返回成功;
}
/**
*@seeRedis 文档: SET
*/
publicvoidunlock(字符串lockKey, 字符串值){
解锁(lockKey,值,0,TimeUnit.MILLISECONDS);
}
/**
* 延迟解锁
*@paramlockKey 密钥
*@paramuuid 客户端(最好是唯一密钥)
*@paramdelayTime 延迟时间
*@paramunit 时间单位
*/
publicvoidunlock(finalString lockKey,finalString uuid,longdelayTime, TimeUnit 单位){
if(StringUtils.isEmpty(lockKey)) {
返回;
}
如果(延迟时间=0){
doUnlock(lockKey, uuid);
}别的{
EXECUTOR_SERVICE.schedule(() -doUnlock(lockKey, uuid), 延迟时间, 单位);
}
}
/**
*@paramlockKey 密钥
*@paramuuid 客户端(最好是唯一密钥)
*/
privatevoiddoUnlock(finalString lockKey,finalString uuid){
String val=stringRedisTemplate.opsForValue().get(lockKey);
FinalString[] 值=val.split(Pattern.quote(DELIMITER));
if(values.length=0) {
返回;
}
if(uuid.equals(values[1])) {
stringRedisTemplate.delete(lockKey);
}
}
}
redis提交请参考博客:
https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/
结尾
本文发布于微星公众号“程序员的成长之路”,回复“1024”你懂的,点个赞吧。
回复[256]Java程序员成长计划
【揭秘8种高效处理重复提交的解决方案,面试难题轻松应对!】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
厉害哇,一口气8个方案!感觉这个面试官可真不容易。
有20位网友表示赞同!
想学习一下幂等性的知识,这个标题看起来很吸引人啊
有8位网友表示赞同!
终于找到解决重复提交问题的文章了,一直在烦恼这个问题呢!
有5位网友表示赞同!
附代码就更好了,直接看效果体验感强!
有20位网友表示赞同!
面试官懵的状态太好笑了吧哈哈,这种高水平的分享确实很强大。
有20位网友表示赞同!
学习一下幂等性,说不定下次面试能拿来用呢!
有18位网友表示赞同!
代码实践总是最管用的,希望能有详细的解释。
有17位网友表示赞同!
重复提交问题真头疼,看看这个方案能不能帮上忙
有6位网友表示赞同!
最近一直在学面试题备考,这篇文章正好可以了解一下实际应用。
有15位网友表示赞同!
感觉这篇博客内容很专业,我得认真学习学习!
有16位网友表示赞同!
看了标题就觉得很有料,期待详细的分享和代码讲解!
有20位网友表示赞同!
幂等性这个概念以前没怎么了解过,看完这篇文章或许就能明白了。
有13位网友表示赞同!
面试官懵了说明作者的知识点真的强啊!
有17位网友表示赞同!
重复提交问题在实际开发中确实很常见,希望能找到更加高效的解决方案。
有9位网友表示赞同!
学习一下这个方法,说不定能提高我的程序开发效率呢!
有5位网友表示赞同!
这种分享方式太棒了,既有理论讲解又有代码示例!
有11位网友表示赞同!
我收藏这篇文章啦,以后有机会再仔细阅读一番。
有5位网友表示赞同!
最近也在研究重复提交问题的解决方案,看下作者的思路是不是也可以参考。
有17位网友表示赞同!
希望文章能详细解释每个方案的原理和优缺点!
有16位网友表示赞同!