Sidekiq 是Ruby社区最受欢迎的异步任务框架之一,几乎是Rails项目标配。
本文,我将从实际使用者的角度来提出疑问,通过一一解答这些问题来剖析Sidekiq是如何工作的。(代码基于Sidekiq-5.1.3)
在Web请求中,有很多任务是可以放到后台执行的,比如用户购买商品付款成功后,就可以直接向用户购买成功,相应的短信发送,物流通知等等就可以放到后台任务去做,不用在用户购买的同时立即执行,这些任务也称作异步任务。在Rails中,使用Sidekiq是这样的:
class HardWorker
include Sidekiq::Worker
sidekiq_options :retry => 5, queue: 'hard'
def perform(name)
# do something
end
end
HardWorker.perform_async('bob') # 异步执行任务
我们通常使用上面的方式,注册一个任务,让它异步执行。那么本文的第一个问题来了:
1. 上面的这条代码 HardWorker.perform_async('bob') 到底做了什么?
很容易看出这个类方法定义在 Sidekiq::Worker 里面,在源码中找到相关的代码:
def perform_async(*args)
client_push('class' => self, 'args' => args)
end
def client_push(item) # :nodoc:
pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options['pool'] || Sidekiq.redis_pool
# 有省略
Sidekiq::Client.new(pool).push(item)
end
可以看出,把自己的Class和方法的参数生成了一个hash,传递进了client_push方法, pool是一个redis连接, 调用了Client的实例方法, 通过一层层代码的查看,最终来到了我们最值得关注的一个方法:
def atomic_push(conn, payloads)
if payloads.first['at'] # 延时任务,例如指定了一分钟之后才执行
conn.zadd('schedule', payloads.map do |hash|
at = hash.delete('at').to_s
[at, Sidekiq.dump_json(hash)]
end)
else
q = payloads.first['queue']
now = Time.now.to_f
to_push = payloads.map do |entry|
entry['enqueued_at'] = now
Sidekiq.dump_json(entry)
end
conn.sadd('queues', q)
conn.lpush("queue:#{q}", to_push) #重点
end
end
payloads是通过上面item参数加工后得到的,里面除了之前的class,args参数之外,还会有sidekiq_options方法定制的部分参数,例如queue。 我们重新专注于上面的代码,第一个问题的答案已经很明显了。
从上面可以清晰的看出,异步执行相关的数据被打包成json,然后使用redis的lpush命令塞进了一个队列中,这个队列的名字和指定的queue有关系,按照我们上面的配置,这个队列的就是 "queue:hard"。写入队列的数据大概像这样:
{'class' => MyWorker, 'args' => [1, 2, 3]}
通过这些参数,就可以知道需要被执行的是哪个Worker,并且参数是什么。 注册异步任务到此就结束了,第二个问题随之来了。
2. 上面这些队列里的数据是怎么被消费的?
通过第一个问题,我们知道了,