保华的Rails学习笔记

Refactor效能提升

效能提升基础概念

说到网站效能提升,一个网站大概分成几个结构

网站放置主机的地方(美国,日本,北京,等等等等)
前端代码结构(CSS/JavaScript)
后端代码结构
Ruby Code 效能
数据库的效能
那我们从哪里入手进行优化呢?
做优化,我们也要有最有效率的地方做起
也就是得从我们的「注意力」可以换取最高的网站提升效率速度来考虑优先级,「一天能够提升的有感速度」。
首先,地理位置是一定会对开启速度造成差异的。
例如,我们在中国访问heroku(它服务器在美国),速度明显很慢,可是我们访问天猫、京东等,明显速度快许多

网站代码产生网页的速度 + 下载网页的速度 + 下载素材的速度
网站代码产生网页的速度(通常在100ms-200ms)
下载网页(通常一张网页约 100-200KB)
下载素材 x 15(通常一张网页大约有 15 个以上素材)
由这个公式我们会总结出

下载所需时间 > (远大于)网页生成时间
如果你的网站放在美国(如 Heroku),素材又很多的话,网站会巨慢

调整前端架构或搬机器比起调整后端代码或数据库架构来说,性价比远远为高。

但是,搬机器的地理位置手续非常复杂,特别是机器往往不是一台的,而是一组的,依据地理位置分配资源的技术与架构实做起来会非常复杂。所以我们这种「个体户程序员」,提速的方向往往会以「修正前端架构」去思考。

购物网站代码重构(二)

1、用helper重写订单的created时间

在order的view页面中,出现了<%= order.created_at.to_s(:long) %>这样的、有一点艰涩的代码,我们可以对它进行优化,使它更具有可读性
在app/helpers/account/orders_helper.rb中加入

def render_order_time(order)
    order.created_at.to_s(:long)
end

在view页面中,就可以用render_order_time(order)来代替order.created_at.to_s(:long)了
修改app/views/account/orders/index.html.erb

   <td><%= link_to(order.id,order_path(order.token)) %></td>
-      <td><%= order.created_at.to_s(:long) %></td>
+      <td><%= render_order_time(order) %></td>
 </tr>

2、用partial优化app/views/products/index.html.erb页面

app/views/products/index.html.erb页面中,代码是这样写的

<div class="row">
  <% @products.each do |product| %>
    <div class="col-xs-6 col-md-3">
      <%= link_to product_path(product) do %>
        <% if product.image.present? %>
          <%= image_tag(product.image.thumb.url,class: "thumnial") %>
        <% else %>
          <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
        <% end %>
      <% end %>
      <%= product.title %> ¥ <%= product.price %>
    </div>
  <% end %>
</div>

修改它

<div class="row">
-  <% @products.each do |product| %>
-    <div class="col-xs-6 col-md-3">
-      <%= link_to product_path(product) do %>
-        <% if product.image.present? %>
-          <%= image_tag(product.image.thumb.url,class: "thumnial") %>
-        <% else %>
-          <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
-        <% end %>
-      <% end %>
-      <%= product.title %> ¥ <%= product.price %>
-    </div>
-  <% end %>
+  <%= render :partial => "product_item", :collection => @products, :as => :product %>

其中,

<%= render :partial => "product_item", :collection => @products, :as => :product %>

也可以用

<%= render: "product_item", collection: @products, as: "product" %>

来代替,两种写法,效果一样

同时创建新文件app/views/products/_product_item.html.erb,它的内容如下:

<div class="col-xs-6 col-md-3">
  <%= link_to product_path(product) do %>
    <% if product.image.present? %>
      <%= image_tag(product.image.thumb.url,class: "thumnial") %>
    <% else %>
      <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
    <% end %>
  <% end %>
  <%= product.title %> ¥ <%= product.price %>
</div>

此时我们查看rails s的log,可以看到


product_item已经被加载了

同时,我们要对app/views/admin/products/index.html.erb做同样的partial优化

