某件拖了一年的任务
前言
我成功的将一件任务,从去年年末拖到今天。将文章的中的股票名转换为链接。以下是一些代码的迭代:
# 第一版
$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