基于redis做打败全国用户排名

2013年11月11日 13:45

360引领了开机速度,打败全国百分之多少的用户,很多产品纷纷开始模仿。基于redis可以快速构建一个性能优良的排名系统,假设用户每次操作一个url后获得一个分数,计算得到该分数在所有url中的排名。由于所有url可能是非常庞大的量级,用树的结构进行插入,排序,查询是效率较高的操作,基于redis的zset可以快速构建原型。

redis.zadd("sitescore",url)           # O(1)
total = redis.zcount("sitescore")     # O(1)
rank = redis.zrank("sitescore",url)   # O(log(n))

puts "打败了全国#{(rank * 100.0 / total).round(2)}%的网站"

简单测试,在10万级别的样本中,0.2秒左右可以得到打败全国网站的百分比

ruby字符串eval

2013年10月17日 17:21

在任务分发系统中,任务的内容使固定的,但是ID是根据时间动态生成的,一开始任务是在发送之前拼装的。后面任务的种类增加了,有的任务ID需要出现2次,发送前拼装就显得捉襟见肘了。ruby的string eval正好可以解决这个问题。先生成pattern,在执行的时候,用相应的需要替换的内容填充。用模式匹配,然后替换也可以,但是这样对于任务种类增加的时候,需要随之进行修改。

direct_content = 'Test ID=#{test_id}\n' + 
                 'url=script://#{test_id}.pts\n' + 
                 ...


test_id = rand(1000)_Time.new.strftime("%Y-%m-%d-%H")

puts eval( '"' + direct_content  + '"')

直接使用eval存在安全隐患,但是我的应用中不会存在,因为这些字符串是我自己的程序构建的。

redis键复制

2013年10月11日 11:52

redis并没有提供key的复制命令,但是可以通过redis提供的lua脚本实现。

require 'rubygems'
require 'redis'

r = Redis.new

KeyCopyScript = <<EOF
    local max_key = tonumber(ARGV[1])
    local i = 0
    while(i <= max_key) do
      local key_1 = "account:" .. i .. ":" .. KEYS[1]
      local res = redis.call('exists',key_1)
      if( res )
      then
        local key_2 = "account:" .. i .. ":" .. KEYS[2]
        local val = redis.call('get',key_1)
        redis.call('set',key_2,val)
        i = i + 1
      end
    end
EOF

puts r.eval(KeyCopyScript,["value_src","value_dest"],[2])

clockwork+sidekiq做耗时任务

2013年9月28日 10:29

sidekiq是一个基于colluloid的任务队列系统,在使用sidekiq之前,我自己用clockwork和redis构建了一个任务调度和分发系统,其中有一些统计的工作,需要遍历redis中的所有记录。随着记录的不断增长,遍历一次的时间越来越长,影响到clockwork的工作了,于是将这部分耗时的工作丢给sidekiq处理。

将原来的类变成sidekiq的worker,将所有这些类的rb文件在sidekiq_env.rb中require进来。

class HardJob
  include Sidekiq::Worker
  def perform opt=:take_snapshot
    case opt.to_sym
    when :take_snapshot
      HardJob.new.take_snapshot
    when
    ...
  end
  ...
end

clockwork的脚本中,require "sidekiq_env.rb"

HardJob.delay.perform_async :take_snapshot

在另一个终端中运行

sidekiq -r ./sidekiq_env.rb

从日志中可以看到sidekiq开始异步执行任务。

redis pipeline

2013年9月27日 18:50

 
在redis的一个集合中有上万的元素,这些元素对应对象属性有几个键值,原来通过逐个读取的方式访问,获取集合中所有元素的信息需要300多秒,严重影响性能,google后发现,redis提供pipeline来获取大量数据,改用pipeline实现后缩短到20秒
 
    $redis = Redis.new
    keys = $redis.smembers("userlist")
    res_ids = $redis.pipelined do
      keys.each do |client_id|
        $redis.get("account:#{client_id}:id")
      end 
    end 
    res = $redis.pipelined do
      res_ids.each do |id|
        x = "account:"+id
        lastlogin = $redis.hmget("#{x}:lastlogin","ip","time")
        firstlogin = $redis.hmget("#{x}:firstlogin","ip","time")
        multi_value = $redis.mget(x+":available",x+":network_type",x+":traffic_used",x+":traffic_wifi_used",
                   x+":tasks_today",x+":max_traffic",x+":tasks_wifi_today",x+":tasks_3g_today",
                   x+":tasks_wifi_finished",x+":tasks_3g_finished",x+":tasks_wifi_arrive",
                   x+":tasks_3g_arrive",x+":tasks_wifi_success",x+":tasks_3g_success",
                   x+":month_3g_traffic",x+":month_3g_traffic_yestoday",x+":month_2g_traffic",
                   x+":month_2g_traffic_yestoday",x+":month_wifi_traffic",
                   x+":month_wifi_traffic_yestoday",x+":system_info",x+":province_name",
                   x+":city_name",x+":isp_name",x+":software_version",
                   x+":task:send",x+":task:arrive",x+":task:finish",x+":task:pcapempty",
                   x+":tasks_3g_yestoday",x+":tasks_wifi_yestoday")
       [lastlogin , firstlogin , multi_value ]
      end 
    end 

 

eventmachine网络学习

2013年9月24日 21:10

EventMachine是Ruby一个基于Reactor设计模式的、用于网络编程和并发编程的事件驱动框架。

