一个高并发架构问题,求指点 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Jinnrry
V2EX    问与答

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

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

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

    // 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
    同问并发点在哪?每个高并发业务其实都是有几个个特别需要着重处理的点,核心解决了,其他的也就顺便解决了
    Duolingo
        20
    Duolingo  
       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  
    PRO
       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  
    PRO
       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  
    PRO
       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     Solana     2820 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 13:32 PVG 21:32 LAX 06:32 JFK 09:32
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86