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

后端如何处理接口幂等性?

  •  
  •   HarryQu · 2018-10-25 12:41:15 +08:00 · 8451 次点击
    这是一个创建于 2214 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在搞 Spring Boot ,遇到了这样的一个问题 : 菜鸡如我,想问问大家是如何处理的。

    场景 :

    有个移动端的用户,手特别快, 注册的时候瞬间连点 100 下 ,发出了 100 个请求 :


    问题 :

    @Service
    @Transactional
    public class UserService {
    
        @Autowired
        UserDao userDao;
        
        public boolean register(User user) {
            Optional<User> user = userDao.findByTelephone(user.telephone);
            if (user.isPresent()) {
                throw new IllegalStateException("账号已存在");
            }
            userDao.save(user);
            return true;
        }
    }
    

    register 是非线程安全的 ,这样就导致该用户重复注册,数据库中有多条数据。


    我的解决方案 : 加锁... 但是我觉得效率上个问题

    大家是如何处理的呢 ?

    第 1 条附言  ·  2018-10-25 15:26:25 +08:00

    有篇文章,读了之后有不少收获 :

    Innodb中的事务隔离级别和锁的关系

    38 条回复    2018-10-26 11:31:04 +08:00
    realityone
        1
    realityone  
       2018-10-25 12:44:26 +08:00 via iPhone
    手机号加个唯一索引
    helloworld12
        2
    helloworld12  
       2018-10-25 12:45:45 +08:00
    前端设置为短时间内只发送一个请求,
    服务端根据填写的信息 hash 计算出唯一 ID
    lastpass
        3
    lastpass  
       2018-10-25 12:49:14 +08:00 via Android   ❤️ 1
    首先在前端,点击一下之后就 disable,在后台数据没有返回之前不恢复。
    后端,加消息队列。
    用锁的话,加锁解锁的开销有点大。
    p2pCoder
        4
    p2pCoder  
       2018-10-25 12:51:59 +08:00
    单机的话
    ```
    synchronized (user.telephone.intern())
    ```
    分布式的话,直接给
    给 telephone 用 redis 加锁就行了

    性能影响不大吧
    HarryQu
        5
    HarryQu  
    OP
       2018-10-25 12:59:35 +08:00
    @realityone 我觉得这应该和索引么关系吧.
    awanabe
        6
    awanabe  
       2018-10-25 13:01:23 +08:00
    @HarryQu unique 索引可以直接在 db 层面上报错了...除了 insert 成功的那条之外..别的都返回 db 错误了...
    xuanbg
        7
    xuanbg  
       2018-10-25 13:02:25 +08:00
    这种非正常的请求,通过限流来防重发就可以了。然后,注册方法中可以增加对手机号是否已存在的判断,如已存在,就返回一个用户已存在的错误信息。
    HarryQu
        8
    HarryQu  
    OP
       2018-10-25 13:03:06 +08:00
    @lastpass
    因为我之前也是做前端的,这种问题,还得后端处理, disable 后, 用户立即退出又重新进入界面,再次提交数据。当然前端可以再次判断,只不过类似场景太多,写起来前端比较麻烦。
    消息队列的话,我还真是没用过,我研究下,谢谢。
    HarryQu
        9
    HarryQu  
    OP
       2018-10-25 13:07:59 +08:00
    @awanabe @realityone 好的 , 我试试。
    jjwjiang
        10
    jjwjiang  
       2018-10-25 13:08:49 +08:00
    既然 telephone 不能重复,那 DB 里就要 unique 呀,这个 error 完全可以通过 db 抛出来
    IssacTomatoTan
        11
    IssacTomatoTan  
       2018-10-25 13:09:28 +08:00 via Android
    后端做 体验 安全一次搞定
    lastpass
        12
    lastpass  
       2018-10-25 13:38:15 +08:00 via Android
    回复 @HarryQu 前端防君子不防小人,但能够过滤掉大部分问题。最终还是要后端来处理数据。前后端都要做。
    HarryQu
        13
    HarryQu  
    OP
       2018-10-25 13:42:04 +08:00
    @lastpass 嗯 有道理
    linbiaye
        14
    linbiaye  
       2018-10-25 13:58:00 +08:00
    1. 手机号加 unique, save 以后再 select 看看。
    linbiaye
        15
    linbiaye  
       2018-10-25 14:02:54 +08:00
    @linbiaye
    1. 手机号 unique
    2. 控制一下事务级别(MySQL READ COMMITTED),save 以后再 findByTelephone 看看是不是有大于 1 条记录。
    reus
        16
    reus  
       2018-10-25 14:03:00 +08:00
    当然是数据库加唯一索引

    如果这是线上服务,那……祝贵司好运,为啥不找合格的后端来做?
    98jiang
        17
    98jiang  
       2018-10-25 14:31:05 +08:00
    @reus 请问一下,合格的应该怎么做呢?
    HarryQu
        18
    HarryQu  
    OP
       2018-10-25 15:23:54 +08:00
    @linbiaye 谢谢,又学到了一点事务级别。
    p2pCoder
        19
    p2pCoder  
       2018-10-25 15:36:27 +08:00
    依据我浅薄的经验来看,把这种幂等性教研扔给 db 的唯一索引来做不合适,
    即使有唯一索引,也最好在应用程序中加锁来实现
    ppyybb
        20
    ppyybb  
       2018-10-25 15:49:35 +08:00 via iPhone
    数据库做唯一索引,当然二级 key 有唯一索引在 innodb 里面也会增加一点开销,会为插入的时候在二级索引上增加隐式锁(好像是的),但是开销应该较小。

    前端做控制,减少大部分误操作了。

    如果还不想加唯一的限制(万一有啥奇葩的需求..),那么我觉得每 30 分钟设置一个 redis 的 bloomfilter 去重吧,这样可以保证如果点过了就一定会被发现,没点过才去 db 检查。这样基本可以保证没有任何问题了。(实际上我觉得数据库做唯一索引基本上不可能出现问题了,而且很简单)
    q397064399
        21
    q397064399  
       2018-10-25 15:49:40 +08:00
    @p2pCoder #19 应用中加锁? 那不是要把用户注册信息加载到 Redis 里面 做分布式锁?
    ysweics
        22
    ysweics  
       2018-10-25 15:57:08 +08:00
    就像前面说的,手机号码这种字段,数据库里面加 unique 索引,然后代码处理的时候加锁,我觉得单机或者分布式用 redis 锁来解决
    metrxqin
        23
    metrxqin  
       2018-10-25 16:01:44 +08:00
    这后端太不称职,数据表设计有严重缺陷。
    p2pCoder
        24
    p2pCoder  
       2018-10-25 16:39:16 +08:00
    @q397064399 如果单机就是 普通的字段加锁就行
    上了多台服务,肯定要用 redis 或者 zk 之类对相应字段加锁
    我目前做的的都是 一个服务部署多台,用的是 redis 加锁
    总之,别把压力抛给 db
    Raymon111111
        25
    Raymon111111  
       2018-10-25 16:41:35 +08:00
    你这叫防并发不是幂等

    幂等是用户慢慢的点 100 下, 处理正确
    fkdog
        26
    fkdog  
       2018-10-25 16:48:32 +08:00
    首先 lz 一堆人把幂等和滤重混淆在一起了。什么是幂等?举个例子,就是一堆下订单的重复请求到达服务器,对所有的订单接口都应该返回一摸一样的订单号数据。

    所以对于幂等要求,正确的做法应该是,接到请求以后去数据库里查询,是否已经存在相关订单数据,如果是,则直接返回已有的订单数据详情。

    当然这个需要考虑一个情况,对于同一个商品,可能用户的确是手动下了两个一样的订单,所以需要判断是否是一定时间间隔范围内的。可以使用时间戳、唯一 token 等方式滤重。

    另外,确保订单入库的过程在同一个事物中。
    tabris17
        27
    tabris17  
       2018-10-25 16:49:13 +08:00
    @p2pCoder 程序怎么加锁?还要用分布式锁,这性能可靠性还不如数据库 unique 索引呢
    foolish1024
        28
    foolish1024  
       2018-10-25 16:49:18 +08:00
    乐观锁+唯一性索引
    richieboy
        29
    richieboy  
       2018-10-25 16:51:55 +08:00
    防并发就是要锁吧,数据库为什么不能重复唯一值,应该也是数据库自己加了锁才实现的吧?我们做的就是根据实际情况尽可能选择轻量级的锁
    p2pCoder
        30
    p2pCoder  
       2018-10-25 17:13:05 +08:00
    @tabris17 单机的话的直接对电话号码字段加锁就行
    如果部署多台分布式的话,肯定就代表业务的并发正在或者以后潜在面临挑战,在这种背景下,把压力扔给 db 不合适,数据库的唯一索引校验,也是给数据库压力,当然如果 db 能接受,数据库唯一索引也行
    这个唯一索引做幂等的本身的扩展性也是问题,当前业务下,我们是对 电话号码添加 unique 校验,但是后来 需要的是限制 姓名+电话号码,来防止重复注册,再到线上的已经膨胀的表中更改索引是不可取的,大表加个索引就可能影响整个 db

    如果是 单机的话,synchronized (user.telephone.intern())就行了,也不用考虑分布式锁带来的问题

    这为老兄的 user.telephone 写法也是很不讲究
    HarryQu
        31
    HarryQu  
    OP
       2018-10-25 18:16:34 +08:00
    @fkdog 不好意思,又查了下,确实我把概念给搞混淆了。
    mmdsun
        32
    mmdsun  
       2018-10-25 18:48:37 +08:00 via Android
    数据库加唯一索引就 OK 了
    ppyybb
        33
    ppyybb  
       2018-10-25 20:15:46 +08:00 via iPhone
    @fkdog 对于下单,不可能幂等,因为第一次一定会带来状态的变化。准确说这个接口是安全非幂等的。只是给建议的人知道楼主的实际需求所以没有纠结在这个概念上而已。去重显然能直接实现安全性
    519718366
        34
    519718366  
       2018-10-25 20:40:38 +08:00
    @Raymon111111 正解,感觉前 24 楼,全跑偏了
    519718366
        35
    519718366  
       2018-10-25 20:44:58 +08:00   ❤️ 1
    这是 leader 之前分享过的一个幂等性文章 https://blog.csdn.net/jks456/article/details/71453053
    做的 b2b,没什么并发压力,在这块比较弱,不能回答你的问题,只能贴上这个文章了
    luozic
        36
    luozic  
       2018-10-25 21:34:02 +08:00 via iPhone
    单个 Ip 地址限制请求频率… 这不是网关默认有的功能?
    passerbytiny
        37
    passerbytiny  
       2018-10-26 09:24:32 +08:00
    我看了你的代码,在已启动事务,并且查找到账户已存在的时候抛出"账号已存在",那么已经够了,用户点击 100 下,只有第一下回成功,后面的全部是“账号已存在”、“事务检测到脏数据”,或者“事务等待超时”。

    你这情况,应该找数据库的问题,代码没有任何问题。
    yc8332
        38
    yc8332  
       2018-10-26 11:31:04 +08:00
    数据库唯一必须的。然后就是接口是加锁的,不知道你说的效率什么意思。就是 mc/redis add 就能搞定了。。。还有就是前端要限制,尽量避免垃圾请求
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2911 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 42ms · UTC 08:00 · PVG 16:00 · LAX 00:00 · JFK 03:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.