目前我们在做一个审核后台,每天早上运营需要领取任务。领任务逻辑大致这样
// 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
codingadog 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 2020-03-07 14:18:25 +08:00
另外,处理领任务,这本来就是队列机制的本分啊?为啥不用呢?
|
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 1
你这个是并发问题, 不是高并发问题
|
45
hxtheone 2020-03-08 09:54:48 +08:00
看并发量吧, 并发量小单数据库直接乐观锁搞定, 多库量大就分布式锁或队列
|