非關係型數據庫(NoSQL = Not Only SQL)的產品很是多,常見的有Memcached、Redis、MongoDB等優秀開源項目,相關概念和資料網上也很是豐富,再也不重複描述,本文主要引入Memcached和Redis與淘寶開源Tair分佈式存儲進行對比測試,因爲各自適用場景不一樣,且每一個產品的可配置參數繁多,涉及緩存策略、分佈算法、序列化方式、數據壓縮技術、通訊方式、併發、超時等諸多方面因素,都會對測試結果產生影響,單純的性能對比存在很是多的侷限性和不合理性,因此不能做爲任何評估依據,僅供參考,加深對各自產品的理解。如下是一些基本認識:html
一、儘管 Memcached 和 Redis 都標識爲Distribute,但從Server端自己而言它們並不提供分佈式的解決方案,須要Client端實現必定的分佈算法將數據存儲到各個節點,從而實現分佈式存儲,二者都提供了Replication功能(Master-Slave)保障可靠性。前端
二、Tair 則自己包含 Config Server 和 Data Server 採用一致性哈希算法分佈數據存儲,由ConfigSever來管理全部數據節點,理論上服務器端節點的維護對前端應用不會產生任何影響,同時數據能按指定複製到不一樣的DataServer保障可靠性,從Cluster角度來看屬於一個總體Solution,組件圖參照上一篇博文(http://www.cnblogs.com/lengfo/p/4171655.html)。java
基於此,本文設定了實驗環境都使用同一臺機器進行 Memcached、Redis 和 Tair 的單Server部署測試。redis
一、虛擬機環境(OS:CentOS6.5,CPU:2 Core,Memory:4G)算法
二、軟件環境數據庫
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 |
三、服務器配置,單一服務器經過配置儘量讓資源分配一致(因爲各個產品服務器端的配置相對複雜,再也不單獨列出,如下僅描述內存、鏈接等基本配置)緩存
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存儲引擎 |
一、從數據庫讀取一組數據緩存(SET)到每一個緩存服務器,其中對於每一個Server的寫入數據是徹底一致的,不設置過時時間,進行以下測試。安全
1)單線程進行1次寫入服務器
2)單線程進行500次寫入多線程
3)單線程進行2000次寫入
4)並行500個線程,每一個線程進行1次寫入
5)並行500個線程,每一個線程進行5次寫入
6)並行2000個線程,每一個線程進行1次寫入
二、分別從每一個緩存服務器讀取(GET)數據,其中對於每一個Server的讀取數據大小是徹底一致的,進行以下測試。
1)單線程進行1次讀取
2)單線程進行500次讀取
3)單線程進行2000次讀取
4)並行500個線程,每一個線程進行1次讀取
5)並行500個線程,每一個線程進行5次讀取
6)並行2000個線程,每一個線程進行1次讀取
一、緩存Model對象(OrderInfo)的定義參照tbOrder表(包括單據號、制單日期、商品、數量等字段)
二、單線程的讀寫操做對於代碼的要求相對較低,不須要考慮Pool,主要代碼以下:
1)Memcached單線程讀寫,使用二進制方式序列化,不啓用壓縮。
1 public static void putItems2Memcache(List<OrderInfo> orders) throws Exception { 2 MemcachedClient memcachedClient = null; 3 try { 4 MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("10.129.221.70:12000")); 5 builder.setCommandFactory(new BinaryCommandFactory()); 6 memcachedClient = builder.build(); 7 8 for (OrderInfo order : orders) { 9 boolean isSuccess = memcachedClient.set("order_" + order.BillNumber, 0, order); 10 if (!isSuccess) { 11 System.out.println("put: order_" + order.BillNumber + " " + isSuccess); 12 } 13 } 14 } catch (Exception ex) { 15 ex.printStackTrace(); 16 } finally { 17 memcachedClient.shutdown(); 18 } 19 } 20 21 public static void getItemsFromMemcache(List<String> billNumbers) throws Exception { 22 MemcachedClient memcachedClient = null; 23 try { 24 MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("10.129.221.70:12000")); 25 builder.setCommandFactory(new BinaryCommandFactory()); 26 memcachedClient = builder.build(); 27 28 for (String billnumber : billNumbers) { 29 OrderInfo result = memcachedClient.get(billnumber); 30 31 if (result == null) { 32 System.out.println(" get failed : " + billnumber + " not exist "); 33 } 34 } 35 } catch (Exception ex) { 36 ex.printStackTrace(); 37 } finally { 38 memcachedClient.shutdown(); 39 } 40 }
2)Redis單線程讀寫,因爲Jedis Client 不支持對象的序列化,須要自行實現對象序列化(本文使用二進制方式)。
1 public static void putItems2Redis(List<OrderInfo> orders) { 2 Jedis jedis = new Jedis("10.129.221.70", 6379); 3 4 try { 5 jedis.connect(); 6 7 for (OrderInfo order : orders) { 8 String StatusCode = jedis.set(("order_" + order.BillNumber).getBytes(), SerializeUtil.serialize(order)); 9 if (!StatusCode.equals("OK")) { 10 System.out.println("put: order_" + order.BillNumber + " " + StatusCode); 11 } 12 } 13 } catch (Exception ex) { 14 ex.printStackTrace(); 15 } finally { 16 jedis.close(); 17 } 18 } 19 20 public static void getItemsFromRedis(List<String> billNumbers) { 21 Jedis jedis = new Jedis("10.129.221.70", 6379); 22 23 try { 24 jedis.connect(); 25 26 for (String billnumber : billNumbers) { 27 byte[] result = jedis.get(billnumber.getBytes()); 28 if (result.length > 0) { 29 OrderInfo order = (OrderInfo) SerializeUtil.unserialize(result); 30 if (order == null) { 31 System.out.println(" unserialize failed : " + billnumber); 32 } 33 } else { 34 System.out.println(" get failed : " + billnumber + " not exist "); 35 } 36 } 37 } catch (Exception ex) { 38 ex.printStackTrace(); 39 } finally { 40 jedis.close(); 41 } 42 }
序列化代碼
1 package common; 2 3 import java.io.ByteArrayInputStream; 4 import java.io.ByteArrayOutputStream; 5 import java.io.ObjectInputStream; 6 import java.io.ObjectOutputStream; 7 8 public class SerializeUtil { 9 10 /** 11 * 序列化 12 * @param object 13 * @return 14 */ 15 public static byte[] serialize(Object object) { 16 ObjectOutputStream oos = null; 17 ByteArrayOutputStream baos = null; 18 19 try { 20 baos = new ByteArrayOutputStream(); 21 oos = new ObjectOutputStream(baos); 22 oos.writeObject(object); 23 byte[] bytes = baos.toByteArray(); 24 return bytes; 25 } catch (Exception e) { 26 e.printStackTrace(); 27 } 28 return null; 29 } 30 31 /** 32 * 反序列化 33 * @param bytes 34 * @return 35 */ 36 public static Object unserialize(byte[] bytes) { 37 ByteArrayInputStream bais = null; 38 try { 39 bais = new ByteArrayInputStream(bytes); 40 ObjectInputStream ois = new ObjectInputStream(bais); 41 return ois.readObject(); 42 } catch (Exception e) { 43 e.printStackTrace(); 44 } 45 46 return null; 47 } 48 }
3)Tair單線程讀寫,使用Java序列化,默認壓縮閥值爲8192字節,但本文測試的每一個寫入項都不會超過這個閥值,因此不受影響。
1 public static void putItems2Tair(List<OrderInfo> orders) { 2 try { 3 List<String> confServers = new ArrayList<String>(); 4 confServers.add("10.129.221.70:5198"); 5 //confServers.add("10.129.221.70:5200"); 6 7 DefaultTairManager tairManager = new DefaultTairManager(); 8 tairManager.setConfigServerList(confServers); 9 tairManager.setGroupName("group_1"); 10 tairManager.init(); 11 12 for (OrderInfo order : orders) { 13 ResultCode result = tairManager.put(0, "order_" + order.BillNumber, order); 14 if (!result.isSuccess()) { 15 System.out.println("put: order_" + order.BillNumber + " " + result.isSuccess() + " code:" + result.getCode()); 16 } 17 } 18 } catch (Exception ex) { 19 ex.printStackTrace(); 20 } 21 } 22 23 public static void getItemsFromTair(List<String> billNumbers) { 24 try { 25 List<String> confServers = new ArrayList<String>(); 26 confServers.add("10.129.221.70:5198"); 27 //confServers.add("10.129.221.70:5200"); 28 29 DefaultTairManager tairManager = new DefaultTairManager(); 30 tairManager.setConfigServerList(confServers); 31 tairManager.setGroupName("group_1"); 32 tairManager.init(); 33 34 for (String billnumber : billNumbers) { 35 Result<DataEntry> result = tairManager.get(0, billnumber); 36 if (result.isSuccess()) { 37 DataEntry entry = result.getValue(); 38 if (entry == null) { 39 System.out.println(" get failed : " + billnumber + " not exist "); 40 } 41 } else { 42 System.out.println(result.getRc().getMessage()); 43 } 44 } 45 } catch (Exception ex) { 46 ex.printStackTrace(); 47 } 48 }
三、測試結果,每項重複測試取平均值
一、除了多線程相關代碼外的公共代碼和單線程基本一致,多線程測試主要增長了Client部分代碼對ConnectionPool、TimeOut相關設置,池策略、大小都會對性能產生很大影響,爲了達到更高的性能,不一樣的使用場景下都須要有科學合理的測算。
二、主要測試代碼
1)每一個讀寫測試線程任務完成後統一調用公共Callback,在每批測試任務完成後記錄消耗時間
1 package common; 2 3 public class ThreadCallback { 4 5 public static int CompleteCounter = 0; 6 public static int failedCounter = 0; 7 8 public static synchronized void OnException() { 9 failedCounter++; 10 } 11 12 public static synchronized void OnComplete(String msg, int totalThreadCount, long startMili) { 13 CompleteCounter++; 14 if (CompleteCounter == totalThreadCount) { 15 long endMili = System.currentTimeMillis(); 16 System.out.println("(總共" + totalThreadCount + "個線程 ) " + msg + " ,總耗時爲:" + (endMili - startMili) + "毫秒 ,發生異常線程數:" + failedCounter); 17 CompleteCounter = 0; 18 failedCounter = 0; 19 } 20 } 21 }
2)Memcached多線程讀寫,使用XMemcached客戶端鏈接池,主要設置鏈接池大小ConnectionPoolSize=5,鏈接超時時間ConnectTimeout=2000ms,測試結果要求沒有超時異常線程。
測試方法
1 /*-------------------Memcached(多線程初始化)--------------------*/ 2 MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("192.168.31.191:12000")); 3 builder.setCommandFactory(new BinaryCommandFactory()); 4 builder.setConnectionPoolSize(5); 5 builder.setConnectTimeout(2000); 6 MemcachedClient memcachedClient = builder.build(); 7 memcachedClient.setOpTimeout(2000); 8 9 /*-------------------Memcached(多線程寫入)--------------------*/ 10 orders = OrderBusiness.loadOrders(5); 11 startMili = System.currentTimeMillis(); 12 totalThreadCount = 500; 13 for (int i = 1; i <= totalThreadCount; i++) { 14 MemcachePutter putter = new MemcachePutter(); 15 putter.OrderList = orders; 16 putter.Namesapce = i; 17 putter.startMili = startMili; 18 putter.TotalThreadCount = totalThreadCount; 19 putter.memcachedClient = memcachedClient; 20 21 Thread th = new Thread(putter); 22 th.start(); 23 } 24 25 //讀取代碼基本一致
線程任務類
1 public class MemcachePutter implements Runnable { 2 public List<OrderInfo> OrderList; 3 public int Namesapce; 4 public int TotalThreadCount; 5 public long startMili; 6 public MemcachedClient memcachedClient = null; // 線程安全的? 7 8 @Override 9 public void run() { 10 try { 11 for (OrderInfo order : OrderList) { 12 boolean isSuccess = memcachedClient.set("order_" + order.BillNumber, 0, order); 13 if (!isSuccess) { 14 System.out.println("put: order_" + order.BillNumber + " " + isSuccess); 15 } 16 } 17 } catch (Exception ex) { 18 ex.printStackTrace(); 19 ThreadCallback.OnException(); 20 } finally { 21 ThreadCallback.OnComplete("Memcached 每一個線程進行" + OrderList.size() + "次 [寫入] ", TotalThreadCount, startMili); 22 } 23 } 24 } 25 26 27 28 public class MemcacheGetter implements Runnable { 29 30 public List<String> billnumbers; 31 public long startMili; 32 public int TotalThreadCount; 33 public MemcachedClient memcachedClient = null; // 線程安全的? 34 35 @Override 36 public void run() { 37 try { 38 for (String billnumber : billnumbers) { 39 OrderInfo result = memcachedClient.get(billnumber); 40 if (result == null) { 41 System.out.println(" get failed : " + billnumber + " not exist "); 42 } 43 } 44 } catch (Exception ex) { 45 ex.printStackTrace(); 46 ThreadCallback.OnException(); 47 } finally { 48 ThreadCallback.OnComplete("Memcached 每一個線程進行" + billnumbers.size() + "次 [讀取] ", TotalThreadCount, startMili); 49 } 50 } 51 }
3)Redis多線程讀寫,使用Jedis客戶端鏈接池,從源碼能夠看出依賴與Apache.Common.Pool2,主要設置鏈接池MaxTotal=5,鏈接超時時間Timeout=2000ms,測試結果要求沒有超時異常線程。
測試方法
1 /*-------------------Redis(多線程初始化)--------------------*/ 2 GenericObjectPoolConfig config = new GenericObjectPoolConfig(); 3 config.setMaxTotal(5); 4 JedisPool jpool = new JedisPool(config, "192.168.31.191", 6379, 2000); 5 6 /*-------------------Redis(多線程寫入)--------------------*/ 7 totalThreadCount = 2000; 8 orders = OrderBusiness.loadOrders(1); 9 startMili = System.currentTimeMillis(); 10 for (int i = 1; i <= totalThreadCount; i++) { 11 RedisPutter putter = new RedisPutter(); 12 putter.OrderList = orders; 13 putter.Namesapce = i; 14 putter.startMili = startMili; 15 putter.TotalThreadCount = totalThreadCount; 16 putter.jpool = jpool; 17 18 Thread th = new Thread(putter); 19 th.start(); 20 }
線程任務類
1 public class RedisPutter implements Runnable { 2 3 public List<OrderInfo> OrderList; 4 public int Namesapce; 5 public int TotalThreadCount; 6 public long startMili; 7 public JedisPool jpool; 8 9 @Override 10 public void run() { 11 Jedis jedis = jpool.getResource(); 12 13 try { 14 jedis.connect(); 15 16 for (OrderInfo order : OrderList) { 17 String StatusCode = jedis.set(("order_" + order.BillNumber).getBytes(), SerializeUtil.serialize(order)); 18 if (!StatusCode.equals("OK")) { 19 System.out.println("put: order_" + order.BillNumber + " " + StatusCode); 20 } 21 } 22 } catch (Exception ex) { 23 // ex.printStackTrace(); 24 jpool.returnBrokenResource(jedis); 25 ThreadCallback.OnException(); 26 } finally { 27 jpool.returnResource(jedis); 28 ThreadCallback.OnComplete("Redis 每一個線程進行" + OrderList.size() + "次 [寫入] ", TotalThreadCount, startMili); 29 } 30 } 31 } 32 33 34 35 public class RedisGetter implements Runnable { 36 public List<String> billnumbers; 37 public long startMili; 38 public int TotalThreadCount; 39 public JedisPool jpool; 40 41 @Override 42 public void run() { 43 Jedis jedis = jpool.getResource(); 44 45 try { 46 jedis.connect(); 47 for (String billnumber : billnumbers) { 48 byte[] result = jedis.get(billnumber.getBytes()); 49 if (result.length > 0) { 50 OrderInfo order = (OrderInfo) SerializeUtil.unserialize(result); 51 if (order == null) { 52 System.out.println(" unserialize failed : " + billnumber); 53 } 54 } else { 55 System.out.println(" get failed : " + billnumber + " not exist "); 56 } 57 } 58 } catch (Exception ex) { 59 // ex.printStackTrace(); 60 jpool.returnBrokenResource(jedis); 61 ThreadCallback.OnException(); 62 } finally { 63 jpool.returnResource(jedis); 64 ThreadCallback.OnComplete("Redis 每一個線程進行" + billnumbers.size() + "次 [讀取] ", TotalThreadCount, startMili); 65 } 66 } 67 }
4)Tair多線程讀寫,使用官方Tair-Client,可設置參數MaxWaitThread主要指最大等待線程數,當超過這個數量的線程在等待時,新的請求將直接返回超時,本文測試設置MaxWaitThread=100,鏈接超時時間Timeout=2000ms,測試結果要求沒有超時異常線程。
測試方法
1 /*-------------------Tair(多線程初始化tairManager)--------------------*/ 2 List<String> confServers = new ArrayList<String>(); 3 confServers.add("192.168.31.191:5198"); 4 DefaultTairManager tairManager = new DefaultTairManager(); 5 tairManager.setConfigServerList(confServers); 6 tairManager.setGroupName("group_1"); 7 tairManager.setMaxWaitThread(100);// 最大等待線程數,當超過這個數量的線程在等待時,新的請求將直接返回超時 8 tairManager.setTimeout(2000);// 請求的超時時間,單位爲毫秒 9 tairManager.init(); 10 11 /*-------------------Tair(多線程寫入)--------------------*/ 12 orders = OrderBusiness.loadOrders(5); 13 startMili = System.currentTimeMillis(); 14 totalThreadCount = 500; 15 for (int i = 1; i <= totalThreadCount; i++) { 16 TairPutter putter = new TairPutter(); 17 putter.OrderList = orders; 18 putter.Namesapce = i; 19 putter.startMili = startMili; 20 putter.TotalThreadCount = totalThreadCount; 21 putter.tairManager = tairManager; 22 23 Thread th = new Thread(putter); 24 th.start(); 25 } 26 /*-------------------Tair(多線程讀取)--------------------*/ 27 //讀取代碼基本一致
線程任務類
1 public class TairGetter implements Runnable { 2 public List<String> billnumbers; 3 public long startMili; 4 public int TotalThreadCount; 5 public DefaultTairManager tairManager; 6 7 @Override 8 public void run() { 9 try { 10 for (String billnumber : billnumbers) { 11 Result<DataEntry> result = tairManager.get(0, billnumber); 12 if (result.isSuccess()) { 13 DataEntry entry = result.getValue(); 14 if (entry == null) { 15 System.out.println(" get failed : " + billnumber + " not exist "); 16 } 17 } else { 18 System.out.println(result.getRc().getMessage()); 19 } 20 } 21 } catch (Exception ex) { 22 // ex.printStackTrace(); 23 ThreadCallback.OnException(); 24 } finally { 25 ThreadCallback.OnComplete("Tair 每一個線程進行" + billnumbers.size() + "次 [讀取] ", TotalThreadCount, startMili); 26 } 27 } 28 } 29 30 31 32 public class TairPutter implements Runnable { 33 34 public List<OrderInfo> OrderList; 35 public int Namesapce; 36 public int TotalThreadCount; 37 public long startMili; 38 public DefaultTairManager tairManager; 39 40 @Override 41 public void run() { 42 try { 43 for (OrderInfo order : OrderList) { 44 ResultCode result = tairManager.put(0, "order_" + order.BillNumber, order); 45 if (!result.isSuccess()) { 46 System.out.println("put: order_" + order.BillNumber + " " + result.isSuccess() + " code:" + result.getCode()); 47 } 48 } 49 } catch (Exception ex) { 50 // ex.printStackTrace(); 51 ThreadCallback.OnException(); 52 } finally { 53 ThreadCallback.OnComplete("Tair 每一個線程進行" + OrderList.size() + "次 [寫入] ", TotalThreadCount, startMili); 54 } 55 } 56 }
三、測試結果,每項重複測試取平均值
Redis在單線程環境下的性能表現很是突出,但在並行環境下則沒有很大的優點,是JedisPool或者CommonPool的性能瓶頸仍是我測試代碼的問題請麻煩告之,過程當中修改setMaxTotal,setMaxIdle都沒有太大的改觀。
Tair因爲須要在服務器端實現數據分佈等相關算法,因此在測試對比中性能有所損耗應該也很好理解。
如以前所言,每一個技術自己的原理、策略、適用場景各不相同,儘管以上測試方法已經考慮了不少影響因素,但仍然可能存在不足之處,因此相似的對比缺少合理性,Tair還有2種存儲引擎沒有測試,並且以上都基於單機環境測試,在Cluster環境下可能也會有差異,因此結果僅供參考,不做任何評估依據。