redis性能测试:多线程向redis插入亿级数据的时间性能(一)

3,479 阅读4分钟

前言

微信公众号:潇雷 当努力到一定程度,幸运自与你不期而遇。

背景

公司的数据库技术选型采用redis,每天会产生8亿条的数据往里面筛入。因此需要测一波redis的存储性能。下面就记录下自己的测试过程,以及一些踩过的坑。

测试需求

测试用多线程向redis中插入亿级数据。预期用10条线程向redis中插入8亿条数据。

服务器性能

本次测试采用自己的虚拟机测试:

参数linux指令
系统cat /etc/redhat-releaseCentOS Linux release 7.5.1804 (Core)
内存free -htotal:3.7G available:3.3G
cpu数量cat /proc/cpuinfocpu cores:2
HZcat /proc/cpuinfo ] grep MHz ] uniq1991.999

一、Jedis 单独插入测试

插入1w条数据

1、单线程:

@Test
void exec() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    new Thread() {
            @Override
            public void run() {
                String key = "32021420001:90000300009999:10001:1601198414621:";
                long startTime = System.currentTimeMillis();
                for (int j = 1; j <= 10000; j++) {
                   jedis.set(key+j+"",key+j+"");
                }
                long endTime = System.currentTimeMillis();
                System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
            }
     }.start();
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(40000);
}

单线程插入1w条数据需要的时间为4.7s

main
exec time : Thread-2:4784

2、两个线程

@Test
void exec() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Jedis jedis2 = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    new Thread() {
            @Override
            public void run() {
                String key = "32021420001:90000300009999:10001:1601198414621:";
                long startTime = System.currentTimeMillis();
                for (int j = 1; j <= 5000; j++) {
                   jedis.set(key+j+"",key+j+"");
                }
                long endTime = System.currentTimeMillis();
                System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
            }
     }.start();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            for (int j = 5001; j <= 10000; j++) {
                jedis2.set(key+j+"",key+j+"");
            }
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(40000);
}

两个线程插入共1w条数据需要1.2s

main
exec time : Thread-2:1266
exec time : Thread-3:1277

3、四个线程插入1w条数据800多毫秒

main
exec time : Thread-4:877
exec time : Thread-2:878
exec time : Thread-3:879
exec time : Thread-5:884

4、十个线程插入1w条数据:500多毫秒

main
exec time : Thread-2:573
exec time : Thread-4:573
exec time : Thread-3:575
exec time : Thread-8:574
exec time : Thread-11:572
exec time : Thread-5:576
exec time : Thread-7:575
exec time : Thread-10:573
exec time : Thread-6:576
exec time : Thread-9:588

【结论分析】:可以看到时间曲线是正常的。线程越多,最终消耗的时间也会降低。但是redis号称10w的吞吐量,在使用这种普通方法插入时,效果却非常的差!!主要原因是我们插入的时候会产生多次连接操作,创建连接需要时间,同时,一个连接就会有数据包,多个数据包的传达网络并不能保证一致,这些都是影响我们大量数据插入性能的。

二、Jedis使用pipeline插入1w条数据

2.1 pipeline出现的背景:

redis客户端执行一条命令分4个过程: 发送命令->命令排队->命令执行->返回结果

这个过程被称为RTT(Round trip time,往返时间),mget,mset等命令有效的节约了RTT,个人理解这就类比于连接mysql获取数据,多次连接造成的io数据会降低性能。但是大部分命令不支持批量操作,需要消耗N次RTT,这个时候就出现了pipeline来解决这个问题。

2.2 pipeline的性能

1、没有使用pipeline执行N次命令的示意图:

2、使用pipeline执行N次命令:

2.3 测试:插入1w条数据

单线程

@Test
void aaaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Pipeline pipelined = jedis.pipelined();
    jedis.flushDB();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,10000).forEach(i->pipelined.set(key+i+"",i+""));
            pipelined.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(40000);
}

时间竟然来到了几十毫秒级别:

exec time : Thread-2:55

两个线程测试:

在插入一万条数据的时候时间变成了70毫秒

exec time : Thread-3:71
exec time : Thread-2:72

四个线程:

可以看到时间变得更长了:

exec time : Thread-4:100
exec time : Thread-5:101
exec time : Thread-2:103
exec time : Thread-3:103

猜测:可能是数据量不大,导致多线程处理时io开销占用时间太多。继续测试!

2.4 加大数据到1000w测试

/**
 * jedis 单线程测试
**/
@Test
void aaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    Pipeline pipeline=jedis.pipelined();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,10000000).forEach(i->pipeline.set(key+i+"",i+""));
            pipeline.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(500000);
}

测了三次之后,得出最终结论:单线程在插入一千万条数据耗时在70s左右。

2.5 多线程测试1000w数据

两个线程:jedis要关闭连接

@Test
void aaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Jedis jedis2 = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    jedis.select(3);
    Pipeline pipeline=jedis.pipelined();
    Pipeline pipeline2=jedis2.pipelined();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,5000000).forEach(
                    i->pipeline.set(key+i+"",i+"")
            );
            pipeline.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            if(jedis != null){
                jedis.close();
            }
            System.out.println("总 exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(5000000,10000000).forEach(
                    i->pipeline2.set(key+i+"",i+"")
            );
            pipeline2.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            if(jedis2 != null){
                jedis2.close();
            }
            System.out.println("总 exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(500000);
}
exec time : Thread-2:65198exec time : Thread-3:65197

五个线程:

exec time : Thread-2:41193exec time : Thread-6:42956exec time : Thread-4:44909exec time : Thread-5:44900exec time : Thread-3:44944

【结论分析】:多线程确实可以在一定程度上降低存储时间。

2.6 拓展思考

分析:for循环和IntStream.forEach()循环没有性能差距。但是如果IntStream后面加上parallel()的并行流操作,可以根据cpu的内核数量成倍的提升速度。但是stream.parallel.forEach()中执行的操作并非线程安全。在刚执行的时候出现了数组越界异常,因此不考虑。

IntStream.parallel().forEach()