V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
luxinfl
V2EX  ›  Java

Java 后台防重复提交一般怎么做的?

  •  
  •   luxinfl · 2020-06-11 11:33:55 +08:00 · 12320 次点击
    这是一个创建于 1633 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们现阶段就是加了一个张表,订单号唯一主键。请求过来的时候会校验数据库有没有这条单号数据,这个方法是非事务的。还有没有什么其他好的方法了。

    我在想要不要放在 redis 里面。。求大佬指教啊

    74 条回复    2020-06-14 21:43:42 +08:00
    wysnylc
        1
    wysnylc  
       2020-06-11 12:13:59 +08:00
    这就是幂等
    gz911122
        2
    gz911122  
       2020-06-11 12:16:40 +08:00
    订单号唯一主键不就天然防重复了...
    shenlanAZ
        3
    shenlanAZ  
       2020-06-11 12:25:49 +08:00
    前端:
    锁住按钮 等请求返回之后再对按钮解锁。

    后端:
    差不多也是用锁的思路 无非就是锁的粒度,可以锁业务,也可以锁订单。看业务需求。
    beryl
        4
    beryl  
       2020-06-11 12:26:39 +08:00
    关键词:防重入
    luckyrayyy
        5
    luckyrayyy  
       2020-06-11 12:27:32 +08:00
    主键的话你插入不进去啊。
    tabris17
        6
    tabris17  
       2020-06-11 12:27:50 +08:00
    每次提交都包含一个业务 ID,重复 ID 的请求丢弃
    oneisall8955
        7
    oneisall8955  
       2020-06-11 13:14:48 +08:00 via Android
    摘抄网上的幂等解决办法
    1.实现幂等性常见的方式有:悲观锁( for update )、乐观锁、唯一约束
    2.几种方式,按照最优排序:乐观锁 > 唯一约束 > 悲观锁
    hantsy
        8
    hantsy  
       2020-06-11 13:20:52 +08:00
    cxrf 。。。设置 Http Header,或者 Http Form 。


    传统框架大部分自带 Form 重复提交了。
    lhx2008
        9
    lhx2008  
       2020-06-11 13:22:52 +08:00 via Android
    最简单的方法可以依赖数据库的唯一键,复杂一点就是请求带 token+redis 黑名单
    hantsy
        10
    hantsy  
       2020-06-11 13:24:17 +08:00   ❤️ 1
    修正:cxrf --> csrf 或者叫 xsrf
    jinzhongyuan
        11
    jinzhongyuan  
       2020-06-11 13:24:43 +08:00
    @gz911122 楼主的意思应该是,指定时间内方法参数一样的请求无法进入方法或者 controller
    luxinfl
        12
    luxinfl  
    OP
       2020-06-11 13:42:07 +08:00
    @jinzhongyuan 不是不是,我就是说两笔相同参数同时提交的问题。。有时候会碰到前台没做防重点按钮点了好多次。
    luxinfl
        13
    luxinfl  
    OP
       2020-06-11 13:46:00 +08:00
    @lhx2008 放在 redis 是不是快一点。因为他这个重复提交也就那么一小会,人手动点的。。感觉用不到数据库
    gz911122
        14
    gz911122  
       2020-06-11 13:50:04 +08:00
    @luxinfl
    那你都加了唯一索引了 点 100 次又何妨
    doudouwu
        15
    doudouwu  
       2020-06-11 13:59:30 +08:00
    #14 @ gz911122 唯一索引的是订单号,提交的是不包含的订单号,待生成订单号的数据呢
    楼主说的场景很可能存在
    kanepan19
        16
    kanepan19  
       2020-06-11 14:03:54 +08:00
    csrf +1
    gz911122
        17
    gz911122  
       2020-06-11 14:07:25 +08:00
    @doudouwu
    订单号肯定是预先生成好的...
    会有生成器之类的负责,

    而且他说了是相同参数.
    dayformyjob
        18
    dayformyjob  
       2020-06-11 14:08:07 +08:00
    threadlocal + 信号量 重入锁,每个线程只能提交一次----单机版情况。
    分布式 多机器,就引入中间件--redis 或者 jms 之类的
    royan
        19
    royan  
       2020-06-11 14:12:15 +08:00
    csrf+1
    kiracyan
        20
    kiracyan  
       2020-06-11 14:12:17 +08:00
    预先生成 token 然后设置过期时间 提交成功后删除 不存在的不让提交
    DJQTDJ
        21
    DJQTDJ  
       2020-06-11 14:13:56 +08:00
    订单号不是自增的吗
    wizzer
        22
    wizzer  
       2020-06-11 14:17:24 +08:00
    年月日时分秒+redis incr N 位自增长
    pinktu
        23
    pinktu  
       2020-06-11 14:19:29 +08:00
    让前端写,哪边简单哪边做
    wizzer
        24
    wizzer  
       2020-06-11 14:23:52 +08:00
    ```
    public synchronized String getNewOrderId() {
    String date = DateUtil.format(new Date(), "yyMMddHHmm");
    long ttl = redisService.ttl(RedisConstant.REDIS_KEY_ORDER_ID + date);
    long id = 0;
    if (ttl <= 0) {
    id = redisService.incr(RedisConstant.REDIS_KEY_ORDER_ID + date);
    redisService.expire(RedisConstant.REDIS_KEY_ORDER_ID + date, 80);
    } else {
    id = redisService.incr(RedisConstant.REDIS_KEY_ORDER_ID + date);
    }
    return date + Strings.alignRight(id, 6, '0');
    }
    ```
    jinzhongyuan
        25
    jinzhongyuan  
       2020-06-11 14:34:35 +08:00
    @luxinfl 额,不是我说的这回事吗?
    zh841318441
        26
    zh841318441  
       2020-06-11 14:55:56 +08:00
    后端做幂等性
    一般是基于数据库方面建立唯一索引或者联合索引
    基于 redis 也可以,生成订单唯一号存储在 redis 里面,保存的时候先从 reids 取,如果 reids 里面有,存储并删除。如果没有则不保存,但是这种并发情况下要考虑一下锁。
    基于 token 也可以,实现情况和 redis 那种差不多.

    前端:做放重复点击,点击过后没返回结果,不能重复点击.
    luxinfl
        27
    luxinfl  
    OP
       2020-06-11 14:56:32 +08:00
    @jinzhongyuan 就是两条完全一模一样的请求,同时进到后台了。后台要怎么处理。就这么个意思
    luxinfl
        28
    luxinfl  
    OP
       2020-06-11 14:57:30 +08:00
    @zh841318441 这个 token 就是上面大佬说的那种 csrf 嘛?
    luxinfl
        29
    luxinfl  
    OP
       2020-06-11 14:58:23 +08:00
    @hantsy 这个前端是不是只要加个隐藏的表单字段就行了。数据什么的后台生成再校验?
    Veneris
        30
    Veneris  
       2020-06-11 15:07:18 +08:00
    redis 的 setnx,key 是 业务+业务 id+用户 id,set 成功则执行业务并 delete key,否则直接 return
    MiBAO
        31
    MiBAO  
       2020-06-11 15:23:59 +08:00
    @pinktu 思路不对呀,还是得讲究一个双向验证。
    youxiachai
        32
    youxiachai  
       2020-06-11 15:27:56 +08:00
    lz 是听不懂幂等是干嘛的....前面那么多人都给出标准答案了...
    为啥还要前端参与....
    no1xsyzy
        33
    no1xsyzy  
       2020-06-11 15:40:11 +08:00   ❤️ 4
    你试试看 v2 在一个主题里写点东西,然后切其他标签页访问个十几分钟在其他主题下做点回复,再回到这个主题的时候点回复,会失败一次,变成一个只有回复内容的网页,需要重新点一次回复。
    这个就是 CSRF,每次请求主题页面就请求一个 CSRF token,包含在 input[type="hidden"][name="once"] 里,过了有效期再发送请求的话,这个请求就会失效。
    luxinfl
        34
    luxinfl  
    OP
       2020-06-11 15:50:55 +08:00
    @no1xsyzy 现在是不是都流行这样搞?反正我们前台没要求我们生成个 token 给他们。。而且小公司,以前写页面的时候都是我们自己写的,也没考虑过这个。用的最多的就是 disabled
    hantsy
        35
    hantsy  
       2020-06-11 15:51:26 +08:00   ❤️ 2
    @luxinfl CSRF 只对当前请求有效。重复提交后的第二次的同样的 CSRF Code 到后端是无法通过验证的。

    一般传统 Web 框架都是隐藏字段,比如 JSF, Jakarta MVC 等,这些框架都是会自己有较验机制,不用手动代码检测。

    https://github.com/hantsy/jakartaee-mvc-sample/blob/master/src/main/webapp/WEB-INF/views/add.xhtml#L13-L15

    https://github.com/hantsy/jakartaee-mvc-sample/blob/master/src/main/java/com/example/web/TaskController.java#L89

    Spring Security 也支持 Csrf,可以用于表单验证(使用 Themleaf 等),或者 Http Header (用于 API )。Spring 官方教程与 Angular 结合的 Microservice 例子,https://spring.io/guides/tutorials/spring-security-and-angular-js/
    luxinfl
        36
    luxinfl  
    OP
       2020-06-11 15:52:05 +08:00
    @no1xsyzy 刚才回复你的时候就碰到你说的这个情况了,跳到了一个回复页面。。是因为我改了标签属性么,加了 disabled 么。。。
    hantsy
        37
    hantsy  
       2020-06-11 15:56:23 +08:00
    @luxinfl 不可能由前台,都是后台生成的。Spring Security 会默认加到 Header 上去的。
    hantsy
        38
    hantsy  
       2020-06-11 16:11:30 +08:00
    Spring 官方那个例子,以前用的 Angularjs,现在换成 Angular ( 2+)了。之前 Angularjs 中 CSRF 的名字是 XSRF,Spring Security 叫 csrf, 不一致还需要转换。 现在 Angular 不用了。
    hecz
        39
    hecz  
       2020-06-11 18:15:43 +08:00
    @shenlanAZ 幂等跟锁不是一个东西。。。。
    wj5868386
        40
    wj5868386  
       2020-06-11 18:23:15 +08:00
    两种处理方法,后端处理,前端处理完全能绕过去的,业务层 数据库层
    业务层:建立提交规则,符合规则的放行,不符合的丢弃。
    数据层:锁,唯一约束。
    业务层其实也就是锁的思路。
    SashaMu
        41
    SashaMu  
       2020-06-11 18:25:23 +08:00
    redis +1
    DanielGuo
        42
    DanielGuo  
       2020-06-11 18:38:28 +08:00
    插入之前判重,这叫幂等——你那个订单号可能不太够,最好是由前端传入一个值作 referenceId,前端连续的重复请求都是同一 referenceId 。插入时判断数据库中有没有此 referenceId,有的话就重复,不允许插入。
    防止并发插入——仅仅做了幂等也不够,若并发时也能重复插入成功。这时要做并发控制,可以在数据库层面将 referenceId 这个字段设置成唯一,由数据库阻止并发;也可以 单机的话直接 java 锁,分布式用 redis (不推荐,因为坑多)或 zookeeper 做分布式锁
    qloog
        43
    qloog  
       2020-06-11 18:41:34 +08:00
    简单点用 redis 的 set nx px 作为分布式锁来做频率控制进行处理。
    luzhh
        44
    luzhh  
       2020-06-11 18:42:27 +08:00
    我之前写过一个通用的框架来保证接口的幂等性,你这个问题我讲一下大概的思路:
    首先下单接口有若干个参数,n 次接口请求某几个参数值如果一样,那么认为是重复请求,那么把这几个参数拿出来组成一个在业务中可以是认为唯一的一个字符串,然后用 FutureTask 构造一个下单任务,将上面的唯一 key 和 task 保存在一个 map 中,每次请求接口时,先生成 ke 到 map 中看 value 存在不,如果存在那等待 task 的返回结果就行,如果不存在那就保存 task 到 map 中并调用 task.run 执行后续的下单业务逻辑。
    这样若干个相同请求进来了只会处理一次。
    上面是针对单个项目的情况,如果是集群部署的话 task 怎么保存和后面重复请求过来了怎么拿到请求结果,相应的用其他中间件改一下应该可以的。
    luxinfl
        45
    luxinfl  
    OP
       2020-06-11 19:15:02 +08:00
    @luzhh 这个 FutureTask 不是线程的那个吧。。。
    pinkrab
        46
    pinkrab  
       2020-06-11 19:52:32 +08:00
    这是一个幂等性问题,一般的处理方式有:
    数据库的唯一约束,这个最简单了,但是需要看业务场景。
    使用 token:提交数据的时候验证 token,一旦提交成功清除,下次来提交必会失败
    状态机幂等:一个数据在整个生命周期中所经历的状态,每次进行状态校验。
    mmdsun
        47
    mmdsun  
       2020-06-11 19:55:10 +08:00 via Android
    网关加的过滤。同一个用户,同一个接口在 1 秒内点击了两次。就过滤掉。
    opengps
        48
    opengps  
       2020-06-11 19:59:00 +08:00
    提交时候就加 requestid,同一个 requestid 都返回同一个结果
    luzhh
        49
    luzhh  
       2020-06-11 20:08:35 +08:00
    @luxinfl 就是线程相关的,通过这个可以等待并获取线程的执行结果。多个重复请求进来只有一个下单任务在执行,后续的请求直接等待结果即可。
    spicecch
        50
    spicecch  
       2020-06-11 20:28:40 +08:00 via iPhone
    赞成 3 楼的,这个前端来判断比较方便,服务器没有返回重复提交就给他弹窗,这个前端可以判断出来的,可以去看下 xhr 的状态
    Returnear
        51
    Returnear  
       2020-06-11 20:30:17 +08:00
    @Veneris 对的,严谨一点加个 3 秒过期
    yukiloh
        52
    yukiloh  
       2020-06-11 21:05:13 +08:00
    不需要改代码,让用户学会"风怒了,编辑"...
    gaius
        53
    gaius  
       2020-06-12 00:44:12 +08:00 via Android
    🐶参数做摘要然后分布式锁
    freebird1994
        54
    freebird1994  
       2020-06-12 00:56:34 +08:00 via Android
    分布式锁
    Vegetable
        55
    Vegetable  
       2020-06-12 01:41:43 +08:00
    后端只要做一部分就行了.

    我把所有非幂等操作 md5(body+token)用 redis 做了缓存,ttl 5 秒,重复值丢弃.建立在误点操作两次请求内容完全相同的前提下能正常工作.
    xcstream
        56
    xcstream  
       2020-06-12 01:54:11 +08:00
    放在 redis 里 key 就是页面生成时随机一个 id
    pinktu
        57
    pinktu  
       2020-06-12 09:16:57 +08:00
    @MiBAO 好吧!以前公司重复提交都是让我加按钮设置
    wupher
        58
    wupher  
       2020-06-12 09:17:19 +08:00
    不清楚你的使用场景。

    如果是业务类型,比如 A 用做 xx 操作时,禁止 A 用户同时做 xx 操作。比如不允许同时发两个密码不同的改密码请求。类似这种,比如商家转账,此时考虑用分布式锁,用数据库、Redis 、ZooKeeper 、ETCD 看你喜好和资源了。

    如果是类似安全类型的,比如重放攻击。可以通过对 Nonce 串进行 Hash 校验,对于重复的请求进行识别与过滤,及至屏蔽转移攻击。
    MiBAO
        59
    MiBAO  
       2020-06-12 09:18:05 +08:00
    @pinktu 哈哈 我们公司不知道还好 被知道了就会和你强调这种东西一定要双向验证
    guoyuchuan
        60
    guoyuchuan  
       2020-06-12 09:22:36 +08:00
    百度一下不就知道了吗
    Wuxj
        61
    Wuxj  
       2020-06-12 09:57:31 +08:00
    跳转到下单页的时候,将一些唯一性参数先传到后端生成唯一性 requestId,通过后端跳转到下单页,将生成的 requestId 放隐藏字段。提交订单的时候,根据 requestId 生成分布式锁。刚毕业那会做的,好像也没啥问题~仅供参考
    TomatoYuyuko
        62
    TomatoYuyuko  
       2020-06-12 10:52:41 +08:00
    扔前端做啊,哪有前端不做设置的
    sola97
        63
    sola97  
       2020-06-12 11:23:19 +08:00
    虽然我不懂,但前端处理防不了各种爬虫、脚本、秒杀软件吧
    zhupeng163
        64
    zhupeng163  
       2020-06-12 11:44:29 +08:00
    定义一个环绕增强的切面,切需要防重提交的 controller 甚至所有 controller,类名+方法名+所有参数拼接起来作为 key,用 redis setNx,给一个失效时间,setNx 返回 false 则抛重复提交异常。
    1107139144
        65
    1107139144  
       2020-06-12 11:51:13 +08:00
    请求加锁
    wanguorui123
        66
    wanguorui123  
       2020-06-12 12:56:27 +08:00 via iPhone
    用户锁+业务判断
    pythonee
        67
    pythonee  
       2020-06-12 14:44:28 +08:00
    csrf+1
    不过数据库的设计也有必要
    pythonee
        68
    pythonee  
       2020-06-12 14:44:57 +08:00
    @guoyuchuan 尽量避免百度哦
    guoyuchuan
        69
    guoyuchuan  
       2020-06-12 14:53:25 +08:00
    @pythonee #68
    那就谷歌
    ningyu1
        70
    ningyu1  
       2020-06-12 14:56:48 +08:00
    csrf+1
    ningyu1
        71
    ningyu1  
       2020-06-12 15:07:32 +08:00
    csrf 能控制一部分重复提交(同一个页面多次点击 button 提交), 另外一部分是相同的业务参数重复提交(刷新页面获取新的 csrftoken 但是提交请求的其他参数不变,多用在录制回放),这个还要配合后端的幂等控制,做法有很多,可以排序后 md5 放到 redis 中去验证是否存在,也可以使用临时表控制幂等。
    BlackBerry999
        72
    BlackBerry999  
       2020-06-12 16:37:16 +08:00
    前端做防抖节流
    wc951
        73
    wc951  
       2020-06-13 16:25:33 +08:00 via Android
    前端是为了用户体验,后端是为了业务安全,都要做防重放的
    reeco
        74
    reeco  
       2020-06-14 21:43:42 +08:00
    分布式悲观锁
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2642 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 10:55 · PVG 18:55 · LAX 02:55 · JFK 05:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.