版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://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;

总结:

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


如果我的文章对你有帮助,你可以给我打赏以促使我拿出更多的时间和精力来分享我的经验和思考总结。

微信支付码

anyShare分享到:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.