EventMachine的rdoc写的很详细,EventMachine可以处理任何协议,一些基本的协议已经实现,非常适合做服务器开发。

 

require 'eventmachine'
class Echo < EM::Connection
 def receive_data(data)
send_data(data)
 end
end
EM.run do
 EM.start_server("0.0.0.0", 10000, Echo)
end

主要有4个回调方法:

  • post_init 在实例初始化的时候调用
  • connection_completed 在连接建立后调用
  • receive_data(data) 当接收到客户端的数据时调用,数据以chunk的到达,需要自己负责处理
  • unbind 当客户端断开连接后调用

作为一个练习,掌握eventmachine的基本用法,这里是一个很好的例子。

使用god监视unicorn

2013年9月17日 19:27

最近使用nginx+unicorn+sinatra的api服务器要上线了,首选的是易于配置、扩展的gem,god

  • Config file is written in Ruby
  • Easily write your own custom conditions in Ruby
  • Supports both poll and event based conditions
  • Different poll conditions can have different intervals
  • Integrated notification system (write your own too!)
  • Easily control non-daemonizing scripts

简单配置

loop do
  puts 'Hello'
  sleep 1
end

 

God.watch do |w|
  w.name = "unicorn"
  w.start = "ruby /full/path/to/app.rb"
  w.keepalive
end

clockwork使用

2013年9月24日 21:15

在定时或者周期执行任务时,crontab是常用的工具,但是很多时候,crontab不是最好的选择。clockwork是ruby中替代crontab的一个gem。

在include Clockwork模块后,可以很直观,方便的管理任务,可以精确到秒的执行任务。

require 'clockwork'
include Clockwork

handler do |job|
  puts "Running #{job}"
end

every(10.seconds, 'frequent.job')
every(3.minutes, 'less.frequent.job')
every(1.hour, 'hourly.job')

every(1.day, 'midnight.job', :at => '00:00')

使用clockwork的一大好处是可以用ruby进行管理,复用redis连接,环境初始化等

 

 

ruby搭配redis使用

2013年9月11日 19:51

设计的客户端管理和任务分发系统使用redis存储,使用名字空间的方式进行管理

对每一个用户,分配一个id,设计键值

account:id:username
account:id:province
account:id:city
account:id:isp

...

一开始直接使用reids-rb进行操作,redis.get("account:#{id}:username")

后来发现一个操作redis的gem--Ohm,体验了ohm,内部也是这样的键值进行存储的,但是Ohm封装的很好,达到了类似activerecord的效果,不过由于系统需要和其他语言进行交互,如果用Ohm会不大方便。于是找到作者的另一个gem--nest,通过nest可以直接复用原来的设计,方便的操作。

account = Nest.new("account",redis)
account[id][:username].getNest

源码如下,非常简洁。

require "redis"

class Nest < String
  METHODS = [:append, :bitcount, :blpop, :brpop, :brpoplpush, :decr,
  :decrby, :del, :dump, :exists, :expire, :expireat, :get, :getbit,
  :getrange, :getset, :hdel, :hexists, :hget, :hgetall, :hincrby,
  :hincrbyfloat, :hkeys, :hlen, :hmget, :hmset, :hset, :hsetnx, :hvals,
  :incr, :incrby, :incrbyfloat, :lindex, :linsert, :llen, :lpop,
  :lpush, :lpushx, :lrange, :lrem, :lset, :ltrim, :move, :persist,
  :pexpire, :pexpireat, :psetex, :pttl, :publish, :rename, :renamenx,
  :restore, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :scard,
  :sdiff, :sdiffstore, :set, :setbit, :setex, :setnx, :setrange,
  :sinter, :sinterstore, :sismember, :smembers, :smove, :sort, :spop,
  :srandmember, :srem, :strlen, :subscribe, :sunion, :sunionstore,
  :ttl, :type, :unsubscribe, :watch, :zadd, :zcard, :zcount,
  :zincrby, :zinterstore, :zrange, :zrangebyscore, :zrank, :zrem,
  :zremrangebyrank, :zremrangebyscore, :zrevrange, :zrevrangebyscore,
  :zrevrank, :zscore, :zunionstore]

  attr :redis

  def initialize(key, redis = Redis.current)
    super(key.to_s)
    @redis = redis
  end

  def [](key)
    self.class.new("#{self}:#{key}", redis)
  end

  METHODS.each do |meth|
    define_method(meth) do |*args, &block|
      redis.send(meth, self, *args, &block)
    end
  end
end

Nest继承String类,使用dynamic methods动态定义方法对redis的操作进行封装,初看很简单,但是细细琢磨,有一些ruby中的细节需要熟悉。我们知道ruby每个方法的最后一行会成为返回值,那么initialize后,@redis是不是成为返回值了?答案是否,使用new实例化对象的时候,并不是简单的调用了initialize方法,new方法返回一个Nest对象。根据stackoverflow上的回答:

new is a class method, which generally creates an instance of the class (this deals with the tricky stuff like allocating memory that Ruby shields you from so you don't have to get too dirty).

Then, initialize, an instance method, tells the object to set its internal state up according to the parameters requested.

class Class
  def new(*args, &block)
    obj = allocate

    obj.initialize(*args, &block)
    # actually, this is obj.send(:initialize, …) because initialize is private

    obj
  end
end

Nest继承String,对象的值为字符串,作为redis的键值,Nest中的self就是键值,[]方法new新的Nest对象,形成键值的映射。通过send方法,将动态生成的Nest的实例方法派发到redis对象执行。

简单,优美的Ruby元编程。