一、前言
非關系型資料庫(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,
基于此,本文設定了實驗環境都使用同一台機器進行 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單線程讀寫,使用二進制方式序列化,不啟用壓縮。
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 }
View Code
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 }
View Code
序列化代碼
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 * @return14 */
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 * @return35 */
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 }
View Code
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 }
3、測試結果,每項重複測試取平均值
五、多線程測試
1、除了多線程相關代碼外的公共代碼和單線程基本一緻,多線程測試主要增加了Client部分代碼對ConnectionPool、TimeOut相關設定,池政策、大小都會對性能産生很大影響,為了達到更高的性能,不同的使用場景下都需要有科學合理的測算。
2、主要測試代碼
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 }
View Code
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 //讀取代碼基本一緻
View Code
線程任務類
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 }
View Code
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 }
View Code
線程任務類
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 }
View Code
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 }
3、測試結果,每項重複測試取平均值
六、Memcached、Redis、Tair 都非常優秀
Redis在單線程環境下的性能表現非常突出,但在并行環境下則沒有很大的優勢,是JedisPool或者CommonPool的性能瓶頸還是我測試代碼的問題請麻煩告之,過程中修改setMaxTotal,setMaxIdle都沒有太大的改觀。