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

benzhang · 发布于 2016年08月02日 · 最后由 sg552sg552 回复于 2016年08月05日 · 1457 次阅读
1090

最近在写点多线程的东西(其实就是一个爬虫)简单说就是把远程数据爬下来以后,然后存进数据库里。因为对多线程的代码不够自信,我都会把爬下来的数据再用单线程爬的数据去比较一下。但我发现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)

共收到 11 条回复
983

这么写肯定有问题啊

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

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

具体还得实验

1090

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

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

775

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

1090

#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的线程安全理解有误,欢迎指正。

Eda824

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

23529

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

1090

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

3211

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

96

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

1090

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

2948

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

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