<tbody>
-    <% @products.each do |product| %>
-      <tr>
-        <td>
-          <%= product.id %>
-        </td>
-        </td>
-        <td>
-          <%= link_to product_path(product) do %>
-            <% if product.image.present? %>
-              <%= image_tag(product.image.thumb.url, class: "thumbnail") %>
-            <% else %>
-              <%= image_tag("http://placehold.it/200x200&text=No Pic", class: "thumbnail") %>
-            <% end %>
-          <% end %>
-        </td>
-      <td>
-        <%= product.title %>
-      </td>
-      <td>
-        <%= product.price %>
-      </td>
-      <td>
-        <%= link_to("Edit",edit_admin_product_path(product)) %>
-      </td>
-      </tr>
-    <% end %>
+    <%= render partial: "product_item", collection: @products, as: "product" %>
   </tbody>

创新文件app/views/admin/products/_product_item.html.erb,它的内容如下:

<tr>
    <td>
      <%= product.id %>
    </td>
    <td>
      <%= link_to product_path(product) do %>
        <% if product.image.present? %>
          <%= image_tag(product.image.thumb.url, class: "thumbnail") %>
        <% else %>
          <%= image_tag("http://placehold.it/200x200&text=No Pic", class: "thumbnail") %>
        <% end %>
      <% end %>
    </td>
  <td>
    <%= product.title %>
  </td>
  <td>
    <%= product.price %>
  </td>
  <td>
    <%= link_to("Edit",edit_admin_product_path(product)) %>
  </td>
  </tr>

购物网站代码重构(一)

1、用before_action进行代码重构

在oders_controller中,发现在show、pay_with_alipay、pay_with_wechat中,都包含有@order = Order.find_by_token(params[:id])这一行代码,我们可以把@order = Order.find_by_token(params[:id])写到一个method中,并且在before_action中写入这个method,重构后的代码如下:

class OrdersController < ApplicationController
  before_action :authenticate_user!, only: [:create]
  before_action :find_order_by_token, only: [:show, :pay_with_alipay, :pay_with_wechat]
  def create
    @order = Order.new(order_params)
    @order.user = current_user
    @order.total = current_cart.total_price

    if @order.save

      current_cart.cart_items.each do |cart_item|

        product_list = ProductList.new
        product_list.order = @order
        product_list.product_name = cart_item.product.title
        product_list.product_price = cart_item.product.price
        product_list.quantity = cart_item.quantity
        product_list.save
      end

      OrderMailer.notify_order_placed(@order).deliver!
      redirect_to order_path(@order.token)
    else
      render 'carts/checkout'
    end
  end

  def show
    @product_lists = @order.product_lists
  end

  def pay_with_alipay
    @order.set_payment_with!("alipay")
    @order.make_payment!
    redirect_to order_path(@order.token), notice: "使用支付宝完成付款"
  end

  def pay_with_wechat
    @order.set_payment_with!("wechat")
    @order.make_payment!
    redirect_to order_path(@order.token), notice: "使用微信成功完成付款"
  end

  def apply_to_cancel
    @order = Order.find(params[:id])
    OrderMailer.apply_cancel(@order).deliver!
    flash[:notice] = "已提交申请"
    redirect_to :back
  end


  private

  def find_order_by_token
    @order = Order.find_by_token(params[:id])
  end

  def order_params
    params.require(:order).permit(:billing_name, :billing_address, :shipping_name, :shipping_address)
  end

end

2、利用继承重构

app/controllers/admin/orders_controller.rb和app/controllers/admin/products_controller.rb中
都包含有代码:

layout "admin"
  before_action :authenticate_user!
  before_action :admin_required

我们可以新建立一个admin_controller.rb,把这三行代码加入到这个admin_controller中

class AdminController < ApplicationController
  layout "admin"
  before_action :authenticate_user!
  before_action :admin_required
end

然后app/controllers/admin/orders_controller.rb和app/controllers/admin/products_controller.rb
继承这个admin_controller

