26 February 2015

前言

我成功的将一件任务,从去年年末拖到今天。将文章的中的股票名转换为链接。以下是一些代码的迭代:

# 第一版
$redis.sadd "stock:chinese_name" , Regexp.escape(Stock.only(:chinese_name).map(:chinese_name))
$redis.smembers("stock:chinese_name").each do |n|
  content.gsub(/n/).each do |x|
    st = Stock.where(chinese_name: n).first
    st ? "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>" : x
  end
end

# 第二版
$redis.sadd "stocks:chinese_name" , Stock.only(:chinese_name).map { |x| Regexp.escape(x.chinese_name) }
content.gsub(/[$redis.smembers("stocks:chinese_name").join("|")]/).each do |x|
  st = Stock.where(chinese_name: x).first
  st ? "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>" : x
end

# 输出失败, 打印了一下,看到转换成功了,看来是自己被检测输出给坑了,应该打印出来看看

# 重试正则表达式:

re = Regexp.new($redis.smembers("stocks:chinese_name").join("|"))
content.gsub(re).each do |x|
  p x
  st = Stock.where(chinese_name: x).first
  p "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>"
  st ? "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>" : x
end

content.gsub(/上汽集团|新华保险/).each do |x|
  st = Stock.where(chinese_name: x).first
  p "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>"
  st ? "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>" : x
end

# 最终版, views的辅助方法
def convert_stock_to_link(content)
  re = Regexp.new($redis.smembers("stocks:chinese_name").join("|"))
  content.gsub(re).each do |x|
    st = Stock.where(chinese_name: x).first
    # p "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>"
    st ? "<strong><a href='/asset_class/stocks/#{st.slug}' target='_blank'>#{x}</a></strong>" : x
  end
end

上面的代码性能不好,但是,实现最简单,调整思路,换了个复杂的方法实现。

# 改变思路了,只要存个正则表达式字符串就行了,表达式构建如下: 
$redis.set "stocks:chinese_name:regexp" , Stock.only(:chinese_name).map { |x| Regexp.escape(x.chinese_name) }.uniq.join('|')

新的正则表达式式方法被前辈否决,回退过去。

原本,想尝试使用后台任务处理的,最后还是选择的简单易写的before_save回调:

# 新的view辅助方法
def convert_stock_to_link(article)
  return article.content if article.stocks.nil?
  re = Regexp.new(article.stocks.only(:chinese_name).map { |x| Regexp.escape(x.chinese_name) }.join('|'))
  article.content.gsub(re).each do |x|
    st = Stock.where(chinese_name: x).first
    # p "<strong><a href='/asset_class/stocks/#{st.slug}'>#{x}</a></strong>"
    st ? "<a href='/asset_class/stocks/#{st.slug}' target='_blank' >#{x}</a>" : x
  end
end

# Article模型的before_save回调
class Article
  ...
  include Mongoid::Document
  has_and_belongs_to_many :stocks

  before_save :add_stocks

  private 
  
  def add_stocks
    if content_changed?
      re = Regexp.new($redis.smembers("stocks:chinese_name").join("|"))
      self.content.scan(re).each do |x|
        st = Stock.where(chinese_name: x).first
        self.stocks << st 
      end
    end
  end
  ....
end

# 创建的Rake任务,7千个文章,执行了半个小时,29万的话,大概要一天,我擦啊!!!
$redis.sadd "stocks:chinese_name" , Stock.only(:chinese_name).map { |x| Regexp.escape(x.chinese_name) }
re = Regexp.new($redis.smembers("stocks:chinese_name").join("|"))
Article.each do |article|
  article.content.scan(re).each do |x|
    st = Stock.where(chinese_name: x).first
    article.stocks << st # 等价于 => articles.stocks.push st
  end
  article.save
end

今天,前辈复查代码时,否决了使用redis集合的想法,觉得用哈希更好,所以,就改用hash表了:

# 沿着之前的sadd的思路折腾了好久,最后,换了个思路,干吗要一次搞定?? 循环遍历
Stock.only(:chinese_name).each { $redis.hset "stocks" , x.id , x.chinese_name  }

