2016年2月17日星期三

使用Redis + Rails来实现一个抽奖系统

春节期间,做了一系列的关于抽奖的小游戏,当大量的用户同时涌入的时候,需要能够快速正确的发出奖品。数据库全部使用的是 MySQL + Redis,MySQL用来存储用户的相关信息,Redis仅仅用来存储奖品数量和待发奖品。为此,对学习到的相关内容以及过程中出现的问题进行说明。

1、A公司的抽奖项目。抽奖比较简单,每人一次,抽到的奖品也都是同一种,因此使用一个计数器来控制奖品数量即可,抽奖的代码很简单:

if  $redis.decr(today_redis_key) > 0
# 更新MySQL中对应用户的中奖信息
end

2、B公司的一个优惠码抽奖项目。这个比上面这种又多了一个限制条件,就是优惠码的面额是不同的,从5元到200元不等,每天放出的数量也是不同的。这里面主要的问题是用户中奖了怎么才能随机的发出这些数量不同的优惠码,而且要保证不能多发,重复发。
         我的处理方法是设置一个奖池,将今天放出的所有优惠码放到 Redis 的 Set 里面,中奖用户从里面随机拿出一个,由于 Redis 中 Set 的 spop 函数是随机的,因此可以保证随机性。具体可以参考这里

项目中出现的问题:

  1. 高估了 MySQL在做 count 操作时候的性能,当几十万用户同时涌入的时候,抽奖函数没有问题,页面上显示中奖人数的地方反而成了性能瓶颈,这个也是当时测试不充分造成的;
  2. 对Redis操作尽量保证数据的一致性;



2015年12月14日星期一

连接远程机器,端口映射


这个主要是测试微信公众号的时候使用。速度快,而且很方便。
执行命令为:

ssh -C -N -g -R 80:127.0.0.1:3000 root@115.28.47.91

配置方法:
在远程机器的ssh配置文件 /etc/ssh/sshd.config 中添加以下内容:
GatewayPorts yes
然后执行命令,重启SSH
/etc/init.d/ssh restart 
/etc/init.d/sshd restart 

2015年12月8日星期二

使用Devise验证用户的设置

        最近项目中使用了 Devise 来做用户验证,不像以前只有简单的用户登录。这次开发涉及到不同角色用户登录,不同使用环境登录(电脑端+手机端),并且可以同时使用手机号码,特定商户号登录等,需要修正的地方有点多,特地记录下来,供下次使用。

1、不同角色用户登录。

直接使用一个 user 表作为基础,添加记录角色的字段即可

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

使用不同的角色类来继承这个表,并设置角色字段的值:

class BusinessUser < User
  default_scope { where(role: 1) }
end

class Admin < User
  default_scope { where(role: 0) }
end
在Routes文件中添加对不同角色的登录routes:
devise_for :admin
devise_for :business_user, controllers: {
  sessions: 'business_user/sessions',
  passwords: 'business_user/passwords'
}

在 application_controller.rb 文件添加这两个类的 Group
devise_group :user, contains: [ :business_user, :admin]


2、同时可以使用手机号码,邮件和特定商户号登录。

User 类中显示设置数据库查询的函数,并添加一个虚拟字段 Login 用来设置用户传入的内容:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :authentication_keys => [:login]

  attr_accessor :login

  def login=(login)
    @login = login
  end

  def login
    @login || self.business_num || self.phone || self.email
  end

  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login)
      where(conditions.to_h).where(["business_num = :value OR phone = :value OR email = :value", { :value => login }]).first
    else
      where(conditions.to_h).first
    end
  end

  def self.find_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login)
      where(conditions).where(["business_num = :value OR email = :value", { :value => login }]).first
    else
      if conditions[:business_num].nil?
        where(conditions).first
      else
        where(business_num: conditions[:business_num]).first
      end
    end
  end

end



在 application_controller.rb 文件中修正 Devise 的 strong parameters:
before_action :configure_permitted_parameters, if: :devise_controller? 
  private
    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_up) { |u| 
        u.permit(:business_num, :phone, :email, :password, :password_confirmation, :remember_me) 
      }
      devise_parameter_sanitizer.for(:sign_in) { |u|
       u.permit(:login, :business_num, :phone, :email, :password, :remember_me) 
      }
      devise_parameter_sanitizer.for(:account_update) { |u| 
        u.permit(:business_num, :phone, :email, :password, :password_confirmation, :current_password, :reset_password_token) 
      }
    end 

将原有的 Login Form 中的 Email 相关的内容替换为以下部分:
<%= f.text_field :login, :autofocus => true, :class => "loginInput", %>

3、设置电脑端登录和手机端登录失败后跳转到不同的页面。

主要是需要设置登录失败后,需要跳转到不同的页面,这个地方需要设置 warden

在 devise.rb 文件中添加代码,自定义用户登录失败后的跳转链接

config.warden do |manager|
    manager.failure_app = CustomFailure
  end


在 lib 文件中新建文件 custom_failure.rb 内容如下:
class CustomFailure < Devise::FailureApp

  def redirect_url

    if warden_options[:scope] == :business_user 
      session[:is_mobile] == true ? mobile_login_url : new_business_user_session_url 
    else
      new_admin_session_path 
    end

  end

  # You need to override respond to eliminate recall
  def respond
    if http_auth?
      http_auth
    else
      redirect
    end
  end
end

同时需要在 application.rb 中添加代码加载这个文件:
config.autoload_paths << Rails.root.join('lib')


参考资料: