写点什么

实战 Redis 序列化性能测试 (Kryo 和字符串)

作者:程序员欣宸
  • 2022 年 6 月 04 日
  • 本文字数:7918 字

    阅读完需:约 26 分钟

实战Redis序列化性能测试(Kryo和字符串)

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 在 Java 应用的开发中,有时候需要将 Java 对象实例保存在 Redis 中,常用方法有两种:


  1. 将对象序列化成字符串后存入 Redis;

  2. 将对象序列化成 byte 数组后存入 Redis;


  • 以上两种方式孰优孰劣?字符串方式来存取的好处是编码和调试更简单容易,而 byte 数组的优势又在哪里呢,今天我们针对这两种存储方式做一次对比试验,用数据来得出结论;

测试方法简述

  • 本次做的是对比测试,写 Redis 和读 Redis 都会测试到,测试一共有以下四种:


  1. 并发场景下对象通过 fastjson 转字符串,然后存入 Redis;

  2. 并发场景下对象通过 Kyro 序列化成 byte 数组,然后存入 Redis;

  3. 并发场景下从 Redis 取出字符串,通过 fastjson 转成对象;

  4. 并发场景下从 Redis 取出 byte 数组,然后通过 Kyro 反序列化成对象;

测试环境简介

  • 本次测试需要以下三台电脑,全部是 Linux:


  1. Redis 服务器;

  2. Web 应用服务器;

  3. 安装有 Apache bench,用于发起性能测试,并统计出测试结果;


  • 整体部署情况如下:

测试步骤梳理

  • 在正式开始前,先将所有步骤整理好以免遗漏,接下来一步一步进行就可以了:


  1. 部署 Redis;

  2. 开发基于字符串存取的 web 应用 redis-performance-demo-string;

  3. 开发基于 Kyro 序列化存取的 web 应用 redis-performance-demo-kryo;

  4. web 应用编译构建;

  5. 在测试端机器上安装 Apache bench;

  6. 部署应用 redis-performance-demo-string;

  7. 用 Apache bench 先 web server 发起请求,然后丢弃测试结果,这次请求中部分处理是在 JIT 之前完成的,不算数;

  8. 清理 Redis 数据,用 Apache bench 先 web server 再次发起请求,保存测试结果;

  9. 清理 Redis 数据,部署应用 redis-performance-demo-kryo;

  10. 用 Apache bench 先 web server 发起请求,然后丢弃测试结果,这次请求中部分处理是在 JIT 之前完成的,不算数;

  11. 清理 Redis 数据,用 Apache bench 先 web server 再次发起请求,保存测试结果;

  12. 对比结果,得出测试结论;

本章源码下载

  • 本章实战的源码可以在 github 下载,地址和链接信息如下表所示:



  • 这个 git 项目中有多个文件夹,本章源码在以下两个文件夹中:


  1. redis-performance-demo-string:对应字符串存取对象的应用;

  2. redis-performance-demo-kryo:对应 kryo 序列化对象的应用;


  • 如下图所示:

应用版本

  1. JDK:1.8.0_161;

  2. Maven:3.5.0;

  3. SpringBoot:1.4.1.RELEASE;

  4. Redis:3.2.12.;

  5. Fastjson:1.2.47;

  6. Kryo:4.0.0;

  7. Apache bench:2.3;

  8. Ubuntu:16.04.3 LTS;


  • 接下来我们开始实战吧;

部署 Redis

  • Redis 的安装和部署就不在本章展开了,以下两点请注意:


  1. 关闭 redis 远程保护:config set protected-mode "no"

  2. 修改 conf 文件,关闭持久化;

开发基于字符串存取的 web 应用 redis-performance-demo-string

  • 这是个基于 SpringBoot 的简单 web 应用,将几处重点列举出来:

  • 首先是 application.properties 文件中有 Redis 配置信息,请将 IP 和端口替换为您的 Redis 服务器的 IP 和端口:


spring.redis.database=0spring.redis.host=192.168.31.104spring.redis.port=6379spring.redis.pool.maxActive=8    spring.redis.pool.maxWait=-1    spring.redis.pool.maxIdle=8    spring.redis.pool.minIdle=0    spring.redis.timeout=0 
复制代码


  • 其次,是 web 接口对应的 controller 类 RedisController.java:


