目前我们在做一个审核后台,每天早上运营需要领取任务。领任务逻辑大致这样
// 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 sjw199166 2020-03-07 12:19:46 +08:00 为什么不用队列呢 |
2 Jinnrry OP @sjw199166 #1 thanks,队列的话,把所有数据存一份进队列?而且,就算用队列,又如何保证队列每条数据只消费一次呢 |
![]() | 3 lhx2008 2020-03-07 12:28:35 +08:00 via Android INNODB 第一行加个 for update 就可以了,然后再测试一下有没有其他问题 |
![]() | 4 lhx2008 2020-03-07 12:29:08 +08:00 via Android 这样性能会比较差,不过你这个场景不会有太多并发 |
![]() | 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 说明没抢到,告诉员工重新取数据。 |
6 Jinnrry OP |
![]() | 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 ....... |
![]() | 8 cabing 2020-03-07 12:44:11 +08:00 是 select * from data id = xxx |
![]() | 10 cabing 2020-03-07 12:47:27 +08:00 目前这种方式是比较简单的。 1 你不用引入额外的业务逻辑,比如你说的任务发号器,如果是多机部署就会有问题吧。 2 不用引入 redis 之类的,这样你引入了外部依赖 明白业务的关键点,简化根本复杂性,避免为了解决问题引入偶发可用性。 |
11 Jinnrry OP @cabing #10 引入 redis 之类的也没关系,发帖主要目的是学习有什么好的解决方案。单业务来说,我们后台就算领重复了也没关系,性能就算慢出翔问题也不大 |
![]() | 12 jadec0der 2020-03-07 12:51:25 +08:00 @Jinnrry 一次更新多条是吗,那可以一条一条的 update,或者用一个事务重新 select 一遍状态再 update。 我理解你的 1 2 条之间是隔了用户手动操作的时间吧?这样直接在 1 上加 select for update 是没有用的。 |
13 Jinnrry OP @jadec0der #12 不,没有手动操作,我的意思是,update 操作不能拿到数据 id 呀,没有 id 的情况下怎么插入任务表 |
![]() | 14 cabing 2020-03-07 12:57:54 +08:00 @Jinnrry 行锁的话最简单,50 条全选也没啥。50*20ms 算也很快。 业务的难点是区分可领取任务多用户领取问题。重复领取的问题。只要标注出:task 状态,uid 和 task_id 关系就行。 如果大的量,都是分布式 cache。你也可以考虑用 redis 玩一下。 |
![]() | 15 jadec0der 2020-03-07 12:59:11 +08:00 哦,我理解错了,我以为是用户先取 50 个任务显示在界面上,然后手动勾选一些领任务。后台收到 id 之后改状态创建任务,如果有任务被抢了就部分成功。 没有手动勾选的话就用一个事务包起来然后 select for update 就行,并不会慢很多。 |
16 Jinnrry OP @cabing #14 redis 的话,我的理解是,维护一个待领任务队列吧,比如让这个队列随时保持 1 万条待领取的任务,然后每次领任务操作从这个队列取数据。但是如何向这个队列补充数据又成难点了 |
![]() | 17 sagaxu 2020-03-07 13:03:36 +08:00 via Android 高并发?几万个运营同时领任务吗? |
![]() | 19 opengps 2020-03-07 13:12:26 +08:00 via Android 同问并发点在哪?每个高并发业务其实都是有几个个特别需要着重处理的点,核心解决了,其他的也就顺便解决了 |
![]() | 20 Duolingo 2020-03-07 13:18:24 +08:00 via Android 后台起线程,一直往 redis 队列里塞任务,这里加个分布式锁,始终只有一个实例在干塞任务的活,任务加入队列同时更新数据库标记任务已入队列。 前台点击领取的时候直接从队列里取 50 个出来,更新对应的数据库行表明执行中,执行完成后更新数据库表示完成。 如果塞任务的线程挂了,redis 里有任务但数据库入列状态未被更新,基本不会产生影响。 如果数据库状态始终是已被领取,但长时间未产生变化的任务标记为失效,重新入列重新领取。 |
![]() | 21 Comdex 2020-03-07 13:21:36 +08:00 via iPhone 一般运营领任务很少见高并发,直接 select for update |
![]() | 22 PDX 2020-03-07 13:46:32 +08:00 via iPhone 这种问题我都是往 redis 里塞个 key 开控制并发 |
![]() | 23 watzds 2020-03-07 13:50:31 +08:00 via Android 这 50 是啥,同时领 50 个任务? |
![]() | 24 horryq 2020-03-07 13:57:32 +08:00 起一个分配任务的线程来分配任务, 领任务的人插到队列里,分配线程消费这个队列, mpsc |
![]() | 27 ferock PRO 另外,处理领任务,这本来就是队列机制的本分啊?为啥不用呢? |
![]() | 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 这样的业务表就不合理,更别说去锁里面的数据了 |
29 firefox12 2020-03-07 14:47:49 +08:00 就是不会用事务,才会有这种问题。以现在 db 的速度,什么性能问题,不存在的。 |
30 kaneg 2020-03-07 14:47:55 +08:00 via iPhone 最近做了个项目,里面有类似的需求:从任务表中取出一批到期要执行的任务,然后存入下次执行的时间。在单机环境下工作没问题,但在 HA 模式下,会出现多个机器拿到同一批数据的问题。 鉴于我们的数据量不大,采取的方式是最简单的 select for update。目前没发现什么问题,不知道这种方案是不是 best practice。 |
![]() | 33 dovme 2020-03-07 17:05:13 +08:00 via Android 你把任务的步骤改一下,变成 132 不就解决了所有的问题?? |
35 zgzhang 2020-03-07 18:02:19 +08:00 低频操作我理解用 Redis 做一把全局锁就可以了 Lock.lock(); doSomeThing(); Lock.unlock(); |
![]() | 36 lewis89 2020-03-07 18:05:32 +08:00 解耦 task 就好,看业务 应该是 data 里面一行数据 会对应 task 里面一行数据,应该在建立 data 一行的时候 建立相关联的一行 task,然后操作 data 的时候 发消息去更改 task 的状态,这样 task 天然就能做到幂等,即使多个业务同时操作数据 task 也状态也只会从 0 -> 1 转换一次 |
![]() | 37 npe 2020-03-07 18:21:40 +08:00 via iPhone 1.队列 2.内存锁 3.db 行锁 |
![]() | 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 冲突了表示任务被领取了,返回失败就可以了。 |
39 22yune 2020-03-07 19:10:46 +08:00 via Android 高并发一般瓶颈在共享资源。应先分析业务过程中哪些是共享的,哪些是可以并发的。 做到高并发的重点在把共享资源的抢占尽量减少。 建议在领任务前把任务分好批次,一个批次 50 个。加大了共享资源的粒度,使单次抢占做更多有效'功'。 |
![]() | 40 zjsxwc 2020-03-07 20:59:08 +08:00 via Android 不用数据库锁就队列加回调 |
![]() | 41 yufeng0681 2020-03-07 21:05:58 +08:00 方案 1:一楼时候的队列,消息队列,MQ,谁取出来,谁消费; 统一生产(将需要处理的任务放进 MQ ) 方案 2:第一步先进行 update,将运营 xxx 要处理的数据更新为 xxx 占用了;第二步查询的时候,多带一个角色的字段去查询 ,xxx 要处理的工作, 第四步更新时,将 xxx 也清除掉,表示任务完成。 |
![]() | 42 vindurriel 2020-03-07 21:38:31 +08:00 via iPhone 处理高并发的思路在改串行 用单线程或锁都可以 推荐单线程因为开销少 锁的引入还可能需要额外的依赖 比如 redis 非要加锁的话 高并发写的情况推荐悲观锁 提前碰撞 如果串行的吞吐量不够 需要加并行 对数据分区 比如多条消息队列 多个 topic 最土的办法是 mysql id mod N |
![]() | 43 helloSpringBoot 2020-03-07 23:38:16 +08:00 2、3 放到事务里面,3update 的时候加个 status 作为 where 的条件 update count = 1:成功,提交事务 update count = 0:失败,回滚 |
44 mnssbe 2020-03-08 01:35:25 +08:00 ![]() 你这个是并发问题, 不是高并发问题 |
![]() | 45 hxtheone 2020-03-08 09:54:48 +08:00 看并发量吧, 并发量小单数据库直接乐观锁搞定, 多库量大就分布式锁或队列 |