如何避免并发情况下的重复提交

发布于 2019-10-11

如何避免并发情况下的重复提交

在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。

在传统的restful风格的项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保证提交请求的唯一性。

上述的思路其实没有问题的,但是需要前后端都稍加改动,如果在业务开发完在加这个的话,改动量未免有些大了,本节的实现方案无需前端配合,纯后端处理。

利用分布式锁

相同的请求在同一时间只能被处理一次,利用分布式锁可以非常方便地解决这个问题。

思路:

[mark_a]

1、自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
2、通过拦截器对所有标记了 @NoRepeatSubmit 的方法拦截。
3、在业务方法执行前,获取当前用户的 token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。
4、业务方法执行后,释放锁。

[/mark_a]

拦截器类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class ViolationInterceptor extends HandlerInterceptorAdapter {    
    @Autowired 
    private RedisLock redisLock;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) throws Exception {
    }
     
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object hadnler, ModelAndView ex) throws Exception {
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String key = request.getHeader("requestId");
            
        boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds); 
        if (isSuccess) { 
            log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); 
            // 获取锁成功, 执行进程 
            Object result; 
            try { 
                requestIds.add(requestId);
                return super.preHandle(request, response, handler);
            } finally { 
                // 释放锁 
                redisLock.releaseLock(key, clientId); 
                log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); 
            }
        }else {
            throw new IllegalArgumentException("Violation Request; Reason requestId already registered");
        }
    }
}

利用数据库的唯一索引

如果设置了唯一约束,那么同一条数据再次插入数据库时,数据库会报唯一索引的错误,这个时候后台对这个异常进行处理并返回给前端提示。

思路:

[mark_a]接口A接收到请求之后,对请求信息hash运算,得到hash值hashCodeA;
保存hashCodeA 到数据库,并且对应的数据库的列(column)满足unique约束;
保存成功之后,才进行正常业务逻辑处理,比如提交订单;
服务器B接收到相同的请求后,也得到相同的hash值,hashCodeA,
服务器B保存hashCodeA 到数据库,肯定失败,因为相同的hash值已经存在;
因为保存失败,所以后面的业务逻辑不会执行。[/mark_a]

缓存计数器
由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。

以支付为例子:

[mark_a]

每次request进来则新建一个以orderId为key的计数器,然后+1。

如果>1(不能获得锁): 说明有操作在进行,删除。
如果=1(获得锁): 可以操作。
操作结束(删除锁):删除这个计数器。

[/mark_a]

喜欢 2
奋楫笃行,臻于至善!

相关文章

使用 Mycat 中间件搭建 MySQL 高可用实现分库分表及读写分离

Mycat 是一款基于阿里开源产品Cobar而研发的开源数据库分库分表中间件(基于Java语言开发),可以用来方便地搭建面向企业应用开发的大数据库集群,支持事务、ACID等特性,其核心是基于代理方案实...
阅读全文

通用架构模式和通用架构服务

架构模式是在给定上下文的软件架构中,针对常发生问题的一种通用、复用的解决方案。架构模式类似于软件设计模式,但是范畴更广。一个好的软件产品往往需要有良好的架构思想和架构服务来支撑整个软件的生命周期,本文...
阅读全文

Java 的可重入锁和不可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中Reentra...
阅读全文

Redis 的两种持久化方式及使用场景分析

Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘。当下次Redis重启时,利用持久化文件实现数据恢...
阅读全文

redis 高可用主从,哨兵,集群解决方案

Redis因为其高性能和易用性在我们后端的服务中发挥了巨大的作用,并且很多重要功能的实现都会依赖redis。除了常用的缓存,还有队列,发布订阅等重要用处。所以redis的服务高可用就显得尤为关键。这里...
阅读全文

Redis 缓存穿透、缓存击穿、缓存雪崩的区别及解决方案

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很...
阅读全文

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注