@Controllerpublic class RedisController {
private static final Logger logger = LoggerFactory.getLogger(RedisController.class);
private static AtomicInteger addPersionIdGenerator = new AtomicInteger(0);
private static AtomicInteger checkPersionIdGenerator = new AtomicInteger(0);
private static final String PREFIX = "person_";
private static final int TIMES = 100;
@Autowired private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/save/{key}/{value}", method = RequestMethod.GET) @ResponseBody public String save(@PathVariable("key") final String key, @PathVariable("value") final String value) { try{ stringRedisTemplate.opsForValue().set(key, value); }catch(Exception e){ e.printStackTrace(); } return "1. success"; }
@RequestMapping(value = "/checksingle/{id}", method = RequestMethod.GET) public void check(@PathVariable("id") final int id, HttpServletResponse response) { checkPerson(id, response); }
@RequestMapping(value = "/check", method = RequestMethod.GET) public void check(HttpServletResponse response) { boolean hasError = false; for(int i=0;i<TIMES;i++) { boolean rlt = checkPerson(checkPersionIdGenerator.incrementAndGet(), response); if(!rlt){ hasError = true; break; } }
if(!hasError){ Helper.success(response, "check success"); } }
@RequestMapping(value = "/add", method = RequestMethod.GET) public void add(HttpServletResponse response) { boolean isSuccess; for(int i=0;i<TIMES;i++) { Person person = Helper.buildPerson(addPersionIdGenerator);
while (true) { isSuccess = false; try { stringRedisTemplate.opsForValue().set(PREFIX + person.getId(), JSONObject.toJSONString(person)); isSuccess = true; } catch (Exception e) { logger.error("save redis error"); return; }
if (isSuccess) { break; } else { try { Thread.sleep(100); } catch (InterruptedException e) { logger.error("1. sleep error, ", e); } } } }
Helper.success(response, "save success"); }
@RequestMapping(value = "/reset", method = RequestMethod.GET) public void reset(HttpServletResponse response){ addPersionIdGenerator.set(0); checkPersionIdGenerator.set(0); Helper.success(response, "id generator reset success!"); }
/** * 检查指定id的数据是否正常 * @param id * @param response */ private boolean checkPerson(int id, HttpServletResponse response){ String raw = null;
boolean isSuccess;
while (true) { isSuccess = false; try { raw = stringRedisTemplate.opsForValue().get(PREFIX + id); isSuccess = true; } catch (Exception e) { logger.error("get from redis error"); }
if (isSuccess) { break; } else { try { Thread.sleep(100); } catch (InterruptedException e) { logger.error("1. sleep error, ", e); } } }
if(null==raw){ Helper.error( response, "[" + id + "] not exist!"); return false; }
Person person = JSONObject.parseObject(raw, Person.class);
String error = Helper.checkPerson(person);
if(null==error){ //Helper.success(response, "[" + id + "] check success!"); return true; }else { Helper.error(response, "[" + id + "] " + error); return false; } }}
复制代码


  • 关于该类有以下几处需要注意:


  1. 字符串转对象、对象转字符串的操作都是通过 Fastjson 实现的;

  2. add 方法是用于写性能测试的主要方法,每次请求该接口,都会连续执行 100 次对象到字符串的转换,然后写入 Redis;

  3. check 方法是用于读性能测试的主要方法,每次请求该接口,都会连续执行 100 次读取 Redis,然后将字符串转换成对象;

  4. add 和 check 方法中获取 Redis 连接时都有可能获取失败,所以如果发生异常就 sleep 后再重试;

  5. 成员变量 addPersionIdGenerator、checkPersionIdGenerator 都是用于 id 增长的 AtomicInteger 实例,这样性能测试时就不用输入 id 了,用这两个对象生成连续的 id;

  6. Helper.success 和 Helper.error 方法会设置 Response 的返回码,Apache bench 是根据 Response 的返回码是否位 200 来判定请求是成功还是失败;

开发基于 Kyro 序列化存取的 web 应用 redis-performance-demo-kryo


@Controllerpublic class RedisController {
private static final Logger logger = LoggerFactory.getLogger(RedisController.class);
private static AtomicInteger addPersionIdGenerator = new AtomicInteger(0);
private static AtomicInteger checkPersionIdGenerator = new AtomicInteger(0);
private static final String PREFIX = "person_";
private static final int TIMES = 100;
@Autowired private RedisClient redisClient;
/** * 检查指定id的记录 * @param id * @param response */ @RequestMapping(value = "/checksingle/{id}", method = RequestMethod.GET) public void check(@PathVariable("id") final int id, HttpServletResponse response) { checkPerson(id, response); }
/** * 将最后一次检查的id加一,然后根据最新id检查记录 * @param response */ @RequestMapping(value = "/check", method = RequestMethod.GET) public void check(HttpServletResponse response) { boolean hasError = false; for(int i=0;i<TIMES;i++) { boolean rlt = checkPerson(checkPersionIdGenerator.incrementAndGet(), response); if(!rlt){ hasError = true; break; } }
if(!hasError){ Helper.success(response, "check success"); } }
/** * 向redis增加一条记录 * @param response */ @RequestMapping(value = "/add", method = RequestMethod.GET) public void add(HttpServletResponse response) { boolean isSuccess;
for(int i=0;i<TIMES;i++) { Person person = Helper.buildPerson(addPersionIdGenerator);
isSuccess = false;
while (true){ try { redisClient.set(PREFIX + person.getId(), person); isSuccess = true; } catch (Exception e) { logger.error("save redis error, ", e); }
if(isSuccess){ break; }else{ try{ Thread.sleep(100); }catch(InterruptedException e){ logger.error("1. sleep error, ", e); } } } }
Helper.success(response, "save success"); }
/** * 将id清零 * @param response */ @RequestMapping(value = "/reset", method = RequestMethod.GET) public void reset(HttpServletResponse response){ addPersionIdGenerator.set(0); checkPersionIdGenerator.set(0); Helper.success(response, "id generator reset success!"); }
/** * 检查指定id的数据是否正常 * @param id * @param response */ private boolean checkPerson(int id, HttpServletResponse response){ Person person = null; boolean isSuccess;
while (true) { isSuccess = false; try { person = redisClient.getObject(PREFIX + id); isSuccess = true; } catch (Exception e) { logger.error("get from redis error"); }
if(isSuccess){ break; }else{ try{ Thread.sleep(100); }catch(InterruptedException e){ logger.error("2. sleep error, ", e); } } }
if(null==person){ Helper.error( response, "[" + id + "] not exist!"); return false; }
String error = Helper.checkPerson(person);
if(null==error){ //Helper.success(response, "[" + id + "] check success, object :\n" + JSONObject.toJSONString(person)); return true; }else { Helper.error(response, "[" + id + "] " + error); return false; } }}
复制代码


  • 以上代码,同样需要关注的是 add 和 check 方法,它们是性能测试时被调用的接口;

web 应用编译构建

  • 在应用 redis-performance-demo-string 的 pom.xml 所在目录执行命令 mvn clean package -U -DskipTests,编译构架成功后,在 target 目录下得到文件 redis-performance-demo-string-0.0.1-SNAPSHOT.jar

  • 在应用 redis-performance-demo-kryo 的 pom.xml 所在目录执行命令 mvn clean package -U -DskipTests,编译构架成功后,在 target 目录下得到文件 redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar

  • redis-performance-demo-string-0.0.1-SNAPSHOT.jar 和 redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar 这两个文件留在稍后部署 web 应用的时候使用;

在测试端机器上安装 Apache bench

  • 准备一台 Linux 机器作为执行性能测试的机器,在上面安装 Apache bench,对于 ubuntu 执行以下命令即可完成安装:


apt-get install -y apache2-utils
复制代码


  • 本次性能测试,我在一台树莓派 3B 上安装了 Apache bench,作为性能测试的执行机器,如果您手里有这类设备也可以尝试,先安装 64 位 Linux 操作系统,详情参照《树莓派 3B 安装 64 位操作系统(树莓派无需连接显示器键盘鼠标)》;

部署应用 redis-performance-demo-string

  • 将前面生成的 redis-performance-demo-string-0.0.1-SNAPSHOT.jar 文件复制到 web 应用服务器上,执行命令 java -jar >redis-performance-demo-string-0.0.1-SNAPSHOT.jar,即可启动应用;

redis-performance-demo-string 应用预热

  • 用 Apache bench 先 web server 发起请求,然后丢弃测试结果,因为这次请求中部分处理是在 JIT 之前完成的,不算数;

  • 在 Apache bench 所在机器上执行如下命令即可发起序列化和写入 Redis 的性能测试:


ab -n 5000 -c 200 http://192.168.31.104:8080/add
复制代码


  • 以上是序列化和写入 Redis 的测试,执行完毕后再执行下面的读 Redis 和反序列化的性能测试:


ab -n 5000 -c 200 http://192.168.31.104:8080/check
复制代码


192.168.31.104 是部署 redis-performance-demo-string 应用的应用服务器 IP 地址,8080 是应用启动后监听的端口;

正式压测 redis-performance-demo-string 并保存结果

  • 先清理预热时残留的数据,在 Redis 服务器上执行 redis-cli 进入命令行,然后执行 flushall 清除该 Redis 所有数据,注意:该命令会删除 Redis 上全部数据,请慎用!!!

  • 通过浏览器访问地址:http://192.168.31.104:8080/reset, 将生成 id 的全局变量重新设置为 0;

  • 测试序列化和写入,在 Apache bench 所在机器再次执行 ab -n 150000 -c 200 http://192.168.31.104:8080/add , 等测试结束后,记录测试结果中的三个关键信息如下:


  • 去 Redis 服务器执行命令 info,得到 Redis 内存使用大小为 3.30G(used_memory_human);

  • 去 Redis 服务器执行命令 dbsize,得到记录数为 15000000,符合预期;

  • 测试反序列化和读取,在 Apache bench 所在机器执行 ab -n 150000 -c 200 http://192.168.31.104:8080/check ,等测试结束后,记录测试结果中的三个关键信息如下:


部署应用 redis-performance-demo-kryo

  • 将前面生成的 redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar 文件复制到 web 应用服务器上,执行命令 java -jar >redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar,即可启动应用;

redis-performance-demo-kryo 应用预热

  • 用 Apache bench 先 web server 发起请求,然后丢弃测试结果,因为这次请求中部分处理是在 JIT 之前完成的,不算数;

  • 在 Apache bench 所在机器上执行如下命令即可发起序列化和写入 Redis 的性能测试:


ab -n 5000 -c 200 http://192.168.31.104:18080/add
复制代码


  • 以上是序列化和写入 Redis 的测试,执行完毕后再执行下面的读 Redis 和反序列化的性能测试:


ab -n 5000 -c 200 http://192.168.31.104:18080/check
复制代码


192.168.31.104 是部署 redis-performance-demo-kryo 应用的应用服务器 IP 地址,18080 是应用启动后监听的端口;

正式压测 redis-performance-demo-kryo 并保存结果

  • 先清理预热时残留的数据,在 Redis 服务器上执行 redis-cli 进入命令行,然后执行 flushall 清除该 Redis 所有数据,注意:该命令会删除 Redis 上全部数据,请慎用!!!

  • 通过浏览器访问地址:http://192.168.31.104:18080/reset , 将生成 id 的全局变量重新设置为 0;

  • 测试序列化和写入,在 Apache bench 所在机器再次执行 ab -n 150000 -c 200 http://192.168.31.104:18080/add , 等测试结束后,记录测试结果中的三个关键信息如下:


  • 去 Redis 服务器执行命令 info,得到 Redis 内存使用大小为 3.20G:

  • 去 Redis 服务器执行命令 dbsize,得到记录数为 15000000,符合预期;

  • 测试反序列化和读取,在 Apache bench 所在机器执行 ab -n 50000 -c 500 http://192.168.31.104:18080/check ,等测试结束后,记录测试结果中的三个关键信息如下:


  • 至此,性能测试已经完毕,我们把关键的 QPS 和内存大小拿来对比一下,如下表所示:


  • 从以上对比可以发现:


  1. 两种序列化方案的数据存入 Redis 后,kryo 占用内存小于 string,但是优势并不明显;

  2. 不论是读还是写,kryo 方案的吞吐率低于 sting 方案,这和之前预期的不同,但是网上已经有很多实践证明 kryo 方案的速度优于字符串方案,所以除了 kryo 本身的优势,对于 kryo 方案的集成以及 redis 连接管理等因素对吞吐率都有影响,SpringBoot 的 StringRedisTemplate 看来是个优秀的处理工具;


  • 测试的硬件环境与生产环境有着不小差别,所以数据仅供参考,也可能是我的测试代码质量堪忧所致(囧),如果您发现其中的问题,期待您的及时指正;

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

发布于: 2022 年 06 月 04 日阅读数: 43
用户头像

搜索"程序员欣宸",一起畅游Java宇宙 2018.04.19 加入

前腾讯、前阿里员工,从事Java后台工作,对Docker和Kubernetes充满热爱,所有文章均为作者原创,个人Github:https://github.com/zq2599/blog_demos

评论

发布
暂无评论
实战Redis序列化性能测试(Kryo和字符串)_Java_程序员欣宸_InfoQ写作社区