今天研究了 redis-rb 的源代码( gem 'redis' ), 分享一下 :
redis 服务本身设计为单线程执行,所以不需要锁机制,每个命令的执行都是原子操作,在前一个命令执行完毕后,才执行下一个命令,由于内存操作所以都很高效。网上的测试结果,读写次数可以达到10万次每秒。
redis 在默认的6379端口接收socket连接和请求,redis-rb 就是一个ruby编写的连接redis服务端的client sdk。 在用户的进程中如何使用redis的连接句柄?是每次使用时创建句柄?还是全局共享一个长连接的句柄(如何保证长连接句柄不超时)?如果是共享句柄的方式,这个句柄就是一个临界资源,多线程的情况下是否安全? 把更多的长连接句柄放入Pool中是否更高效?
少量请求
如果用户进程很少请求redis(很少是指5分钟或更久请求一次),因为redis默认的timeout 是300秒,所以这种情况,每次使用前创建句柄,使用后关闭。如果维持一个长连接的句柄,每次使用前还需要检查连接是否有效,如果无效,还是要reconnect一次。所以少量请求的场景下建议如下使用:
redis = Redis.new(host: "127.0.0.1", port: 6379)
redis.*** #命令 get set 等
redis.quit # 使用后关闭
如果你使用3.0.5及其更早的版本,你更需要使用上面的代码来及时释放不用的句柄,否则句柄数会一直增长(超时默认回收),有可能会导致句柄超过最大数量限制。3.0.7版本中修复了这个缺陷,即使不关闭,同一个pid,共享句柄。 参考 https://github.com/redis/redis-rb/issues/382
tower(https://tower.im/s/91i)在LU机制优化时就因为没有quit,导致句柄数超限,当时临时把timeout由300降低到30秒。
中等规模请求
考虑到网络延时等因素,假设一次redis请求耗时5ms,那么一个socket句柄,1秒钟可以处理200次请求。因此在用户进程中如果请求少于200次每秒,多余每5分钟1次,那么就数据中等规模请求。在这个场景下,建议在用户进程中保持一个全局共享的长连接句柄,
全局共享的变量可以保存在 config/environments/development.rb
redis = Redis.new(host: "127.0.0.1", port: 6379)
Redis.current = redis
如果使用 unicorn, 可以保存在 config/unicorn.rb
after_fork do |server, worker|
.....
redis = Redis.new(host: "127.0.0.1", port: 6379)
Redis.current = redis
end
使用时
redis = Redis.current
redis.get*** # 命令等
注意: Redis.current 本身是非线程安全的,看它的代码,如果@current未初始化,那么多线程执行会有问题,但如果在单线程中预先对这个做了设置就没有问题了,所以在进程初始化的时候做 current = 操作。
def self.current
@current ||= Redis.new
end
用户进程内,有可能时多线程的,如何保证多线程访问共享的句柄安全呢? 这就要求redis-rb 线程安全,简单的说就是,多个线程用一个句柄,写入时要排好队,一个个执行。
redis-rb 是这样来保证的,
def synchronize
mon_synchronize { yield(@client) }
end
def ping
synchronize do |client|
client.call([:ping])
end
end
所有的call调用,都封装在同步方法中。
还有一种情况,在用户的进程中使用redis,有可能是需要事务的。比如,select 和get命令需要放在一起执行不受其他线程干扰,这种情况可以使用 multi 方法。
redis.multi do |multi|
multi.select 1
multi.set("key", "value")
end
大规模请求
如果用户进程中,对redis调用的次数每秒上千次以上, 那么单个连接是无法满足要求的。redis的资料中介绍, 50个并发能达到10万次的读写, 一个连接理想情况下2000次访问也是到最高值了,如果是跨网络访问,性能还有下降一个数量级。
针对这种规模的请求,最好的办法就是同时创建多个句柄连接,然后放到共享池中。redis-rb 的文档中有提到,在以后会支持如下这种调用方式。
Redis.current = Redis::Pool.connect # 以后的版本会支持
可以配置多个连接,每次获取一个空闲的句柄使用。
也可以参考 Connection Pool Gem 地址 https://github.com/mperham/connection_pool
关于底层的连接方式(drivers)
redis-rb 目前支持3种连接方式, 如下, 第一种是默认的。
Redis::Connection.drivers << Redis::Connection::Ruby
Redis::Connection.drivers << Redis::Connection::Hiredis
Redis::Connection.drivers << Redis::Connection::Synchrony
建议使用第2种,
测试代码, 执行1万次调用,对比了 第一种和第二种方法的效率,1万次调用 hiredis能节省100ms左右。
%w[ruby hiredis].each do |d|
redis = Redis.new(host: "127.0.0.1", port: 6379, driver: "#{d}")
Redis.current = redis
redis.set("var1", 0)
t0 = Time.now
for i in (1..10000) do
Redis.current.incr("var1")
end
t1 = Time.now
p (t1-t0)
end
$ ruby test.rb
0.761553
0.68214
$ ruby test.rb
0.764066
0.659123
$ ruby test.rb
0.761662
0.637084