Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc

引子:

有这么一种场景,对于外部系统提交的任务,我们要把任务扫出来,推送到
消息队列中,然后消费者监听在消息队列上, 取到任务进行消费。要防止任务被重复消费,扫出的任务要修改对应数据库状态值。

问题

假定数据库表结构为

task

字段 类型 说明 备注
id int 主键
task_id int 任务ID
status int 状态 0:等待中, 1:运行中, 2:成功, 3:失败
body string 任务body体
version string 为了区分写入成功的对象

我们知道把任务扫出来,至少需要执行3步操作
1) 扫描出等待中的任务

select * from task where status = 0 limit 10;

2)将扫出的任务推送到消息队列中
3) 修改任务状态
假定扫描出的任务task_id 分别为为1、2、3

update task set status = 1 where task_id in (1,2,3) and status = 0;

显然这个过程不是原子的,如果同时有多个scanner进行操作,显然会任务可能被重复推入消息队列中

方法1:

每次只允许一个Scanner执行扫描(可以利用MySQL的行锁,或者Redis的分布式锁)
方法1 显然是能够解决这个问题的,但是它的吞吐能力实在堪忧

方法2:

由于任务至少要被消费一次,我们其实担忧的是,任务被反复推送到消息队列中。因此我们引入version字段,用来标识这条记录是被谁修改的。谁修改了任务状态,就由谁负责把任务推动到消息队列中,这样也就避免了重复推送的问题。

看下面的例子

时序 Scanner1 Scanner2 备注
1 select * from task where status = 0 order by rand() limit 10 假定扫描出的任务task_id 分别为为1、2、3
2 select * from task where status = 0 order by rand() limit 10 假定扫描出的任务task_id 分别为为2、3、4
3 update task set status = 1, version = 'xx1' where task_id in (1,2,3) and status = 0;
4 update task set status = 1, version = 'xx2' where task_id in (2,3,4) and status = 0;
5 select * from task where task_id in (1,2,3) and version = 'xx1'
6 select * from task where task_id in (2,3,4) and version = 'xx2'

version可以用uuid生成,冲突的概率大概只有1/60亿, 冲突的可能性可以忽略
由于Scanner1只会推送version为"xx1"的任务,Scanner2只会推送version为"xx2"的任务,因此任务不会被重复推送。
注意: 我这里的SQL语句是随机取10条,否则Scanner1和Scanner2如果更新的是相同的记录,MySQL默认也是会加锁的
可以通过rand()函数让随机选出的数据尽可能错开, SQL语句形如下:

select * from task where status = 0 order by rand() limit 10

但由于使用了rand()函数以后,查询不走索引,所以查询语句需要改造成如下

select * from task t, (select id, rand() as r from task where status = 0 order by r limit 10) as p where t.id = p.id;

总结:

本文试图利用乐观锁的思想,找到一种无锁的并发执行策略。


请我喝瓶饮料

微信支付码

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注