Rails 关于 Active Record 的线程安全问题

benzhang · 2016年08月02日 · 最后由 sg552sg552 回复于 2016年08月05日 · 3043 次阅读

最近在写点多线程的东西(其实就是一个爬虫)简单说就是把远程数据爬下来以后,然后存进数据库里。因为对多线程的代码不够自信,我都会把爬下来的数据再用单线程爬的数据去比较一下。但我发现 activerecord 的 update_attributes 这个方法好像并不是线程安全的。据我所知 Rails 4 是支持线程安全的了。不知道是我的理解有误还是我的代码在别的地方是非线程安全的。欢迎指正。以下是我的代码

class Business < ActiveRecord::Base
  def retrieve_logo
    threads = []
    [[40, 'logo_url_xs'], [80, 'logo_url_sm'], [160, 'logo_url_md'], [320, 'logo_url_lg']].each do |iter|
      threads << Thread.new do
        begin
          ActiveRecord::Base.connection_pool.with_connection do
            logo_response = JSON.parse Net::HTTP.get(URI("https://graph.facebook.com/#{account_uid}?fields=picture.width(#{iter[0]}).height(#{iter[0]})&access_token=#{account_user.access_token}"))
            if logo_response['error'].nil?
              update_attributes(iter[1] => logo_response['picture']['data']['url'])
            end
          end
        rescue
        end
      end
    end
    threads.each(&:join)
  end
end

爬虫的代码无非就类似

Business.find_each {|b| b.retrieve_logo }

以上代码,多线程跑的结果总会有几个 business 的 log 是空的。但我用 update_column 却没有问题。当然数据量大的时候才会有几个。 准备要补充的是我用的是 Rails 4.1.8 Ruby 2.1.5 (MRI)

这么写肯定有问题啊

代码层面的解决方案有: 建议对竞争资源加锁,将抓去和修改数据封装到一个类中。

数据库层面: 可以使用乐观锁或者悲观锁

具体还得实验

#1 楼 @chucai 感谢你的回复。我还在学习阶段。不知是否还能指出问题所在。不胜感激

但是看 update_attributes 的源码,发现他也是直接修改@attributes这个变量,也没有加锁,总感觉不是线程安全

你确定用 update_column 的时候真的拿到数据了?log 都没有,thread 表示这个锅不背

#3 楼 @nouse 我用 update_column 跑数据的时候已经把数据库清空了的。而且我用 update_attributes 的时候跑了好几次,每次都会清空数据库,然后用 git diff 比较。但 update_column 就没有问题,update_attributes 就有问题。根据线程安全的一些原理:any concurrent modifications to the same object are not thread-safe. update_column, update_attributes 确实是违反了这个准则,但因为 activerecord 是线程安全的,就像 Queue 一样,所以应该不会出问题才对的,也许是我对 activerecord 的线程安全理解有误,欢迎指正。

升级到 5.0.0 和 2.3.1 再讨论比较好,你用的版本都是官方不维护的,就算你发现了问题,官方也不会修。。。

rails4 是线程安全指的是这个程序本身是线程安全的(可以用多线程 server 跑)不代表你的用户代码不用考虑竞态条件,由于 update_attributes 从 assign_attributes 到 save 还有 validate 等一大票流程,会丢更新并不奇怪,你可以加锁,但是最好是把更新合并之后一次过保存

#6 楼 @mizuhashi 非常感谢你的回复。我终于明白了。

可以用update_attributes!,把异常抛出来看是哪里出问题了。 或者做个判断如果update_attributes返回false就把self.errors打出来看看。

update_column 是直接更新数据库了,应该不会担心并发问题。你可以先执行线程把数据取回,再调用更新 model 的属性。而不是最后每个线程都打开一个数据库连接。

#9 楼 @jimrokliu 谢谢提醒,已经这么做了😄

#9 楼 @jimrokliu 兄弟,加个微信吧!我的微信号:sg552sg552

需要 登录 后方可回复, 如果你还没有账号请 注册新账号