一口气说出8种幂等性解决重复提交的方案,面试官懵了!(附代码)

1.什么是幂等

在我们编程中常见幂等 

1)select查询天然幂等   

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

3)update直接更新某个值的,幂等 

4)update更新累加操作的,非幂等 

5)insert非幂等操作,每次新增一条

2.产生原因

由于重复点击或者网络重发  eg:   

1)点击提交按钮两次; 

2)点击刷新按钮; 

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

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

5)浏览器重复的HTTP请; 

6)nginx重发等情况; 

7)分布式RPC的try重发等;

3.解决方案

在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。

简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。

这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。

在服务器端,生成一个唯一的标识符,将它存入session,同时将它写入表单的隐藏字段中,然后将表单页面发给浏览器,用户录入信息后点击提交,在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除;不相等说明是重复提交,就不再处理。

比较复杂  不适合移动端APP的应用 这里不详解

insert使用唯一索引 update使用 乐观锁 version版本法

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

使用select ... for update  ,这种和 synchronized 

锁住先查再insert or update一样,但要避免死锁,效率也较差 

针对单体 请求并发不大 可以推荐使用

原理:使用了 ConcurrentHashMap 并发容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定时任务,也可以使用guava cache的机制, gauva中有配有缓存的有效时间 也是可以的key的生成 Content-MD5 Content-MD5 是指 Body 的 MD5 值,只有当 Body 非Form表单时才计算MD5,计算方式直接将参数和参数名称统一加密MD5。

MD5在一定范围类认为是唯一的,近似唯一,当然在低并发的情况下足够了 。

当然本地锁只适用于单机部署的应用。

①配置注解

importjava.lang.annotation.*;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public@interfaceResubmit {

/**

* 延时时间 在延时多久后可以再次提交

*

*@returnTime unit is one second

*/

intdelaySeconds()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;

/**

*@authorlijing

* 重复提交锁

*/

@Slf4j

publicfinalclassResubmitLock{

privatestaticfinalConcurrentHashMapLOCK_CACHE =newConcurrentHashMap<>(200);

privatestaticfinalScheduledThreadPoolExecutor EXECUTOR =newScheduledThreadPoolExecutor(5,newThreadPoolExecutor.DiscardPolicy());

// private static final CacheCACHES = 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 是原子操作保证线程安全

*

*@paramkey 对应的key

*@paramvalue

*@return

*/

publicbooleanlock(finalString key, Object value){

returnObjects.isNull(LOCK_CACHE.putIfAbsent(key, value));

}

/**

* 延时释放锁 用以控制短时间内的重复提交

*

*@paramlock 是否需要解锁

*@paramkey 对应的key

*@paramdelaySeconds 延时时间

*/

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;

/**

*@ClassNameRequestDataAspect

*@Description数据重复提交校验

*@Authorlijing

*@Date2019/05/16 17:05

**/

@Log4j

@Aspect

@Component

publicclassResubmitDataAspect{

privatefinalstaticString DATA ="data";

privatefinalstaticObject PRESENT =newObject();

@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")

publicObjecthandleResubmit(ProceedingJoinPoint joinPoint)throwsThrowable{

Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

//获取注解信息

Resubmit annotation = method.getAnnotation(Resubmit.class);

intdelaySeconds = annotation.delaySeconds();

Object[] pointArgs = joinPoint.getArgs();

String key ="";

//获取第一个参数

Object firstParam = pointArgs[0];

if(firstParaminstanceofRequestDTO) {

//解析参数

JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());

JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));

if(data !=null) {

StringBuffer sb =newStringBuffer();

data.forEach((k, v) -> {

sb.append(v);

});

//生成加密参数 使用了content_MD5的加密方式

key = ResubmitLock.handleKey(sb.toString());

}

}

//执行锁

booleanlock =false;

try{

//设置解锁key

lock = ResubmitLock.getInstance().lock(key, PRESENT);

if(lock) {

//放行

returnjoinPoint.proceed();

}else{

//响应重复提交异常

returnnewResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);

}

}finally{

//设置解锁key和解锁时间

ResubmitLock.getInstance().unLock(lock, key, delaySeconds);

}

}

}

④注解使用案例

@ApiOperation(value ="保存我的帖子接口", notes ="保存我的帖子接口")

@PostMapping("/posts/save")

@Resubmit(delaySeconds =10)