# 循环遍历的方案被否决了,
$redis.mapped_hmset("stocks",Stock.only(:chinese_name).map { |x| [ x.id, Regexp.escape(x.chinese_name) ] })
$redis.mapped_hmset("stocks",Stock.only(:chinese_name).map { |x| [ x.id ,  { id:  x.id,  chinese_name: x.chinese_name} ] })
# 取出来方法
$redis.hgetall("stocks").values

注: 在折腾Redis的hash表的用法时,费了不少时间,主要是各种猜,感觉编程就像猜谜一样: 这个方法行不行,有没有,试试吧, 咦,居然真的有这个方法。我去,报错了。啊,终于搞定了。

# 初版 & 终版
$redis.mapped_hmset("stocks",Stock.only(:chinese_name).map { |x| [ x.id , Regexp.escape(x.chinese_name) ] })
re = Regexp.new($redis.hgetall("stocks").values.join("|"))
Article.each do |article|
  article.content.scan(re).each do |x|
    #改进的话,需要使用chinese_name取到id, 怎么才能根据值去取key,hash结构设计有问题。
    st = Stock.where(chinese_name: x).first
    article.stocks << st
  end
  article.save
end
# 某中间版
# 放弃的原因是: article和stock是多对多的关系,单方面操作id会出问题的
$redis.mapped_hmset("stocks",Stock.only(:chinese_name).map { |x| [ x.id , x.chinese_name ] })
s = $redis.hgetall("stocks").invert # 反转hash,有时很很有用处
re = Regexp.new($redis.hgetall("stocks").values.map { |x| Regexp.escape(x) }.join("|"))
Article.each do |article|
  article.content.scan(re).each do |x|
    article.stock_ids << s[x] 
  end
  article.save
end

由于引入了Redis缓存,缓存就需要维护,所以,在模型中添加了一些过滤方法:

before_save :update_stock_cache
after_create :create_stock_cache
after_destory :remove_stock_cahce

protected

def create_stock_cache
  $redis.hset "stocks", self.id, Regexp.escape(self.chinese_name)
end

def remove_stock_cache
  $redis.hdel "stocks", self.id
end

def update_stock_cache
  $redis.hset("stocks", self.id, Regexp.escape(self.chinese_name)) if self.chinese_name_changed?
end

上述实现的视图辅助方法性能不行,修改为如下:

def convert_stock_to_link(article)
  content = article.content
  article.stocks.only(:chinese_name, :slug).each do |stock|
    content.gsub!(Regexp.escape(stock.chinese_name), "<a href='/asset_class/stocks/#{stock.slug}' target='_blank' class='blue_txt' >#{stock.chinese_name}</a>" )
  end
  content
end

后台任务的重写:

task :create_stock_ids => :environment do
  Stock.check_key "stocks"
  re = Regexp.new($redis.hgetall("stocks").values.join("|"))
  Article.each do |article|
    next if article.content.scan(re).blank?
    article.stocks = Stock.where(:chinese_name.in => article.content.scan(re))
    article.save
  end
end

备注: Article有29万条数据,发现Article.each性能有问题,修改为Article.find_each(batch_size: 5000)。 mongoid没有find_each方法,其存在的是batch_size(xxx).each方法,处理好慢。

备注:该任务最终有退回到了使用Redis的set实现,好比出去转了一圈,又回来了,心里到底是什么样的滋味。总的来说, 收获了不少经验,人也成长了很多。

通过观察Mongodb的调用执行长时间任务时,系统的负载、内存占用以及网络数据,运行发现了个现象,Mongoid似乎会每次批量的从数据库中获取一定的数据,具体的这个批量的值,大概是个固定的大小的内存数,还是固定数目的条数,不太清楚。

后记

我还是觉得最初写的view辅助方法很好,很简洁,就是慢了点而已,之后的实现要复杂的多,这就是所谓的分离。

哈哈,居然从年前拖到了年后,我还真是有才。话说,自己说自己有才,真不要脸。。。




傲娇的使用Disqus