基于version的MySQL并发无锁策略
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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;
总结:
本文试图利用乐观锁的思想,找到一种无锁的并发执行策略。