public ResponseDTOsaveBbsPosts(@RequestBody@ValidatedRequestDTOrequestDto) {

returnbbsPostsBizService.saveBbsPosts(requestDto);

}

以上就是本地锁的方式进行的幂等提交  使用了Content-MD5 进行加密   只要参数不变,参数加密 密值不变,key存在就阻止提交。

当然也可以使用  一些其他签名校验  在某一次提交时先 生成固定签名  提交到后端 根据后端解析统一的签名作为 每次提交的验证token 去缓存中处理即可。

在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依赖即可

org.springframework.bootgroupId>

spring-boot-starter-webartifactId>

dependency>

org.springframework.bootgroupId>

spring-boot-starter-aopartifactId>

dependency>

org.springframework.bootgroupId>

spring-boot-starter-data-redisartifactId>

dependency>

dependencies>

属性配置 在 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.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 方案

*

*@authorLevin

*@since2018/6/12 0012

*/

@Aspect

@Configuration

publicclassLockMethodInterceptor{

@Autowired

publicLockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator){

this.redisLockHelper = redisLockHelper;

this.cacheKeyGenerator = cacheKeyGenerator;

}

privatefinalRedisLockHelper redisLockHelper;

privatefinalCacheKeyGenerator cacheKeyGenerator;

@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")

publicObjectinterceptor(ProceedingJoinPoint pjp){

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

Method method = signature.getMethod();

CacheLock lock = method.getAnnotation(CacheLock.class);

if(StringUtils.isEmpty(lock.prefix())) {

thrownewRuntimeException("lock key don't null...");

}

finalString lockKey = cacheKeyGenerator.getLockKey(pjp);

String value = UUID.randomUUID().toString();

try{

// 假设上锁成功,但是设置过期时间失效,以后拿到的都是 false

finalbooleansuccess = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());

if(!success) {

thrownewRuntimeException("重复提交");

}

try{

returnpjp.proceed();

}catch(Throwable throwable) {

thrownewRuntimeException("系统异常");

}

}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

*

*@authorLevin

*@since2018/6/15 0015

*/

@Configuration

@AutoConfigureAfter(RedisAutoConfiguration.class)

publicclassRedisLockHelper{

privatestaticfinalString DELIMITER ="|";

/**

* 如果要求比较高可以通过注入的方式分配

*/

privatestaticfinalScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

privatefinalStringRedisTemplate stringRedisTemplate;

publicRedisLockHelper(StringRedisTemplate stringRedisTemplate){

this.stringRedisTemplate = stringRedisTemplate;

}

/**

* 获取锁(存在死锁风险)

*

*@paramlockKey lockKey

*@paramvalue value

*@paramtime 超时时间

*@paramunit 过期单位

*@returntrue or false

*/

publicbooleantryLock(finalString lockKey,finalString value,finallongtime,finalTimeUnit unit){

returnstringRedisTemplate.execute((RedisCallback) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));

}

/**

* 获取锁

*

*@paramlockKey lockKey

*@paramuuid UUID

*@paramtimeout 超时时间

*@paramunit 过期单位

*@returntrue or false

*/

publicbooleanlock(String lockKey,finalString uuid,longtimeout,finalTimeUnit unit){

finallongmilliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();

booleansuccess = 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;

}

/**

*@seeRedis Documentation: SET

*/

publicvoidunlock(String lockKey, String value){

unlock(lockKey, value,0, TimeUnit.MILLISECONDS);

}

/**

* 延迟unlock

*

*@paramlockKey key

*@paramuuid client(最好是唯一键的)

*@paramdelayTime 延迟时间

*@paramunit 时间单位

*/

publicvoidunlock(finalString lockKey,finalString uuid,longdelayTime, TimeUnit unit){

if(StringUtils.isEmpty(lockKey)) {

return;

}

if(delayTime <=0) {

doUnlock(lockKey, uuid);

}else{

EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);

}

}

/**

*@paramlockKey key

*@paramuuid client(最好是唯一键的)

*/

privatevoiddoUnlock(finalString lockKey,finalString uuid){

String val = 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的提交参照博客:

https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/


END

本文发于 微星公众号「程序员的成长之路」,回复「1024」你懂得,给个赞呗。

回复 [ 256 ] Java 程序员成长规划

回复 [ 777 ] 接私活的七大平台利器

回复 [ 2048 ] 免费领取C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等 5T 学习资料