测试目的
本次测试目的是 消费 Redis List类型 里的数据 以各种方式来快速消费,得到最佳消费方式。消费框架为 spring boot,消费工具库为 lettuce,结合redisredisTemplate 的 api 来载入和消费数据,消费数据量分别为 1.5w、2w、10w。消费数据会提前加载到 Redis list 中,消费api 为 redisredisTemplate.opsForList().rightPop(key, Duration.ofSeconds(3)),该api 被封装为 redisSdk.LRightPopBlock(key,second)。
使用 ForkJoinPool 的方式测试
ForkJoinPool 的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。线程的数量是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。
测试机配置
CPU 类型
基准速度
系统类型
内存
内核
逻辑CPU
Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
1.99 GHz
64位
8G
4
8
单线程测试
单线程不存在 ForkJoinPool 的方式,所以这里没用到。
@Override
public void run(String... args) throws Exception {
long sumTime = 0;
while(true){
try{
long startTime = System.currentTimeMillis();
String str = redisSdk.LRightPopBlock("a",3);
if(!StrUtil.isEmpty(str)) {
long endTime = System.currentTimeMillis();
sumTime += endTime - startTime;
log.info("处理数据 {}, 累计时间 {}",str,sumTime);
}
}catch(Exception e){
log.error(e.getMessage());
}
}
}
测试结果
处理 15000 条数据,所需耗时 444s
ForkJoinPool 多线程消费代码
public void test2(){
int fcore = 2;
ForkJoinPool forkJoinPool = new ForkJoinPool(fcore);
AtomicInteger count = new AtomicInteger(0);
AtomicInteger breaks = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
IntStream.range(0,fcore).forEach(j ->{
forkJoinPool.execute(() -> {
while (true) {
try {
String str = redisSdk.LRightPopBlock("a", 3);
if (!StrUtil.isEmpty(str)) {
log.info("累计处理 {} 条数据", count.incrementAndGet());
} else {
breaks.incrementAndGet();
break;
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
});
});
while(true) {
try {
if(count.get() >= 15000) {
break;
}
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
log.info("总耗时 {}",endTime - startTime - 1000);
}
测试结果
并行线程数
耗时
消费数据
备注
2
210s
15000
4
108s
15000
6
79s
15000
8
81s
15000
16
31.234s
15000
32
36.271s
15000
ThreadPoolExecutor 测试
ThreadPoolExecutor 是一般线程池,ThreadPoolExecutor 我这里使用的是定长方式,和 Executors.newFixedThreadPool() 是一样的。代码如下:
public void test5(){
int fxcore = 4;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(fxcore, fxcore, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
AtomicInteger count = new AtomicInteger(0);
AtomicInteger breaks = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
IntStream.range(0,fxcore).forEach(j ->{
threadPoolExecutor.execute(() -> {
while (true) {
try {
String str = redisSdk.LRightPopBlock("a", 3);
if (!StrUtil.isEmpty(str)) {
log.info("累计处理 {} 条数据", count.incrementAndGet());
} else {
breaks.incrementAndGet();
break;
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
});
});
while(true) {
try {
if(count.get() >= 15000) {
break;
}
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
log.info("总耗时 {}",endTime - startTime - 1000);
}
测试结果
并行线程数
耗时
消费数据
备注
4
111.124s
15000
8
55.325s
15000
16
29.04s
15000
32
14.57s
15000
总结
ThreadPoolExecutor 开的线程越多,处理速度越快
ForkJoinPool + ThreadPoolExecutor 测试
ForkJoinPool + ThreadPoolExecutor 的测试方案是我自己想的一套,如果说 ForkJoinPool 可以充分让多核 cpu 处理任务,那让每个cpu在建立自己的多线程处理会不会更快呢?测试代码如下:
public void test2(){
int fcore = 4;
int fxcore = 16;
ForkJoinPool forkJoinPool = new ForkJoinPool(fcore);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(fxcore, fxcore, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
AtomicInteger count = new AtomicInteger(0);
AtomicInteger breaks = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
IntStream.range(0,fcore).forEach(j ->{
forkJoinPool.execute(() -> {
for(int i=0;i< fxcore/fcore ;i++) {
threadPoolExecutor.execute(() -> {
while (true) {
try {
String str = redisSdk.LRightPopBlock("a", 3);
if (!StrUtil.isEmpty(str)) {
log.info("累计处理 {} 条数据", count.incrementAndGet());
} else {
breaks.incrementAndGet();
break;
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
});
}
});
});
while(true) {
try {
if(count.get() >= 15000) {
break;
}
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
log.info("总耗时 {}",endTime - startTime - 1000);
}
测试结果
ForkJoinPool线程数
ThreadPool线程数
消费数据
耗时
备注
4
16
15000
28.55s
6
16
15000
37.55s
并行越多反而越慢
4
32
15000
15.53s
6
32
15000
16.15s
并行越多反而越慢,但线程多反而快
总结
看来并没有什么用,还是用 ThreadPool 速度最快。
Pipelined
查了很多方式,查到redis Pipelined也可以 使用rpop,代码如下:
public void test7(int i){
long startTime = System.currentTimeMillis();
List list = redisSdk.getTemplate().executePipelined(new RedisCallback() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
for(int i=0;i<20000;i++) {
connection.rPop("a".getBytes());
}
return null;
}
});
long endTime = System.currentTimeMillis();
log.info("共取到 {} 耗时 {}",list.size(),endTime-startTime);
}
测试结果
这种方式不像上面单条消费,这里可以自己存到list,然后在写消费程序。注意这里如果redis list数据没有那么多,可能取到的是 null 值,该null值为对象,不为字符串。这里的测试每次都会打开一个新的连接
数据量
时间
1W
980ms
2W
1790ms
3W
2880ms
多线程测试数据并发
private List list1 = new ArrayList<>();
private List list2 = new ArrayList<>();
private List list3 = new ArrayList<>();
public static void main(String[] args) {
SpringApplication.run(RealTimeLibraryApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
long[] a = LongStream.range(0, 10000).toArray();
String[] aArr = Stream.of(Arrays.toString(a)).collect(Collectors.joining("", "[", "]")).split(",");
redisSdk.LLeftSet("a", aArr);
System.out.println("初始化完毕");
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
for(int i=0;i<3;i++) {
int finalI = i;
threadPoolExecutor.execute(() -> test7(finalI));
}
while(true) {
if(list1.size() > 0 && list2.size() > 0 && list3.size() >0){
Thread.sleep(3000);
break;
}
Thread.sleep(1000);
}
// disjoint true 就是没有交集 false 就是有交集
log.info("list1 与 list2 交集存在为 {}", Collections.disjoint(list1,list2)?"否":"是");
log.info("list1 与 list2 交集存在为 {}",Collections.disjoint(list1,list3)?"否":"是");
log.info("list1 与 list2 交集存在为 {}",Collections.disjoint(list2,list3)?"否":"是");
}
public void test7(int i){
long startTime = System.currentTimeMillis();
List list = redisSdk.getTemplate().executePipelined(new RedisCallback() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
for(int i=0;i<20000;i++) {
connection.rPop("a".getBytes());
}
return null;
}
});
long endTime = System.currentTimeMillis();
log.info("共取到 {} 耗时 {}",list.size(),endTime-startTime);
while(list.contains(null)){
list.remove(null);
}
list.forEach(System.out::println);
if(i ==0){
list1.addAll(list);
}
if(i ==1){
list2.addAll(list);
}
if(i ==2){
list3.addAll(list);
}
}
测试结果发现数据差不多是均匀的分布在每个list中(每个list在3000+多),且数据没有重复,所以上集群是没有问题的。
业务数据模拟测试代码
@Override
public void run(String... args) throws Exception {
List list1 = new ArrayList<>();
for(int i=0;i<10000;i++){
list1.add("{\"sysId\":41040020001,\"mpntId\":1000070001,\"attrId\":1,\"dataItemId\":1000001,\"timestamp\":1603789510000,\"value\":1,\"key\":\"41040020001:1000070001:1:1000001\"}");
}
List list2 = new ArrayList<>();
for(int i=0;i<10000;i++){
list2.add("{\"sysId\":41040020001,\"mpntId\":1000070001,\"attrId\":1,\"dataItemId\":1000002,\"timestamp\":1603789510000,\"value\":1,\"key\":\"41040020001:1000070001:1:1000002\"}");
}
List list3 = new ArrayList<>();
for(int i=0;i<10000;i++){
list3.add("{\"sysId\":41040020001,\"mpntId\":1000070001,\"attrId\":1,\"dataItemId\":1000012,\"timestamp\":1603789510000,\"value\":1,\"key\":\"41040020001:1000070001:1:1000012\"}");
}
List list4 = new ArrayList<>();
for(int i=0;i<10000;i++){
list4.add("{\"sysId\":41040020001,\"mpntId\":1000070001,\"attrId\":1,\"dataItemId\":1000013,\"timestamp\":1603789510000,\"value\":1,\"key\":\"41040020001:1000070001:1:1000013\"}");
}
List list5 = new ArrayList<>();
for(int i=0;i<10000;i++){
list5.add("{\"sysId\":41040020001,\"mpntId\":1000070001,\"attrId\":1,\"dataItemId\":1000014,\"timestamp\":1603789510000,\"value\":1,\"key\":\"41040020001:1000070001:1:1000014\"}");
}
redisSdk.sAddAll("power:station:keys", Arrays.asList("41040020001"));
redisSdk.lLeftAddAll("41040020001",list1);
redisSdk.lLeftAddAll("41040020001",list2);
redisSdk.lLeftAddAll("41040020001",list3);
redisSdk.lLeftAddAll("41040020001",list4);
redisSdk.lLeftAddAll("41040020001",list5);
System.out.println("初始化完毕");
mainService.start();
}
测试结果打印
在测试代码里面针对不同的 dataItemId 各创建1W条数据。
获取数量
获取时间
整个流程所耗时间
3W
996 ms
1030 ms
3W
1084
1303 ms
3W
1006 ms
1032 ms
3W
982 ms
1027 ms
最终结论
以上的 Pipelined 测试为表现最好的一个,能批量处理数据,减少网络IO,在实际业务应用中测试的表现也优于其他方式,故针对实时库的消费方式定为此方式。
lettuce jedis 性能比较
使用的都是spring redis提供的开发API,在数据包大小差不多一样的情况下,没用到链接池。
lettuce(ms)
jedis(ms)
606
231
490
202
436
183
382
173
381
167
373
158
...
150
...
149
...
148
...
...
jedis 稳定在 148-149ms
lettuce 稳定在 373ms
补:这里引发的一个问题就是,我到底使用多少个线程会比较好呢?如果线程执行的是计算型任务可以核数 * 2,因为数值计算快,给太多线程会频繁切换线程。如果是io型(任务型,业务型),线程可以给几百上千都没问题,但是太多的线程会占用内存,所以根据内存分配。把处理器想象成一个队列(不恰当想象),那么多线程只是等待被执行而已。切记小心,防止oom。