-class Admin::OrdersController < ApplicationController
-  layout "admin"
-
-  before_action :authenticate_user!
-  before_action :admin_required
+class Admin::OrdersController < AdminController
-class Admin::ProductsController < ApplicationController
-  layout "admin"
-  before_action :authenticate_user!
-  before_action :admin_required
+class Admin::ProductsController < AdminController

3、mixin 与 ActiveSupport::Concern

在app/models/order.rb中

before_create :generate_token

  def generate_token
    self.token = SecureRandom.uuid
  end

这几行代码,其它地方也可能会用到,如果都复制、粘贴这三行,显的有些麻烦,我们可以在app/models/concerns
下边建立一个tokenable.rb文件,这个文件内容如下:

module Tokenable

    extend ActiveSupport::Concern

    included do
      before_create :generate_token
    end


    def generate_token
      self.token = SecureRandom.uuid
    end
end

现在修改app/models/order.rb,把之前的

before_create :generate_token

  def generate_token
    self.token = SecureRandom.uuid
  end

去掉,在app/models/order.rb上边添加include Tokenable,变为:

class Order < ApplicationRecord
   include AASM

+  include Tokenable
   aasm do
     state :order_placed, initial: true
     state :paid
   end

-  before_create :generate_token
-  def generate_token
-    self.token = SecureRandom.uuid
-  end

进行这三步重构后,代码看起来要清爽一些了

后端性能优化学习

一个网站,在后端方面,90%的机率是慢在数据库的操作的,10%是慢在ruby code代码,
所以我们在优化后端性能的时候,可以着重优化关于数据库的操作

我们可以从以下方面入手:
检测网站加载时间可以用:Rails 里面的测速神器:https://github.com/MiniProfiler/rack-mini-profiler

1、消灭N+1 Query

例如我们访问:


查服务器的logs信息,可以看到:

它执行了4次查询,即N+1 Query,第一个是查询groups的,后三个都是查询user表的,
如果网页数据比较多,会执行很多次查询,会大大的拖慢网站的加载速度,这时候我们可以用
includes(:class)的方式来解决N+1 Query的问题
例如在groups_controller中的

@groups = Group.all可以改为@groups = Group.includes(:user).all

这样在查询每个group的user时,执行一次sql查询就可以了

def index
    @groups = Group.includes(:user).all
  end

再找logs信息


这里它把查询user合并为查询一次了

2、消灭Table Scan

Table Scan 的意思是一般数据库,在没有加 Index (索引)的情况下,找资料是一笔一笔的循序的找
这样查询效率非常低,解决办法是
为某些经常查询的栏位,加上索引
加索引是新增一个 migration 然后打上 Index,例如

def change
   add_index :orders, :user_id
end

同样,我们也可以给布尔类型的栏位加索引,给日期栏位加索引,还可以给 state_machine 的条件加索引
索引的建立也是有效率区别的,它们按照索引效率高低分为:
boolean > integer > string > date > datetime

3、使用 counter cache

我们在查询资料有多少笔时,例如group.posts.count时,发现logs信息是执行了select count()这样的命令,
其实count(
)挺伤效能的
解决办法就是用counter cache,它的实现方法是为has_many的一方添加一个栏位(例如给group添加posts_count栏位)
然后在belogs_to的一方model中添加上counter_cache: true

4、用工具自动侦测 N+1 Query, Counter Cache

Bullet,参考地址: https://github.com/flyerhzm/bullet很好用的工具,我上一篇写过,这里不再赘述

bullet的安装及使用

bullet是一个很好的监测调试工具,它可以检测出网站中N+1 query,TableScan,Countercache问题
它的安装可以参考:
https://github.com/flyerhzm/bullet
我们可以按照下边的demo来安装,下边就是rails101为例来说明它的安装步骤以及使用方法

1、gemfile里边安装bullet

1)gem "bullet"


2)在终端执行bundle install
3)执行bundle install

2、在config/environments/development.rb中设置bullet

添加以下代码进去:

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
#  Bullet.growl = true
  Bullet.rails_logger = true
  Bullet.add_footer = true
end

如下图:

3、重启rails s

