从用户到Redis请求过程分析
以最常用场景缓存为例,流量从用户到Redis Server的过程如下所示:
image- 用户访问后端服务器,调用对应的Controller
- Controller命中缓存记录,通过Jedis客户端调用Reids从缓存获取记录。 如果使用的Jedis连接池获取Jedis对象,从Jedis连接池获取一个Jedis连接实例。
- Jedis使用Redis序列化协议(RESP)将命令编码,放到Redis Server输入缓冲区中。
- Redis Server从输入缓冲区获取命令并执行。
- 执行结束后将执行结果放入到输出缓冲区。
- Jedis客户端从输出缓冲区获取执行结果并返回给Controller。
- Controller执行完业务逻辑相应用户的请求。
从上面时序图可以看出,用户请求通过Redis client经由网路到达Redis Server。
因此在考虑使用Redis性能的时候要从客户端和服务端两个角度考虑。 对于业务方来说, 合理使用Redis特性比Redis服务器的优化可操作性更强,也更容易获得好的效果。
下面将从业务优化和服务器优化两个方面介绍Redis的优化。
业务优化
查询本地redis的延迟通常低于1毫秒,而查询同一个数据中心的redis的延迟通常低于5毫秒。也就是说,网络传输的损耗为实际操作用时的5倍。
因此,从客户端角度,如何减少网络耗时至关重要。
使用连接池减少建立连接和销毁连接的时间开销
Jedis是Java语言使用最多的Redis客户端。 Jedis支持直连和连接池的两种方式。
直连的方式:
# 1\. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
Jedis jedis = new Jedis("127.0.0.1", 6379);
# 2\. jedis执行set操作
jedis.set("hello", "world");
# 3\. jedis执行get操作 value="world"
String value = jedis.get("hello");
所谓直连是指Jedis每次都会新建TCP 连接,使用后再断开连接。 我们都知道新建TCP连接经过3次握手,释放TCP连接经过4次挥手,新建和回收是非常耗时操作。对于频繁访问Redis的场景显然不是高效的使用方式。
Jedis也提供了连接池的方式
image// common-pool连接池配置,这里使用默认配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = null; try {
// 1\. 从连接池获取jedis对象
jedis = jedisPool.getResource();
// 2\. 执行操作
jedis.get("hello");
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
使用Pipeline或者Lua脚本减少请求次数
通过连接池,减少建立和断开TCP连接的时间开销。 另外,redis提供了其他三种方式,通过减少请求次数提升性能。 (1) 批量操作的命令,如mget,mset等 (2) pipeline方式 (3) Lua脚本
pipeline方式
使用redis-benchmark在Intel(R) Xeon(R) CPU E5520 @ 2.27GHz对比pipeline(每次16个命令)和普通请求。
使用pipeline的情况:
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -P 16 -q
SET: 552028.75 requests per second
GET: 707463.75 requests per second
LPUSH: 767459.75 requests per second
LPOP: 770119.38 requests per second
Intel(R) Xeon(R) CPU E5520 @ 2.27GHz (without pipelining)
无pipeline的情况:
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
SET: 122556.53 requests per second
GET: 123601.76 requests per second
LPUSH: 136752.14 requests per second
LPOP: 132424.03 requests per second
从benchmark的结果可以看出,使用pipeline技术比没有使用性能提升5-10倍左右。
Jedis支持Pipeline特性,我们知道 Redis提供了mget、mset方法,但是并没有提供mdel方法,如果想实现这个功 能,可以借助Pipeline来模拟批量删除,虽然不会像mget和mset那样是一个原 子命令,但是在绝大数场景下可以使用。
public void mdel(List<String> keys) {
Jedis jedis = new Jedis("127.0.0.1");
// 1)生成pipeline对象 Pipe
line pipeline = jedis.pipelined();
// 2)pipeline执行命令,注意此时命令并未真正执行
for (String key : keys) {
pipeline.del(key);
}
// 3)执行命令
pipeline.sync();
}
将del命令封装到pipeline中,可以调用pipeline.del(String key),此时不会真正的 执行命令。
使用pipeline.sync()完成此次pipeline对象的调用。
除了pipeline.sync(),还可以使用pipeline.syncAndReturnAll()将 pipeline的命令进行返回。
pipeline提升性能的原因
pipeline提升性能的一个原因是减少了命令总的RTT时间(往返时延), 另外一方面减少 总的系统调用的次数。
RTT(Round-Trip Time): 往返时延。在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。往返延时(RTT)由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。其中,前面两个部分的值作为一个TCP连接相对固定,路由器的缓存中的排队和处理时间会随着整个网络拥塞程度的变化而变化。所以RTT的变化在一定程度上反映了网络拥塞程度的变化。简单来说就是发送方从发送数据开始,到收到来自接受方的确认信息所经历的时间。
pipline和lua脚本的不同
Redis原生支持Lua语言,并且提供了通过客戶端执行lua脚本的命令。
image比如我们可以用Lua脚本在低版本的Redis上实现分布式锁。
# 1\. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
Jedis jedis = new Jedis("127.0.0.1", 6379);
# 2\. jedis执行set操作
jedis.set("hello", "world");
# 3\. jedis执行get操作 value="world"
String value = jedis.get("hello");
调用EVAL命令可以传入不定的KEY和ARGS的值, 这些值被可以通过KEY[i]和ARGV[i]访问对应的入参,并且通过return返回执行结果。
更多的Lua脚本,会在其他文章中介绍。
可以关注微信公众号:非典型理科男,查看全部文章列表阅读Lua脚本相关的文章。
pipeline和Lua比较:
(1) 返回结果不同: pipeline会把命令执行结果都返回出来, lua脚本只有一个返回结果。
(2) 使用场景不同: lua脚本可以提供复杂逻辑运算并且提供了缓存脚本的功能,提升像原生命令一样的性能体验。 因此lua脚本可以用在处理逻辑复杂,不需要返回或者只返回操作结果的场景。 pipeline用在合并命令减少执行开销和redis server压力的场景下。
在使用pipeline时有几个注意事项:
(1) pipeline执行命令虽然没有明确的执行命令数量的限制,但是建议限制执行命令数量。 执行命令数量过多一方面占用网络带宽,另一方面会阻塞客户端。
Redis Server性能影响因素
影响Redis Server性能主要有硬件、数据分布和配置有关。
硬件因素
Redis喜欢下面的硬件条件:
- 高带宽,低延迟的网络: Redis的性能中网络带宽和延迟通常是最大短板。因此,需要选择高带宽,低延迟的网络。
- 大缓存快速 CPU: 而不是多核。这种场景下面,比较推荐 Intel CPU。AMD CPU 可能只有 Intel CPU 的一半性能(通过对 Nehalem EP/Westmere EP/Sandy 平台的对比)。 当其他条件相当时候,CPU 就成了 redis-benchmark 的限制因素。
- 大对象(>10k)存储时内存和带宽显得尤其重要。 但是更重要是优化大对象的存储。
- 将Redis运行在物理机器上:Redis 在 VM 上会变慢。虚拟化对普通操作会有额外的消耗,Redis 对系统调用和网络终端不会有太多的 overhead。建议把 Redis 运行在物理机器上。
大Value的影响
包大小影响Redis的相应速度。 以太网网数据包在 1500 bytes 以下时, 将多条命令包装成 pipelining 可以大大提高效率。事实上,处理 10 bytes,100 bytes, 1000 bytes 的请求时候,吞吐量是差不多的,详细可以见下图。
image所以,当大value(>10k)存在时要及时优化掉。
网友评论