基于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秒左右可以得到打败全国网站的百分比

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])

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 

 

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元编程。