加载成功,下边我们用它检测网站


现在我们访问localhost:3000(即groups#index),网页弹出一个框


因为这个页面调用了user(group.user)
根据提示框的信息,我们修改groups_controller的index,修改如下:

此时我们观察logs,发现

我们访问my posts页面(localhost:3000/account/posts),弹出框


根据提示,我们修改account下边的posts_controller文件的index

点击弹出框的确定,然后刷新刚才的my posts页面,发现不再弹出了
此时观察logs,可以看到:

再检测group的view页面
弹出对话框,如下图:


根据提示, 我们修改groups_contrller中的show

counter cache的配置步骤

counter cache主要是用来优化数据库存查询的,例如在下边的应用场景下:

class Group < ApplicationRecord
  has_many :posts
end
class Post < ApplicationRecord
  belongs_to :group
end

在执行语句group.posts.count时,服务器log信息如下:


出现了很多个count(*),可是count(*)是很费服务器的效能的,如果数据量足够的时候,影响是非常大的

这正是counter cache要做的事情
它配置的步骤如下:

1、在post.rb中(belongs_to的一方),添加counter_cache: true

即把之前的belongs_to :group这一行修改为:
belongs_to :group, counter_cache: true

2、在groups表(即has_many的一方)中增加一个posts_count栏位

1)执行 rails g migration add_posts_count_to_group

2)在新产生的db/migrate/xxx_posts_count_to_group.rb文件中,添加

   add_column :groups, :posts_count, :integer, default: 0

    Group.pluck(:id).each do |i|
      Group.reset_counters(i, :posts) # 全部重算一次
    end

其中,后三行代码如果不加,那么counter cache对于之前的数据就没优化成功,所以需要添加上,重新计算,这样对于之前的历史数据,counter cache也可以起作用了

3)执行 rake db:migrate

注意:第1步与第2步,不能颠倒,如果先增加栏位,在rake db:migrate的时候,就会报错如下:


需要先修改post.rb,才能增加栏位posts_count

3、修改app/views/account/groups/index.html.erb页面中

把<%= group.posts.count %>修改为
<%= group.posts_count %>或者<%= group.posts.size %>
这样就配置完成了,现在log信息显示如下:

如果要查询posts_count这个栏位是否生效,可以去rails console中,用Group.all查询这个栏位的值

无主群组的另类解决方法(不用delete_all删除)

当一个table表有新增加栏位后,如果之前有历史数据,当再次展现这些数据时,就会报错,因为历史数据在新增加的栏位上,它的值是nil

例如在我们的rails101教材中,当给groups增加user_id栏位后,就出现了无主群组的报错,如下图


教材中,给我们的解决方案是

Group.delete_all

简单直接,对于刚开始做rails101的朋友来说,可以快速解除bug,顺利跳过这个坑

可是如果我们rails101做了好多遍,而且是在product环境中、groups表里已经有了很多数据(甚至是重要的数据),我们是否能保留这些数据、而且解决掉bug呢?答案当然是肯定的,下边我提供两种解决方案供参考

首先,我们可以先证实这些groups确实是无主的,办法是先进入rails console中,输入Group.all,如果执行结果中所有group的user_id栏位值都是nil,那就可以证实确实是无主群组问题引起的

第一种解决方案,其实也很简单,既然这些group的user_id值都为nil,那么我们给这些group的user_id批量赋个值就可以了,方法是先进入rails console中,然后输入

Group.update_all(user_id:1) 

如果没有user_id为1的用户,上边的代码可以修改为:

Group.update_all(user_id:User.first.id)


这样所有的group的user_id都为1了,group从无主变有有主,再刷新浏览器,问题就解决了

问题虽然解决了,可是所有group的user_id都为1了,不太符合真实环境下的数据,真实环境里边的数据,这里的user应该是分散的

为了实现user_id有一定的随机性,我们可以这样做

进入rails c环境中,执行下边的代码段

Group.all.each do |group|
   group.user_id = User.ids.sample
   group.save
end

