实战Redis序列化性能测试(Kryo和字符串)
Posted 程序员欣宸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战Redis序列化性能测试(Kryo和字符串)相关的知识,希望对你有一定的参考价值。
欢迎访问我的GitHub
本篇概览
-
在Java应用的开发中,有时候需要将Java对象实例保存在Redis中,常用方法有两种:
- 将对象序列化成字符串后存入Redis;
- 将对象序列化成byte数组后存入Redis;
- 以上两种方式孰优孰劣?字符串方式来存取的好处是编码和调试更简单容易,而byte数组的优势又在哪里呢,今天我们针对这两种存储方式做一次对比试验,用数据来得出结论;
测试方法简述
- 本次做的是对比测试,写Redis和读Redis都会测试到,测试一共有以下四种:
- 并发场景下对象通过fastjson转字符串,然后存入Redis;
- 并发场景下对象通过Kyro序列化成byte数组,然后存入Redis;
- 并发场景下从Redis取出字符串,通过fastjson转成对象;
- 并发场景下从Redis取出byte数组,然后通过Kyro反序列化成对象;
测试环境简介
-
本次测试需要以下三台电脑,全部是Linux:
- Redis服务器;
- Web应用服务器;
- 安装有Apache bench,用于发起性能测试,并统计出测试结果;
- 整体部署情况如下:
测试步骤梳理
- 在正式开始前,先将所有步骤整理好以免遗漏,接下来一步一步进行就可以了:
- 部署Redis;
- 开发基于字符串存取的web应用redis-performance-demo-string;
- 开发基于Kyro序列化存取的web应用redis-performance-demo-kryo;
- web应用编译构建;
- 在测试端机器上安装Apache bench;
- 部署应用redis-performance-demo-string;
- 用Apache bench先web server发起请求,然后丢弃测试结果,这次请求中部分处理是在JIT之前完成的,不算数;
- 清理Redis数据,用Apache bench先web server再次发起请求,保存测试结果;
- 清理Redis数据,部署应用redis-performance-demo-kryo;
- 用Apache bench先web server发起请求,然后丢弃测试结果,这次请求中部分处理是在JIT之前完成的,不算数;
- 清理Redis数据,用Apache bench先web server再次发起请求,保存测试结果;
- 对比结果,得出测试结论;
本章源码下载
- 本章实战的源码可以在github下载,地址和链接信息如下表所示:
名称 | 链接 | 备注 |
---|---|---|
项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https协议 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |
-
这个git项目中有多个文件夹,本章源码在以下两个文件夹中:
- redis-performance-demo-string:对应字符串存取对象的应用;
- redis-performance-demo-kryo:对应kryo序列化对象的应用;
- 如下图所示:
应用版本
- JDK:1.8.0_161;
- Maven:3.5.0;
- SpringBoot:1.4.1.RELEASE;
- Redis:3.2.12.;
- Fastjson:1.2.47;
- Kryo:4.0.0;
- Apache bench:2.3;
- Ubuntu:16.04.3 LTS;
- 接下来我们开始实战吧;
部署Redis
- Redis的安装和部署就不在本章展开了,以下两点请注意:
- 关闭redis远程保护:config set protected-mode "no";
- 修改conf文件,关闭持久化;
开发基于字符串存取的web应用redis-performance-demo-string
- 这是个基于SpringBoot的简单web应用,将几处重点列举出来:
-
首先是application.properties文件中有Redis配置信息,请将IP和端口替换为您的Redis服务器的IP和端口:
spring.redis.database=0 spring.redis.host=192.168.31.104 spring.redis.port=6379 spring.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:
@Controller public 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;
- 关于该类有以下几处需要注意:
- 字符串转对象、对象转字符串的操作都是通过Fastjson实现的;
- add方法是用于写性能测试的主要方法,每次请求该接口,都会连续执行100次对象到字符串的转换,然后写入Redis;
- check方法是用于读性能测试的主要方法,每次请求该接口,都会连续执行100次读取Redis,然后将字符串转换成对象;
- add和check方法中获取Redis连接时都有可能获取失败,所以如果发生异常就sleep后再重试;
- 成员变量addPersionIdGenerator、checkPersionIdGenerator都是用于id增长的AtomicInteger实例,这样性能测试时就不用输入id了,用这两个对象生成连续的id;
- Helper.success和Helper.error方法会设置Response的返回码,Apache bench是根据Response的返回码是否位200来判定请求是成功还是失败;
开发基于Kyro序列化存取的web应用redis-performance-demo-kryo
-
在SpringBoot框架使用Kyro作为Redis序列化工具的详细过程请参考《
SpringBoot下用Kyro作为Redis序列化工具》, 这里就不多说了,同样是类需要关注:@Controller public 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 , 等测试结束后,记录测试结果中的三个关键信息如下:
名称 | 数值 | 含义 |
---|---|---|
Requests per second | 399.18 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 501.033 | 用户平均请求等待时间,毫秒 |
Time per request<br>(mean, across all concurrent requests) | 2.505 | 服务器平均请求等待时间;<br>它是吞吐率的倒数;<br>它也等于"用户平均请求等待时间"除以"并发用户数" |
- 去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 ,等测试结束后,记录测试结果中的三个关键信息如下:
名称 | 数值 | 含义 |
---|---|---|
Requests per second | 425.22 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 470.341 | 用户平均请求等待时间,毫秒 |
Time per request<br>(mean, across all concurrent requests) | 2.352 | 服务器平均请求等待时间;<br>它是吞吐率的倒数;<br>它也等于"用户平均请求等待时间"除以"并发用户数" |
部署应用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 , 等测试结束后,记录测试结果中的三个关键信息如下:
名称 | 数值 | 含义 |
---|---|---|
Requests per second | 381.06 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 524.851 | 用户平均请求等待时间,毫秒 |
Time per request<br>(mean, across all concurrent requests) | 2.624 | 服务器平均请求等待时间;<br>它是吞吐率的倒数;<br>它也等于"用户平均请求等待时间"除以"并发用户数" |
- 去Redis服务器执行命令info,得到Redis内存使用大小为3.20G:
- 去Redis服务器执行命令dbsize,得到记录数为15000000,符合预期;
- 测试反序列化和读取,在Apache bench所在机器执行ab -n 50000 -c 500 http://192.168.31.104:18080/check ,等测试结束后,记录测试结果中的三个关键信息如下:
名称 | 数值 | 含义 |
---|---|---|
Requests per second | 386.49 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 517.484 | 用户平均请求等待时间,毫秒 |
Time per request<br>(mean, across all concurrent requests) | 2.587 | 服务器平均请求等待时间;<br>它是吞吐率的倒数;<br>它也等于"用户平均请求等待时间"除以"并发用户数" |
- 至此,性能测试已经完毕,我们把关键的QPS和内存大小拿来对比一下,如下表所示:
名称 | string | Kryo |
---|---|---|
每秒吞吐率(序列化和写入Reids) | 399.18 | 381.06 |
每秒吞吐率(反序列化和读取Reids) | 425.22 | 386.49 |
所占内存大小 | 3.30G | 3.20G |
-
从以上对比可以发现:
- 两种序列化方案的数据存入Redis后,kryo占用内存小于string,但是优势并不明显;
- 不论是读还是写,kryo方案的吞吐率低于sting方案,这和之前预期的不同,但是网上已经有很多实践证明kryo方案的速度优于字符串方案,所以除了kryo本身的优势,对于kryo方案的集成以及redis连接管理等因素对吞吐率都有影响,SpringBoot的StringRedisTemplate看来是个优秀的处理工具;
- 测试的硬件环境与生产环境有着不小差别,所以数据仅供参考,也可能是我的测试代码质量堪忧所致(囧),如果您发现其中的问题,期待您的及时指正;
欢迎关注51CTO博客:程序员欣宸
以上是关于实战Redis序列化性能测试(Kryo和字符串)的主要内容,如果未能解决你的问题,请参考以下文章