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

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

作者: 良月柒 | 来源:发表于2020-05-26 20:53 被阅读0次

    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 学习资料

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/obcbahtx.html