
在Java应用的开发中,有时候需要将Java对象实例保存在Redis中,常用方法有两种:
以上两种方式孰优孰劣?字符串方式来存取的好处是编码和调试更简单容易,而byte数组的优势又在哪里呢,今天我们针对这两种存储方式做一次对比试验,用数据来得出结论;
原文地址:https://blog.csdn.net/boling_cavalry/article/details/80719683
本次做的是对比测试,写Redis和读Redis都会测试到,测试一共有以下四种:
本次测试需要以下三台电脑,全部是Linux:
整体部署情况如下:

在正式开始前,先将所有步骤整理好以免遗漏,接下来一步一步进行就可以了:
本章实战的源码可以在github下载,地址和链接信息如下表所示:
名称 | 链接 | 备注 |
|---|---|---|
项目主页 | 该项目在GitHub上的主页 | |
git仓库地址(https) | 该项目源码的仓库地址,https协议 | |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |
这个git项目中有多个文件夹,本章源码在以下两个文件夹中:
如下图所示:

接下来我们开始实战吧;
Redis的安装和部署就不在本章展开了,以下两点请注意:
这是个基于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;
}
}
}关于该类有以下几处需要注意:
在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方法,它们是性能测试时被调用的接口;
redis-performance-demo-string-0.0.1-SNAPSHOT.jar和redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar这两个文件留在稍后部署web应用的时候使用;
准备一台Linux机器作为执行性能测试的机器,在上面安装Apache bench,对于ubuntu执行以下命令即可完成安装:
apt-get install -y apache2-utils本次性能测试,我在一台树莓派3B上安装了Apache bench,作为性能测试的执行机器,如果您手里有这类设备也可以尝试,先安装64位Linux操作系统,详情参照《树莓派3B安装64位操作系统(树莓派无需连接显示器键盘鼠标)》;
将前面生成的redis-performance-demo-string-0.0.1-SNAPSHOT.jar文件复制到web应用服务器上,执行命令java -jar >redis-performance-demo-string-0.0.1-SNAPSHOT.jar,即可启动应用;
用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/check192.168.31.104是部署redis-performance-demo-string应用的应用服务器IP地址,8080是应用启动后监听的端口;
名称 | 数值 | 含义 |
|---|---|---|
Requests per second | 399.18 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 501.033 | 用户平均请求等待时间,毫秒 |
Time per request(mean, across all concurrent requests) | 2.505 | 服务器平均请求等待时间;它是吞吐率的倒数;它也等于"用户平均请求等待时间"除以"并发用户数" |
名称 | 数值 | 含义 |
|---|---|---|
Requests per second | 425.22 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 470.341 | 用户平均请求等待时间,毫秒 |
Time per request(mean, across all concurrent requests) | 2.352 | 服务器平均请求等待时间;它是吞吐率的倒数;它也等于"用户平均请求等待时间"除以"并发用户数" |
将前面生成的redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar文件复制到web应用服务器上,执行命令java -jar >redis-performance-demo-kryo-0.0.1-SNAPSHOT.jar,即可启动应用;
用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/check192.168.31.104是部署redis-performance-demo-kryo应用的应用服务器IP地址,18080是应用启动后监听的端口;
名称 | 数值 | 含义 |
|---|---|---|
Requests per second | 381.06 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 524.851 | 用户平均请求等待时间,毫秒 |
Time per request(mean, across all concurrent requests) | 2.624 | 服务器平均请求等待时间;它是吞吐率的倒数;它也等于"用户平均请求等待时间"除以"并发用户数" |
名称 | 数值 | 含义 |
|---|---|---|
Requests per second | 386.49 | 每秒吞吐率,单位时间内能处理的最大请求数 |
Time per request | 517.484 | 用户平均请求等待时间,毫秒 |
Time per request(mean, across all concurrent requests) | 2.587 | 服务器平均请求等待时间;它是吞吐率的倒数;它也等于"用户平均请求等待时间"除以"并发用户数" |
至此,性能测试已经完毕,我们把关键的QPS和内存大小拿来对比一下,如下表所示:
名称 | string | Kryo |
|---|---|---|
每秒吞吐率(序列化和写入Reids) | 399.18 | 381.06 |
每秒吞吐率(反序列化和读取Reids) | 425.22 | 386.49 |
所占内存大小 | 3.30G | 3.20G |
从以上对比可以发现:
测试的硬件环境与生产环境有着不小差别,所以数据仅供参考,也可能是我的测试代码质量堪忧所致(囧),如果您发现其中的问题,期待您的及时指正;