Memcached、Redis OR Tair
一、前言
非关系型数据库(NoSQL = Not Only SQL)的产品非常多,常见的有Memcached、Redis、MongoDB等优秀开源项目,相关概念和资料网上也非常丰富,不再重复描述,本文主要引入Memcached和Redis与淘宝开源Tair分布式存储进行对比测试,由于各自适用场景不同,且每个产品的可配置参数繁多,涉及缓存策略、分布算法、序列化方式、数据压缩技术、通信方式、并发、超时等诸多方面因素,都会对测试结果产生影响,单纯的性能对比存在非常多的局限性和不合理性,所以不能作为任何评估依据,仅供参考,加深对各自产品的理解。以下是一些基本认识:
1、尽管 Memcached 和 Redis 都标识为Distribute,但从Server端本身而言它们并不提供分布式的解决方案,需要Client端实现一定的分布算法将数据存储到各个节点,从而实现分布式存储,两者都提供了Replication功能(Master-Slave)保障可靠性。
2、Tair 则本身包含 Config Server 和 Data Server 采用一致性哈希算法分布数据存储,由ConfigSever来管理所有数据节点,理论上服务器端节点的维护对前端应用不会产生任何影响,同时数据能按指定复制到不同的DataServer保障可靠性,从Cluster角度来看属于一个整体Solution,组件图参照上一篇博文(http://www.cnblogs.com/lengfo/p/4171655.html)。
基于此,本文设定了实验环境都使用同一台机器进行 Memcached、Redis 和 Tair 的单Server部署测试。
二、前置条件
1、虚拟机环境(OS:CentOS6.5,CPU:2 Core,Memory:4G)
2、软件环境
| Sever | Client | |
| Memcached | Memcached 1.4.21 | Xmemcached 2.0.0 |
| Redis | Redis 2.8.19 | Jedis 2.8.5 |
| Tair | Tair 2.3 | Tair Client 2.3.1 |
3、服务器配置,单一服务器通过配置尽可能让资源分配一致(由于各个产品服务器端的配置相对复杂,不再单独列出,以下仅描述内存、连接等基本配置)
| IP_Port | Memory_Size | Max_Connection | 备注 | |
| Memcached | 10.129.221.70:12000 | 1024MB | 2048 | |
| Redis | 10.129.221.70:6379 | 1gb(1000000000byte) | 10000(默认) | |
| Tair Config Server | 10.129.221.70:5198 | |||
| Tair Data Server | 10.129.221.70:5191 | 1024MB | 使用mdb存储引擎 |
三、用例场景,分别使用单线程和多线程进行测试
1、从数据库读取一组数据缓存(SET)到每个缓存服务器,其中对于每个Server的写入数据是完全一致的,不设置过期时间,进行如下测试。
1)单线程进行1次写入
2)单线程进行500次写入
3)单线程进行2000次写入
4)并行500个线程,每个线程进行1次写入
5)并行500个线程,每个线程进行5次写入
6)并行2000个线程,每个线程进行1次写入
2、分别从每个缓存服务器读取(GET)数据,其中对于每个Server的读取数据大小是完全一致的,进行如下测试。
1)单线程进行1次读取
2)单线程进行500次读取
3)单线程进行2000次读取
4)并行500个线程,每个线程进行1次读取
5)并行500个线程,每个线程进行5次读取
6)并行2000个线程,每个线程进行1次读取
四、单线程测试
1、缓存Model对象(OrderInfo)的定义参照tbOrder表(包括单据号、制单日期、商品、数量等字段)
2、单线程的读写操作对于代码的要求相对较低,不需要考虑Pool,主要代码如下:
1)Memcached单线程读写,使用二进制方式序列化,不启用压缩。
public static void putItems2Memcache(List<OrderInfo> orders) throws Exception {
MemcachedClient memcachedClient = null;
try {
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("10.129.221.70:12000"));
builder.setCommandFactory(new BinaryCommandFactory());
memcachedClient = builder.build();
for (OrderInfo order : orders) {
boolean isSuccess = memcachedClient.set("order_" + order.BillNumber, 0, order);
if (!isSuccess) {
System.out.println("put: order_" + order.BillNumber + " " + isSuccess);
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
memcachedClient.shutdown();
}
}
public static void getItemsFromMemcache(List<String> billNumbers) throws Exception {
MemcachedClient memcachedClient = null;
try {
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("10.129.221.70:12000"));
builder.setCommandFactory(new BinaryCommandFactory());
memcachedClient = builder.build();
for (String billnumber : billNumbers) {
OrderInfo result = memcachedClient.get(billnumber);
if (result == null) {
System.out.println(" get failed : " + billnumber + " not exist ");
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
memcachedClient.shutdown();
}
}
2)Redis单线程读写,由于Jedis Client 不支持对象的序列化,需要自行实现对象序列化(本文使用二进制方式)。
public static void putItems2Redis(List<OrderInfo> orders) {
Jedis jedis = new Jedis("10.129.221.70", 6379);
try {
jedis.connect();
for (OrderInfo order : orders) {
String StatusCode = jedis.set(("order_" + order.BillNumber).getBytes(), SerializeUtil.serialize(order));
if (!StatusCode.equals("OK")) {
System.out.println("put: order_" + order.BillNumber + " " + StatusCode);
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
jedis.close();
}
}
public static void getItemsFromRedis(List<String> billNumbers) {
Jedis jedis = new Jedis("10.129.221.70", 6379);
try {
jedis.connect();
for (String billnumber : billNumbers) {
byte[] result = jedis.get(billnumber.getBytes());
if (result.length > 0) {
OrderInfo order = (OrderInfo) SerializeUtil.unserialize(result);
if (order == null) {
System.out.println(" unserialize failed : " + billnumber);
}
} else {
System.out.println(" get failed : " + billnumber + " not exist ");
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
jedis.close();
}
}
序列化代码
package common; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; public class SerializeUtil { /**
* 序列化
* @param object
* @return
*/
public static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null; try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
} /**
* 反序列化
* @param bytes
* @return
*/
public static Object unserialize(byte[] bytes) {
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
} return null;
}
}
3)Tair单线程读写,使用Java序列化,默认压缩阀值为8192字节,但本文测试的每个写入项都不会超过这个阀值,所以不受影响。
public static void putItems2Tair(List<OrderInfo> orders) {
try {
List<String> confServers = new ArrayList<String>();
confServers.add("10.129.221.70:5198");
//confServers.add("10.129.221.70:5200");
DefaultTairManager tairManager = new DefaultTairManager();
tairManager.setConfigServerList(confServers);
tairManager.setGroupName("group_1");
tairManager.init();
for (OrderInfo order : orders) {
ResultCode result = tairManager.put(0, "order_" + order.BillNumber, order);
if (!result.isSuccess()) {
System.out.println("put: order_" + order.BillNumber + " " + result.isSuccess() + " code:" + result.getCode());
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void getItemsFromTair(List<String> billNumbers) {
try {
List<String> confServers = new ArrayList<String>();
confServers.add("10.129.221.70:5198");
//confServers.add("10.129.221.70:5200");
DefaultTairManager tairManager = new DefaultTairManager();
tairManager.setConfigServerList(confServers);
tairManager.setGroupName("group_1");
tairManager.init();
for (String billnumber : billNumbers) {
Result<DataEntry> result = tairManager.get(0, billnumber);
if (result.isSuccess()) {
DataEntry entry = result.getValue();
if (entry == null) {
System.out.println(" get failed : " + billnumber + " not exist ");
}
} else {
System.out.println(result.getRc().getMessage());
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
3、测试结果,每项重复测试取平均值


五、多线程测试
1、除了多线程相关代码外的公共代码和单线程基本一致,多线程测试主要增加了Client部分代码对ConnectionPool、TimeOut相关设置,池策略、大小都会对性能产生很大影响,为了达到更高的性能,不同的使用场景下都需要有科学合理的测算。
2、主要测试代码
1)每个读写测试线程任务完成后统一调用公共Callback,在每批测试任务完成后记录消耗时间
package common;
public class ThreadCallback {
public static int CompleteCounter = 0;
public static int failedCounter = 0;
public static synchronized void OnException() {
failedCounter++;
}
public static synchronized void OnComplete(String msg, int totalThreadCount, long startMili) {
CompleteCounter++;
if (CompleteCounter == totalThreadCount) {
long endMili = System.currentTimeMillis();
System.out.println("(总共" + totalThreadCount + "个线程 ) " + msg + " ,总耗时为:" + (endMili - startMili) + "毫秒 ,发生异常线程数:" + failedCounter);
CompleteCounter = 0;
failedCounter = 0;
}
}
}
2)Memcached多线程读写,使用XMemcached客户端连接池,主要设置连接池大小ConnectionPoolSize=5,连接超时时间ConnectTimeout=2000ms,测试结果要求没有超时异常线程。
测试方法
/*-------------------Memcached(多线程初始化)--------------------*/
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("192.168.31.191:12000"));
builder.setCommandFactory(new BinaryCommandFactory());
builder.setConnectionPoolSize(5);
builder.setConnectTimeout(2000);
MemcachedClient memcachedClient = builder.build();
memcachedClient.setOpTimeout(2000); /*-------------------Memcached(多线程写入)--------------------*/
orders = OrderBusiness.loadOrders(5);
startMili = System.currentTimeMillis();
totalThreadCount = 500;
for (int i = 1; i <= totalThreadCount; i++) {
MemcachePutter putter = new MemcachePutter();
putter.OrderList = orders;
putter.Namesapce = i;
putter.startMili = startMili;
putter.TotalThreadCount = totalThreadCount;
putter.memcachedClient = memcachedClient; Thread th = new Thread(putter);
th.start();
} //读取代码基本一致
线程任务类
public class MemcachePutter implements Runnable {
public List<OrderInfo> OrderList;
public int Namesapce;
public int TotalThreadCount;
public long startMili;
public MemcachedClient memcachedClient = null; // 线程安全的?
@Override
public void run() {
try {
for (OrderInfo order : OrderList) {
boolean isSuccess = memcachedClient.set("order_" + order.BillNumber, 0, order);
if (!isSuccess) {
System.out.println("put: order_" + order.BillNumber + " " + isSuccess);
}
}
} catch (Exception ex) {
ex.printStackTrace();
ThreadCallback.OnException();
} finally {
ThreadCallback.OnComplete("Memcached 每个线程进行" + OrderList.size() + "次 [写入] ", TotalThreadCount, startMili);
}
}
}
public class MemcacheGetter implements Runnable {
public List<String> billnumbers;
public long startMili;
public int TotalThreadCount;
public MemcachedClient memcachedClient = null; // 线程安全的?
@Override
public void run() {
try {
for (String billnumber : billnumbers) {
OrderInfo result = memcachedClient.get(billnumber);
if (result == null) {
System.out.println(" get failed : " + billnumber + " not exist ");
}
}
} catch (Exception ex) {
ex.printStackTrace();
ThreadCallback.OnException();
} finally {
ThreadCallback.OnComplete("Memcached 每个线程进行" + billnumbers.size() + "次 [读取] ", TotalThreadCount, startMili);
}
}
}
3)Redis多线程读写,使用Jedis客户端连接池,从源码可以看出依赖与Apache.Common.Pool2,主要设置连接池MaxTotal=5,连接超时时间Timeout=2000ms,测试结果要求没有超时异常线程。
测试方法
/*-------------------Redis(多线程初始化)--------------------*/
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(5);
JedisPool jpool = new JedisPool(config, "192.168.31.191", 6379, 2000); /*-------------------Redis(多线程写入)--------------------*/
totalThreadCount = 2000;
orders = OrderBusiness.loadOrders(1);
startMili = System.currentTimeMillis();
for (int i = 1; i <= totalThreadCount; i++) {
RedisPutter putter = new RedisPutter();
putter.OrderList = orders;
putter.Namesapce = i;
putter.startMili = startMili;
putter.TotalThreadCount = totalThreadCount;
putter.jpool = jpool; Thread th = new Thread(putter);
th.start();
}
线程任务类
public class RedisPutter implements Runnable {
public List<OrderInfo> OrderList;
public int Namesapce;
public int TotalThreadCount;
public long startMili;
public JedisPool jpool;
@Override
public void run() {
Jedis jedis = jpool.getResource();
try {
jedis.connect();
for (OrderInfo order : OrderList) {
String StatusCode = jedis.set(("order_" + order.BillNumber).getBytes(), SerializeUtil.serialize(order));
if (!StatusCode.equals("OK")) {
System.out.println("put: order_" + order.BillNumber + " " + StatusCode);
}
}
} catch (Exception ex) {
// ex.printStackTrace();
jpool.returnBrokenResource(jedis);
ThreadCallback.OnException();
} finally {
jpool.returnResource(jedis);
ThreadCallback.OnComplete("Redis 每个线程进行" + OrderList.size() + "次 [写入] ", TotalThreadCount, startMili);
}
}
}
public class RedisGetter implements Runnable {
public List<String> billnumbers;
public long startMili;
public int TotalThreadCount;
public JedisPool jpool;
@Override
public void run() {
Jedis jedis = jpool.getResource();
try {
jedis.connect();
for (String billnumber : billnumbers) {
byte[] result = jedis.get(billnumber.getBytes());
if (result.length > 0) {
OrderInfo order = (OrderInfo) SerializeUtil.unserialize(result);
if (order == null) {
System.out.println(" unserialize failed : " + billnumber);
}
} else {
System.out.println(" get failed : " + billnumber + " not exist ");
}
}
} catch (Exception ex) {
// ex.printStackTrace();
jpool.returnBrokenResource(jedis);
ThreadCallback.OnException();
} finally {
jpool.returnResource(jedis);
ThreadCallback.OnComplete("Redis 每个线程进行" + billnumbers.size() + "次 [读取] ", TotalThreadCount, startMili);
}
}
}
4)Tair多线程读写,使用官方Tair-Client,可设置参数MaxWaitThread主要指最大等待线程数,当超过这个数量的线程在等待时,新的请求将直接返回超时,本文测试设置MaxWaitThread=100,连接超时时间Timeout=2000ms,测试结果要求没有超时异常线程。
测试方法
/*-------------------Tair(多线程初始化tairManager)--------------------*/
List<String> confServers = new ArrayList<String>();
confServers.add("192.168.31.191:5198");
DefaultTairManager tairManager = new DefaultTairManager();
tairManager.setConfigServerList(confServers);
tairManager.setGroupName("group_1");
tairManager.setMaxWaitThread(100);// 最大等待线程数,当超过这个数量的线程在等待时,新的请求将直接返回超时
tairManager.setTimeout(2000);// 请求的超时时间,单位为毫秒
tairManager.init(); /*-------------------Tair(多线程写入)--------------------*/
orders = OrderBusiness.loadOrders(5);
startMili = System.currentTimeMillis();
totalThreadCount = 500;
for (int i = 1; i <= totalThreadCount; i++) {
TairPutter putter = new TairPutter();
putter.OrderList = orders;
putter.Namesapce = i;
putter.startMili = startMili;
putter.TotalThreadCount = totalThreadCount;
putter.tairManager = tairManager; Thread th = new Thread(putter);
th.start();
}
/*-------------------Tair(多线程读取)--------------------*/
//读取代码基本一致
线程任务类
public class TairGetter implements Runnable {
public List<String> billnumbers;
public long startMili;
public int TotalThreadCount;
public DefaultTairManager tairManager;
@Override
public void run() {
try {
for (String billnumber : billnumbers) {
Result<DataEntry> result = tairManager.get(0, billnumber);
if (result.isSuccess()) {
DataEntry entry = result.getValue();
if (entry == null) {
System.out.println(" get failed : " + billnumber + " not exist ");
}
} else {
System.out.println(result.getRc().getMessage());
}
}
} catch (Exception ex) {
// ex.printStackTrace();
ThreadCallback.OnException();
} finally {
ThreadCallback.OnComplete("Tair 每个线程进行" + billnumbers.size() + "次 [读取] ", TotalThreadCount, startMili);
}
}
}
public class TairPutter implements Runnable {
public List<OrderInfo> OrderList;
public int Namesapce;
public int TotalThreadCount;
public long startMili;
public DefaultTairManager tairManager;
@Override
public void run() {
try {
for (OrderInfo order : OrderList) {
ResultCode result = tairManager.put(0, "order_" + order.BillNumber, order);
if (!result.isSuccess()) {
System.out.println("put: order_" + order.BillNumber + " " + result.isSuccess() + " code:" + result.getCode());
}
}
} catch (Exception ex) {
// ex.printStackTrace();
ThreadCallback.OnException();
} finally {
ThreadCallback.OnComplete("Tair 每个线程进行" + OrderList.size() + "次 [写入] ", TotalThreadCount, startMili);
}
}
}
3、测试结果,每项重复测试取平均值


六、Memcached、Redis、Tair 都非常优秀
Redis在单线程环境下的性能表现非常突出,但在并行环境下则没有很大的优势,是JedisPool或者CommonPool的性能瓶颈还是我测试代码的问题请麻烦告之,过程中修改setMaxTotal,setMaxIdle都没有太大的改观。
Tair由于需要在服务器端实现数据分布等相关算法,所以在测试对比中性能有所损耗应该也很好理解。
如之前所言,每个技术本身的原理、策略、适用场景各不相同,尽管以上测试方法已经考虑了很多影响因素,但仍然可能存在不足之处,所以类似的对比缺乏合理性,Tair还有2种存储引擎没有测试,而且以上都基于单机环境测试,在Cluster环境下可能也会有差别,所以结果仅供参考,不作任何评估依据。
七、向开源工作者和组织致敬,@Memcached @Redis @Tair @Jedis @Xmemcached,感谢对开源事业作出的任何贡献
八、祝2015新年快乐,Happy New Year
Memcached、Redis OR Tair的更多相关文章
- linux下php7安装memcached、redis扩展
linux下php7安装memcached.redis扩展 1.php7安装Memcached扩展 比如说我现在使用了最新的 Ubuntu 16.04,虽然内置了 PHP 7 源,但 memcache ...
- dockerfile创建php容器(安装memcached、redis、gd、xdebug扩展)
dockerfile创建php容器(含有memcached.redis.gd.xdebug扩展) 代码如下: FROM php:7.2-fpm COPY redis-3.1.6.tgz /home/r ...
- Java缓存相关memcached、redis、guava、Spring Cache的使用
随笔分类 - Java缓存相关 主要记录memcached.redis.guava.Spring Cache的使用 第十二章 redis-cluster搭建(redis-3.2.5) 摘要: redi ...
- 缓存、队列(Memcached、redis、RabbitMQ)
本章内容: Memcached 简介.安装.使用 Python 操作 Memcached 天生支持集群 redis 简介.安装.使用.实例 Python 操作 Redis String.Hash.Li ...
- 缓存大全(Memcached、redis、RabbitMQ )
Memcached: 简介.安装.使用 python操作Memcached Memcached天生支持集群 Redis: 简介.安装.使用.实例 Python操作Redis String.Hash.L ...
- 各种缓存(Memcached、Redis、RabbitMQ、SQLlchemy)
Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度 ...
- Python自动化开发 - Python操作Memcached、Redis、RabbitMQ
Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载. 它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速 ...
- Memcached、Redis、RabbitMQ
目录 一.Memcached 二.Redis 三.RabbitMQ Memcached Memcache 是一个开源.高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中 ...
- 模块-Memcached、Redis
目录 Mecache 安装 使用 Redis 安装 Python操作Redis 操作模式 连接池 操作 String Hash List Set sort set 其他常用操作 管道 发布订阅 sen ...
随机推荐
- php中global与$GLOBALS的用法及区别-转载
php中global 与 $GLOBALS[""] 差别 原本觉得global和$GLOBALS除了写法不一样觉得,其他都一样,可是在实际利用中发现2者的差别还是很大的! 先看下面 ...
- linux 系统下查看raid信息,以及磁盘信息
有时想知道服务器上有几块磁盘,如果没有做raid,则可以简单使用fdisk -l 就可以看到. 但是做了raid呢,这样就看不出来了.那么如何查看服务器上做了raid? 软件raid:只能通过Lin ...
- Java for LeetCode 028 Implement strStr()
Implement strStr(). Returns the index of the first occurrence of needle in haystack, or -1 if needle ...
- docke跨主机通信之gre隧道
GRE简介 GRE可以对网络层的任何协议来进行封装,类似LVS的IPIP协议,在原有的数据报上增加GRE协议数据报.然后在网络上传输,到达对端后,解开GRE数据报头,得到真实的数据报.其中的mac地址 ...
- 埃及分数(codevs 1288)
题目描述 Description 在古埃及,人们使用单位分数的和(形如1/a的, a是自然数)表示一切有理数. 如:2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为加数中有相同的. 对于一 ...
- 组合数(codevs 1631)
1631 组合数 时间限制: 1 s 空间限制: 256000 KB 题目等级 : 钻石 Diamond 题解 查看运行结果 题目描述 Description 组合数C(N, K)表示 ...
- MySQL支持的列类型
MySQL支持大量的列类型,它可以被分为3类:数字类型.日期和时间类型以及字符串(字符)类型.本节首先给出可用类型的一个概述,并且总结每个列类型的存储需求,然后提供每个类中的类型性质的更详细的描述. ...
- vijos 1038 括号+路径 ***
链接:点我 就是自己写不出来 #include <cstdio> #include <climits> #include <memory.h> using name ...
- loj 1251(2-sat + 输出一组可行解)
题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=26961 思路:u表示留下,~u表示离开,同理v,对于+u,-v,我 ...
- Python实现的粒子群优化算法
01.from numpy import array 02.from random import random 03.from math import sin, sqrt 04. 05.iter_ma ...