Jinnrry
V2EX  ›  问与答

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

  •  1
     
  •   Jinnrry · Mar 7, 2020 · 6458 views
    This topic created in 2432 days ago, the information mentioned may be changed or developed.

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

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

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

    假设 status 0 是 未领取,1 是已领取
    先 update data set status = 1 where xxxx and status = 0
    返回值是 affected row,如果返回 1 说明抢到了,再 insert task,如果返回 0 说明没抢到,告诉员工重新取数据。
    Jinnrry
    Jinnrry
    OP
    Mar 7, 2020
    6
    @lhx2008 #4 感谢,确实没啥问题,但是想学习一下高并发的时候咋办
    @jadec0der 直接更新数据表,那我咋知道领取到的 id 是哪些呢,因为返回的是只是一个修改了多少行啊
    cabing
    cabing
    Mar 7, 2020
    7
    // 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
    cabing
    Mar 7, 2020
    8
    是 select * from data id = xxx
    Jinnrry
    Jinnrry
    OP
    Mar 7, 2020
    9
    @cabing #7 加行锁确实没问题,但是因为遇到这个问题想就此学习下,如果是高并发的情况下咋办
    cabing
    cabing
    Mar 7, 2020
    10
    目前这种方式是比较简单的。
    1 你不用引入额外的业务逻辑,比如你说的任务发号器,如果是多机部署就会有问题吧。
    2 不用引入 redis 之类的,这样你引入了外部依赖

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

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

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

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

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

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

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

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

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

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

    如果串行的吞吐量不够 需要加并行 对数据分区 比如多条消息队列 多个 topic 最土的办法是 mysql id mod N
    helloSpringBoot
    helloSpringBoot
    Mar 7, 2020
    43
    2、3 放到事务里面,3update 的时候加个 status 作为 where 的条件
    update count = 1:成功,提交事务
    update count = 0:失败,回滚
    mnssbe
    mnssbe
    Mar 8, 2020❤️1
    44
    你这个是并发问题, 不是高并发问题
    hxtheone
    hxtheone
    Mar 8, 2020
    45
    看并发量吧, 并发量小单数据库直接乐观锁搞定, 多库量大就分布式锁或队列
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   2660 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 61ms · UTC 14:23 · PVG 22:23 · LAX 06:23 · JFK 09:23
    ♥ Do have faith in what you're doing.