V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
Jinnrry
V2EX  ›  问与答

一个高并发架构问题,求指点

  •  1
     
  •   Jinnrry · 2020-03-07 12:17:45 +08:00 · 5102 次点击
    这是一个创建于 1707 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前我们在做一个审核后台,每天早上运营需要领取任务。领任务逻辑大致这样

    // 1、先取数据
    select * from data where status = xxx limit 50
    
    
    // 2、写入任务表
    insert into task .......
    
    
    
    // 3、修改数据状态
    
    update data set status = xxx where xxxx
    

    但是!这样有个缺陷,如果多个人同时点领取,那么可能导致多个人领到同一条任务。目前想到的解决办法: 1、把这个操作写成一个事务,然后使用 serializable 隔离级别,保证每次只执行一个 2、把分任务的操作单独出来做出一个服务,使用单线程实现,保证每次只处理一个人的领取

    但是这 2 种方法好像都有点影响性能,虽然我们后台没什么关系,但是本着对技术的追求,想来问问各位大佬,有没有什么更好的解决方案?

    第 1 条附言  ·  2020-03-07 12:52:19 +08:00
    感谢每位回复的老哥,发帖主要目的是想借此问题学习下有什么好的解决方案,不考虑实现难度和成本。
    45 条回复    2020-03-08 09:54:48 +08:00
    sjw199166
        1
    sjw199166  
       2020-03-07 12:19:46 +08:00
    为什么不用队列呢
    Jinnrry
        2
    Jinnrry  
    OP
       2020-03-07 12:27:02 +08:00
    @sjw199166 #1 thanks,队列的话,把所有数据存一份进队列?而且,就算用队列,又如何保证队列每条数据只消费一次呢
    lhx2008
        3
    lhx2008  
       2020-03-07 12:28:35 +08:00 via Android
    INNODB 第一行加个 for update 就可以了,然后再测试一下有没有其他问题
    lhx2008
        4
    lhx2008  
       2020-03-07 12:29:08 +08:00 via Android
    这样性能会比较差,不过你这个场景不会有太多并发
    jadec0der
        5
    jadec0der  
       2020-03-07 12:30:13 +08:00
    没看懂,最简单的做法不是乐观锁吗?

    假设 status 0 是 未领取,1 是已领取
    先 update data set status = 1 where xxxx and status = 0
    返回值是 affected row,如果返回 1 说明抢到了,再 insert task,如果返回 0 说明没抢到,告诉员工重新取数据。
    Jinnrry
        6
    Jinnrry  
    OP
       2020-03-07 12:42:24 +08:00
    @lhx2008 #4 感谢,确实没啥问题,但是想学习一下高并发的时候咋办
    @jadec0der 直接更新数据表,那我咋知道领取到的 id 是哪些呢,因为返回的是只是一个修改了多少行啊
    cabing
        7
    cabing  
       2020-03-07 12:43:46 +08:00
    // 1、先取数据
    select * from data where status = xxx limit 50


    // 2、写入任务表
    insert into task .......



    // 3、修改数据状态

    update data set status = xxx where xxxx


    就像楼上说的 innodb 支持行锁

    1 select * from data id for update[这个时候其他的读都是阻塞的]
    2 update data set status = xxx where xxxx
    3 如果成功 insert into task .......
    cabing
        8
    cabing  
       2020-03-07 12:44:11 +08:00
    是 select * from data id = xxx
    Jinnrry
        9
    Jinnrry  
    OP
       2020-03-07 12:46:56 +08:00
    @cabing #7 加行锁确实没问题,但是因为遇到这个问题想就此学习下,如果是高并发的情况下咋办
    cabing
        10
    cabing  
       2020-03-07 12:47:27 +08:00
    目前这种方式是比较简单的。
    1 你不用引入额外的业务逻辑,比如你说的任务发号器,如果是多机部署就会有问题吧。
    2 不用引入 redis 之类的,这样你引入了外部依赖

    明白业务的关键点,简化根本复杂性,避免为了解决问题引入偶发可用性。
    Jinnrry
        11
    Jinnrry  
    OP
       2020-03-07 12:50:28 +08:00
    @cabing #10 引入 redis 之类的也没关系,发帖主要目的是学习有什么好的解决方案。单业务来说,我们后台就算领重复了也没关系,性能就算慢出翔问题也不大
    jadec0der
        12
    jadec0der  
       2020-03-07 12:51:25 +08:00
    @Jinnrry 一次更新多条是吗,那可以一条一条的 update,或者用一个事务重新 select 一遍状态再 update。

    我理解你的 1 2 条之间是隔了用户手动操作的时间吧?这样直接在 1 上加 select for update 是没有用的。
    Jinnrry
        13
    Jinnrry  
    OP
       2020-03-07 12:54:05 +08:00
    @jadec0der #12 不,没有手动操作,我的意思是,update 操作不能拿到数据 id 呀,没有 id 的情况下怎么插入任务表
    cabing
        14
    cabing  
       2020-03-07 12:57:54 +08:00
    @Jinnrry

    行锁的话最简单,50 条全选也没啥。50*20ms 算也很快。

    业务的难点是区分可领取任务多用户领取问题。重复领取的问题。只要标注出:task 状态,uid 和 task_id 关系就行。

    如果大的量,都是分布式 cache。你也可以考虑用 redis 玩一下。
    jadec0der
        15
    jadec0der  
       2020-03-07 12:59:11 +08:00
    哦,我理解错了,我以为是用户先取 50 个任务显示在界面上,然后手动勾选一些领任务。后台收到 id 之后改状态创建任务,如果有任务被抢了就部分成功。

    没有手动勾选的话就用一个事务包起来然后 select for update 就行,并不会慢很多。
    Jinnrry
        16
    Jinnrry  
    OP
       2020-03-07 13:02:41 +08:00
    @cabing #14 redis 的话,我的理解是,维护一个待领任务队列吧,比如让这个队列随时保持 1 万条待领取的任务,然后每次领任务操作从这个队列取数据。但是如何向这个队列补充数据又成难点了
    sagaxu
        17
    sagaxu  
       2020-03-07 13:03:36 +08:00 via Android
    高并发?几万个运营同时领任务吗?
    Jinnrry
        18
    Jinnrry  
    OP
       2020-03-07 13:04:46 +08:00
    @sagaxu #17 哈哈,只是假设高并发哈,借此问题向各位大佬学习下
    opengps
        19
    opengps  
       2020-03-07 13:12:26 +08:00 via Android
    同问并发点在哪?每个高并发业务其实都是有几个个特别需要着重处理的点,核心解决了,其他的也就顺便解决了
    codingadog
        20
    codingadog  
       2020-03-07 13:18:24 +08:00 via Android
    后台起线程,一直往 redis 队列里塞任务,这里加个分布式锁,始终只有一个实例在干塞任务的活,任务加入队列同时更新数据库标记任务已入队列。
    前台点击领取的时候直接从队列里取 50 个出来,更新对应的数据库行表明执行中,执行完成后更新数据库表示完成。
    如果塞任务的线程挂了,redis 里有任务但数据库入列状态未被更新,基本不会产生影响。
    如果数据库状态始终是已被领取,但长时间未产生变化的任务标记为失效,重新入列重新领取。
    Comdex
        21
    Comdex  
       2020-03-07 13:21:36 +08:00 via iPhone
    一般运营领任务很少见高并发,直接 select for update
    PDX
        22
    PDX  
       2020-03-07 13:46:32 +08:00 via iPhone
    这种问题我都是往 redis 里塞个 key 开控制并发
    watzds
        23
    watzds  
       2020-03-07 13:50:31 +08:00 via Android
    这 50 是啥,同时领 50 个任务?
    horryq
        24
    horryq  
       2020-03-07 13:57:32 +08:00
    起一个分配任务的线程来分配任务, 领任务的人插到队列里,分配线程消费这个队列, mpsc
    ferock
        25
    ferock  
       2020-03-07 14:17:04 +08:00
    @Jinnrry #6 msyql 下,了解一下 last_insert_id() 这个方法
    Jinnrry
        26
    Jinnrry  
    OP
       2020-03-07 14:18:18 +08:00
    @watzds #23 一次领 50 条数据进行审核
    ferock
        27
    ferock  
       2020-03-07 14:18:25 +08:00
    另外,处理领任务,这本来就是队列机制的本分啊?为啥不用呢?
    mcfog
        28
    mcfog  
       2020-03-07 14:22:02 +08:00 via Android
    虽然各种 db 内外的锁机制都解决这个问题,但还是建议考虑数据结构是否可以设计得更好

    比如为什么不让 status 变成(新增) xxx 的同时就建立 task ? 解耦 task 和 data 使得以后有其他 datum foo bar 业务表也可以复用 task 的结构和逻辑?

    当 task 是单纯的工单,自然领取任务这样的业务就都是单纯的单表操作,直接 update asignee where limit 就行

    “分配任务”这个例程还要读 data 这样的业务表就不合理,更别说去锁里面的数据了
    firefox12
        29
    firefox12  
       2020-03-07 14:47:49 +08:00
    就是不会用事务,才会有这种问题。以现在 db 的速度,什么性能问题,不存在的。
    kaneg
        30
    kaneg  
       2020-03-07 14:47:55 +08:00 via iPhone
    最近做了个项目,里面有类似的需求:从任务表中取出一批到期要执行的任务,然后存入下次执行的时间。在单机环境下工作没问题,但在 HA 模式下,会出现多个机器拿到同一批数据的问题。
    鉴于我们的数据量不大,采取的方式是最简单的 select for update。目前没发现什么问题,不知道这种方案是不是 best practice。
    sadfQED2
        31
    sadfQED2  
       2020-03-07 15:39:55 +08:00 via Android
    @mcfog 数据生产和数据审核,有可能是两个不同的团队负责的,不一定能随便改生产流程
    ferock
        32
    ferock  
       2020-03-07 15:46:30 +08:00
    @kaneg #30 当然不是啊
    dovme
        33
    dovme  
       2020-03-07 17:05:13 +08:00 via Android
    你把任务的步骤改一下,变成 132 不就解决了所有的问题??
    dovme
        34
    dovme  
       2020-03-07 17:06:29 +08:00 via Android
    @dovme #33 好像还是不行(▼皿▼#)。
    zgzhang
        35
    zgzhang  
       2020-03-07 18:02:19 +08:00
    低频操作我理解用 Redis 做一把全局锁就可以了
    Lock.lock();
    doSomeThing();
    Lock.unlock();
    lewis89
        36
    lewis89  
       2020-03-07 18:05:32 +08:00
    解耦 task 就好,看业务 应该是 data 里面一行数据 会对应 task 里面一行数据,应该在建立 data 一行的时候 建立相关联的一行 task,然后操作 data 的时候 发消息去更改 task 的状态,这样 task 天然就能做到幂等,即使多个业务同时操作数据 task 也状态也只会从 0 -> 1 转换一次
    npe
        37
    npe  
       2020-03-07 18:21:40 +08:00 via iPhone
    1.队列
    2.内存锁
    3.db 行锁
    GoLand
        38
    GoLand  
       2020-03-07 18:42:59 +08:00
    这个还需要锁吗....如果 task 表有 data 表的主键字段,给 task 表加上 data_id 的 unique key 不就行了吗。data 业务上可以重复取,但是一个 data 只能被一个人领取(对应 task 的 unique ),task 表写成功了继续更新 data 表的 status,unique key 冲突了表示任务被领取了,返回失败就可以了。
    22yune
        39
    22yune  
       2020-03-07 19:10:46 +08:00 via Android
    高并发一般瓶颈在共享资源。应先分析业务过程中哪些是共享的,哪些是可以并发的。
    做到高并发的重点在把共享资源的抢占尽量减少。
    建议在领任务前把任务分好批次,一个批次 50 个。加大了共享资源的粒度,使单次抢占做更多有效'功'。
    zjsxwc
        40
    zjsxwc  
       2020-03-07 20:59:08 +08:00 via Android
    不用数据库锁就队列加回调
    yufeng0681
        41
    yufeng0681  
       2020-03-07 21:05:58 +08:00
    方案 1:一楼时候的队列,消息队列,MQ,谁取出来,谁消费; 统一生产(将需要处理的任务放进 MQ )
    方案 2:第一步先进行 update,将运营 xxx 要处理的数据更新为 xxx 占用了;第二步查询的时候,多带一个角色的字段去查询 ,xxx 要处理的工作, 第四步更新时,将 xxx 也清除掉,表示任务完成。
    vindurriel
        42
    vindurriel  
       2020-03-07 21:38:31 +08:00 via iPhone
    处理高并发的思路在改串行 用单线程或锁都可以 推荐单线程因为开销少 锁的引入还可能需要额外的依赖 比如 redis

    非要加锁的话 高并发写的情况推荐悲观锁 提前碰撞

    如果串行的吞吐量不够 需要加并行 对数据分区 比如多条消息队列 多个 topic 最土的办法是 mysql id mod N
    helloSpringBoot
        43
    helloSpringBoot  
       2020-03-07 23:38:16 +08:00
    2、3 放到事务里面,3update 的时候加个 status 作为 where 的条件
    update count = 1:成功,提交事务
    update count = 0:失败,回滚
    mnssbe
        44
    mnssbe  
       2020-03-08 01:35:25 +08:00   ❤️ 1
    你这个是并发问题, 不是高并发问题
    hxtheone
        45
    hxtheone  
       2020-03-08 09:54:48 +08:00
    看并发量吧, 并发量小单数据库直接乐观锁搞定, 多库量大就分布式锁或队列
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2558 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 15:33 · PVG 23:33 · LAX 07:33 · JFK 10:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.