注意:在rails c中执行上边的命令行时,可以一行一行输入
也可以直接复制我的这4行代码,到rails c中执行

其中,User.ids执行的结果是所有user的user_id所组成的数组,例如[1,2]
User.ids.sample的执行结果为user中id的随机值

得到的结果如下图


group的user有一定的离散性了,更符合真实环境下的数据

总之,解决无主群组的问题时,建议首先要确认是否真的是group无主问题,可大多数同学都是靠猜的(个人建议rails101做了好多遍的同学还是确认下,不但要知道解决方案,还要知道问题发生的根源),确认以后,给这些group的user_id赋值就可以了,无论是批量给group都赋同一个值(例如第一种方法),还是给每一个group随机赋个值(第二种方法),都是可以解决的

如何快速排除bug(一)

正式做全栈营线上助教一个星期了,还是挺有成就感的,帮助同学们取得进步的同时,也倒逼自己成长了许多,其实做助教如果想做好,每天要花的时间还挺多的,除了值班的时间段内替同学们解决问题,功夫往往还要下在别处,为了提高排查问题的速度,最近研究了一些linux命令,还是挺好用的,下边分享一下grep的简单、实用的用法

grep是搜索文本内容的强大工具,它把匹配出来的行展示出来,其中-i参数表示在匹配时,忽略大小写,-E参数后边可以用正则表达式,可以匹配多个内容,例如可以匹配包含has_many与belongs_to的行并显示到终端,善用grep命令行工具,可以大大提高排除bug的效率

heroku logs | grep -i error


当我们在上传heroku出错时,为了排查问题,会去看heroku logs信息,而heroku logs的信息,还是挺庞杂的,一行一行的找报错信息,还是挺费眼睛的,夸张一点,有点像大海捞针
直接执行heroku logs的结果如下图:


其实报错信息一般都会有error字眼,而heroku logs | grep -i error的作用,就是只显示包含有error的行,其他信息都不显示,这样大大减少了浏览器的log信息量,可以快速定位出错信息
用heroku logs | grep -i error的结果如下图:

git grep -i bootstrap

这个命令的作用是把这个专案中,文件中包含有bootstrap字眼的行展示出来,供我们集中查看,不用一个文件一个文件的排查问题了,大大提高了效率


我们直接可以在终端查看app/assets/javascripts/application.js、app/assets/stylesheets/application.scss和Gemfile中包含bootstrap的行了,更快的定位bug问题所在

git grep -i -E 'has_many|belongs_to'

这个命令的作用是把专案中的文件包含有has_many或者belongs_to的行列出来,展现到终端,它的执行结果如下:


当发生关联关系错误时,用这句命令可以快速定位问题所在,不需要一个一个文件打开,如上例中,相当于我们直接查看group.rb、post.rb、user.rb和group_relationship.rb这4个文件中起关联作用的行,效率提高了不少吧

grep的主要作用就是文本搜索,大家可以展开想象,扩展它的应用场景

使用一些简单的linux命令工具辅助查找问题,可以大大提高效率,这些命令的作用很强大,它有很多选项,不过不建议过度深究,能达到我们想要的效果,够用就行

git diff的用法

git diff [...]:这个命令最常用,在每次add进入index前会运行这个命令,查看即将add进入index时所做的内容修改,即working directory和index的差异。

git diff --cached [...]:这个命令初学者不太常用,却非常有用,它表示查看已经add进入index但是尚未commit的内容同最后一次commit时的内容的差异。即index和git directory的差异。

git diff --cached [] [...]:这个命令初学者用的更少,也非常有用,它表示查看已经add进入index但是尚未commit的内容同指定的之间的差异,和上面一条很相似,差别仅仅,即index和git directory中指定版本的差异。

git diff [...]:这个命令用来查看工作目录和指定的commit之间的差别,如果要和Git directory中最新版比较差别,则=HEAD。如果要和某一个branch比较差别,=分支名字

git diff [...]:这个命令用来比较git directory中任意两个之间的差别,如果想比较任意一个和最新版的差别,把其中一个换成HEAD即可。