数据库我爱编程Java学习

Redis批量操作详解及性能分析

2018-04-18  本文已影响7099人  近路

在之前的文章中,我们对redis批量处理指令mget进行了压测并分析了性能瓶颈,显然通过mget批量执行指令可以节约网络连接和数据传输开销,在高并发场景下可以节约大量系统资源。本文中,我们更进一步,比较一下redis提供的几种批量执行指令的性能。

1.为什么需要批量执行redis指令

众所周知,Redis协议采取的是客户端-服务器方式,即在一次round trip中,客户端发送一条指令,服务端解析指令并执行,然后向客户端返回结果。这是一种典型的tcp交互方式。

粗略的分,客户端发起一次Redis请求主要有如下开销:

笔者曾经在一次性能调优中发现,每次服务请求访问redis次数高达数十次,使得redis请求次数达到服务qps的数十倍,触发了redis服务器的极限(大概5~10万qps)而导致服务性能低下,多个请求对redis连接池进行了激烈竞争,并且由于redis响应速度的下降导致大量线程在获取连接处阻塞并频繁进行线程切换。在改进实现采用了批量指令处理后,服务性能瞬间达到了数十倍的提升。

因此,如果每次服务掉用需要触发多次redis请求,合理地适用批量执行技术,可以使系统运行更加有效,数据吞吐得到明显提升。

2.redis批量指令介绍

Redis主要提供了以下几种批量操作方式:

2.1 批量命令

批量命令即redis对应的命令:

严格来说上述命令不属于批量操作,而是在一个指令中处理多个key。

优势:

缺点:

集群行为

代码示例:

Jedis jedis = pool.getResource();
try{
    long duration = System.currentTimeMillis();
    jedis.mget(keys);
    duration = System.currentTimeMillis() - duration;
    log(duration);
}finally {
    if(jedis!=null) jedis.close();
}

2.2 管道(pipelining)

管道(pipelining)方式意味着客户端可以在一次请求中发送多个命令。例如在下例中,一次将多个命令传给redis,redis将在一个round trip中完成多命令并依次返回结果。

$ printf "incr x\r\nincr x\r\nincr x\r\n" | nc localhost 6379
:1
:2
:3
$ printf "get x\r\ndel x\r\n" | nc localhost 6379
$1
3
:1

在上面的例子中,首先通过管道执行了三次incr x指令,第二次通过管道执行了get xdel x两个指令。

优势

缺点

集群行为

代码示例

Jedis jedis = pool.getResource();
try{
    Pipeline p = jedis.pipelined();
    try {
        // TODO
        // p.get("XXX");
        // p.get("XXX");
        resp = p.syncAndReturnAll();
    } finally {
        p.close();
    }
} catch (Exception e) {
    // TODO
}finally { if(jedis!=null) jedis.close(); }

2.3 事务操作

事务(Transactions)操作允许在一步中执行一组redis操作,并对这一组redis命令有如下保证:

事务操作相关命令:

优势

缺点

集群行为

代码示例

Jedis jedis = pool.getResource();
try {
    Transaction tx = jedis.multi();
    try {
        tx.get("XXX");
        tx.set("YYY","ZZZ")
        resp = tx.exec();
    } finally { tx.close(); }
} catch (Exception e) {
    // TODO
}finally { jedis.close(); }

2.4 基于管道的事务

在Redis中,管道是通过RESP,即redis协议来实现的,它允许在一个消息包中按照指定格式传递多个命令。而事务是通过命令实现的,因此管道和事务之间并不冲突,事务可以承载与管道之上。在某些场景,需要在一次请求处理中发起多次事务的场景下,通过引入管道,可以获得略高于单独执行多次事务的性能,但是两者的差距非常小,小到可以忽略。

代码示例

Jedis jedis = pool.getResource();
try {
    Pipeline p = jedis.pipelined();
    try {
        p.multi();
        //p.get(..);
        //p.set(..,..);
        resp = p.exec();
    } finally { p.close(); }
} catch (Exception e) {
    // TODO
} finally { jedis.close(); }

3.压测用例分析

针对上述4种批量操作,设计如下case:

单位:ms

SET性能压测结果

步进 1 10 100 1000 10,000 100,000 ALL
set 42128 - - - - - -
mset 43767 5142 1512 1188 1077 1072 -
pipeline 44039 6252 2312 1818 1714 1790 1836
transaction 45053 6866 2780 2292 2371 2618 2357
trans in pipeline 45063 6806 2760 2200 2304 2402 2372

表现为折线图如下:

SET

GET性能压测结果

步进 1 10 100 1000 10,000 100,000 ALL
get 39875 - - - - - -
mget 41499 4666 1159 794 935 982 -
pipeline 41693 5808 1677 1297 1354 1212 1551
transaction 43960 5902 2191 1685 1825 2015 2033
trans in pipeline 44848 6140 2026 1699 1822 1949 1985

表现为折线图如下:

SET

从上述测试结果中可以看出,不同的处理方式,最终性能曲线基本一致。

上一篇下一篇

猜你喜欢

热点阅读