第 4-1 課:Spring Boot 操作 Memcache

在常見的企業架構中,隨着公司業務高速發展,最先出現瓶頸的是數據庫,這個時候很多企業就會考慮使用緩存來緩解數據庫的壓力,這是緩存使用最多的場景之一;另外在高併發搶購、分佈式 Session 等場景下,也會使用緩存來提高系統的高可用性。常用的緩存中間件有 Memcache 和 Redis,今天我們先來學習 Memcache 的使用。

Memcache 介紹

Memcache 是一個自由和開放源代碼、高性能、分配的內存對象緩存系統。簡單來說,Memcache 是一個高性能的分佈式內存對象的 key-value 緩存系統,用於加速動態 Web 應用程序,減輕數據庫負載,現在也有很多人將它作爲內存式數據庫在使用。

它可以應對任意多個連接,使用非阻塞的網絡 IO,由於它的工作機制是在內存中開闢一塊空間,然後建立一個 Hash 表,Memcached 自動管理這些 Hash 表。

Memcache 由國外社區網站 LiveJournal 開發團隊開發,設計理念就是小而強大,它簡單的設計促進了快速部署、易於開發並解決面對大規模的數據緩存的許多難題,而所開放的 API 使得 Memcache 能用於 Java、C/C++/C#、Perl、Python、PHP、Ruby 等大部分流行的程序語言。

Memcache 和 Memcached 的區別:Memcache 是這個項目的名稱,而 Memcached 是服務器端的主程序名稱。

Memcache 特點

協議簡單

Memcache 的服務端客戶端通信使用簡單的文本協議,通過 Telnet 即可在 Memcached 上存取數據。

基於 Libevent 的事件處理

Libevent 是一套跨平臺的事件處理接口的封裝,能夠兼容包括這些操作系統:Windows/Linux/BSD/Solaris 等操作系統的的事件處理,包裝的接口包括:poll、select(Windows)、epoll(Linux)、kqueue(BSD)/dev/pool(Solaris)。

Memcache 使用 Libevent 來進行網絡併發連接的處理,能夠保持在很大併發情況下,仍舊能夠保持快速的響應能力。

內置內存存儲方式

Memcache 中保存的數據都存儲在 Memcache 內置的內存存儲空間中。由於數據僅存在於內存中,因此重啓 Memcache、重啓操作系統會導致數據全部丟失。Memcache LRU(Least Recently Used)算法自動刪除不使用的緩存,不過這個功能是可以配置的,Memcache 啓動時通過“-M”參數可以禁止 LRU。不過,Memcache 本身是爲緩存而設計的,建議開啓 LRU。

不適應場景

  • 緩存對象不能大於 1 MB
  • key 的長度大於 250 字符
  • Memcache 未提供任何安全策略
  • 不支持持久化

Memcache 安裝

在 Centos 下安裝使用 yum 命令安裝 Memcache 非常簡單:

yum install -y memcached

啓動:

/usr/bin/memcached -b  -p 11211 -m 150 -u root >> /tmp/memcached.log  &

啓動參數可以配置,常用的命令選項如下:

  • m 內存
  • c 最大鏈接數
  • p 端口
  • u 用戶
  • t 線程數

查看 memcached 是否在運行:

ps -ef | grep memcached

Memcache 客戶端

Memcached Client 目前有 3 種:

  • Memcached Client for Java(已經停止更新)
  • SpyMemcached(已經停止更新)
  • XMemcached(主流使用)

Memcached Client for Java 比 SpyMemcached 更穩定、更早、更廣泛;SpyMemcached 比 Memcached Client for Java 更高效;XMemcached 比 SpyMemcache 併發效果更好。

曾經有一段時間 SpyMemcached 使用比較廣泛,我簡單介紹一下。

Spymemcached 介紹

Spymemcached 是一個採用 Java 開發的異步、單線程的 Memcached 客戶端,使用 NIO 實現。Spymemcached 是 Memcached 的一個流行的 Java Client 庫,性能表現出色,廣泛應用於 Java + Memcached 項目中。

Spymemcached 最早由 Dustin Sallings 開發,Dustin 後來和別人一起創辦了 Couchbase(原NorthScale),職位爲首席架構師,2014 年加入 Google。

XMemcached 簡介

現在使用最廣泛的 Memcache Java 客戶端是 XMemcached,它是一個新的 Java Memcache Client 。Memcached 通過它的自定義協議與客戶端交互,而 XMemcached 就是它的一個 Java 客戶端實現。相比其他客戶端,XMemcached 有什麼優點呢?

XMemcached 的主要特性

XMemcached 支持設置連接池、宕機報警、使用二進制文件、一致性哈希算法、進行數據壓縮等操作,總結如下:

  • 高性能,由 Nio 支持;
  • 協議完整,Xmemcached 支持所有的 Memcached 協議,包括 1.4.0 正式開始使用的二進制協議;
  • 支持客戶端分佈,提供了一致性哈希(Consistent Hash)算法的實現;
  • 允許設置節點權重,XMemcached 允許通過設置節點的權重來調節 Memcached 的負載,設置的權重越高,該 Memcached 節點存儲的數據將越多,所承受的負載越大;
  • 動態增刪節點,Memcached 允許通過 JMX 或者代碼編程實現節點的動態添加或者移除,方便用戶擴展和替換節點等;
  • XMemcached 通過 JMX 暴露的一些接口,支持 Client 本身的監控和調整,允許動態設置調優參數、查看統計數據、動態增刪節點等;
  • 支持客戶端連接池,對同一個 Memcached 可以創建 N 個連接組成連接池來提高客戶端在高併發環境下的表現,而這一切對使用者來說卻是透明的;
  • 可擴展性,XMemcached 是基於 Java Nio 框架 Yanf4j 實現的,因此在實現上結構相對清楚,分層比較明晰。

快速上手

上面介紹了這麼多,最需要關注的是 XMemcached 是最佳的選擇,下面我們先用一個示例,來感受一下 Spring Boot 使用 Xmemcached 集成 Memcache。

添加配置

添加依賴包:

<dependency>
    <groupId>com.googlecode.xmemcached</groupId>
    <artifactId>xmemcached</artifactId>
    <version>2.4.5</version>
</dependency>

添加配置文件:

# 單個 Memcached 配置
memcached.servers=192.168.0.161:11211
# 連接池
memcached.poolSize=10
#操作超時時間
memcached.opTimeout=6000

配置 Memcached 的地址和端口號、連接池和操作超時時間,使用集羣時可以拼接多個地址:"host1:port1 host2:port2 …"

創建 XMemcachedProperties 類,讀配置信息:

@Component
@ConfigurationProperties(prefix = "memcached")
public class XMemcachedProperties {
    private String servers;
    private int poolSize;
    private long opTimeout;
    //省略 getter/setter
}

啓動加載

利用 @Configuration 註解,在啓動時對 Memcached 進行初始化。

@Configuration
public class MemcachedBuilder {
    protected static Logger logger =  LoggerFactory.getLogger(MemcachedBuilder.class);
    @Resource
    private XMemcachedProperties xMemcachedProperties;

    @Bean
    public  MemcachedClient getMemcachedClient() {
        MemcachedClient memcachedClient = null;
        try {
            MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(xMemcachedProperties.getServers()));
            builder.setConnectionPoolSize(xMemcachedProperties.getPoolSize());
            builder.setOpTimeout(xMemcachedProperties.getOpTimeout());
            memcachedClient = builder.build();
        } catch (IOException e) {
            logger.error("inint MemcachedClient failed ",e);
        }
        return memcachedClient;
    }
}

因爲 XMemcachedClient 的創建有比較多的可選項,所以提供了一個 XMemcachedClientBuilder 類用於構建 MemcachedClient。MemcachedClient 是主要接口,操作 Memcached 的主要方法都在這個接口,XMemcachedClient 是它的一個實現。

在方法 getMemcachedClient() 添加 @Bean 註解,代表啓動時候將方法構建好的實例注入到 Spring 容器中,後面在需要使用的類中,直接注入 MemcachedClient 即可。

進行測試

我們創建一個 MemcachedTests 類,來測試 Memcached 配置信息是否配置正確。

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemcachedTests {
    @Autowired
    private MemcachedClient memcachedClient;
}

測試 Memcached 的 get 、set 方法。

@Test
public void testGetSet() throws Exception {
    memcachedClient.set("hello", 0, "Hello,xmemcached");
    String value = memcachedClient.get("hello");
    System.out.println("hello=" + value);
    memcachedClient.delete("hello");
}

存儲數據是通過 set 方法,它有三個參數,第一個是存儲的 key 名稱,第二個是 expire 時間(單位秒),超過這個時間,memcached 將這個數據替換出去,0 表示永久存儲(默認是一個月),第三個參數就是實際存儲的數據,可以是任意的 Java 可序列化類型。

獲取存儲的數據是通過 get 方法,傳入 key 名稱即可;如果要刪除存儲的數據,可以通過 delete 方法,它也是接受 key 名稱作爲參數。

執行 testMemcached() 單元測試之後,控制檯會輸出:

hello=Hello,xmemcached

證明 Memcached 配置、設置和獲取值成功。

XMemcached 語法介紹

XMemcached 有非常豐富的語法來支持,我們對緩存使用的各種場景,接下來一一介紹。

常用操作

除過上面的 get、set、delete 等方法外,Memcache 還有很多常用的操作。

@Test
public void testMore() throws Exception {
    if (!memcachedClient.set("hello", 0, "world")) {
        System.err.println("set error");
    }
    if (!memcachedClient.add("hello", 0, "dennis")) {
        System.err.println("Add error,key is existed");
    }
    if (!memcachedClient.replace("hello", 0, "dennis")) {
        System.err.println("replace error");
    }
    memcachedClient.append("hello", " good");
    memcachedClient.prepend("hello", "hello ");
    String name = memcachedClient.get("hello", new StringTranscoder());
    System.out.println(name);
    memcachedClient.deleteWithNoReply("hello");
}
  • add 命令,用於將 value(數據值)存儲在指定的 key(鍵)中。如果 add 的 key 已經存在,則不會更新數據(過期的 key 會更新),之前的值將仍然保持相同,並且將獲得響應 NOT_STORED。
  • replace 命令,用於替換已存在的 key(鍵)的 value(數據值)。如果 key 不存在,則替換失敗,並且將獲得響應 NOT_STORED。
  • append 命令,用於向已存在 key(鍵)的 value(數據值)後面追加數據。
  • prepend 命令,用於向已存在 key(鍵)的 value(數據值)前面追加數據。
  • deleteWithNoReply 方法,這個方法刪除數據並且告訴 Memcached,不用返回應答,因此這個方法不會等待應答直接返回,比較適合於批量處理。

Incr 和 Decr

Incr 和 Decr 類似數據的增和減,兩個操作類似 Java 中的原子類如 AtomicIntger,用於原子遞增或者遞減變量數值,示例如下:

@Test
public void testIncrDecr() throws Exception {
    memcachedClient.delete("Incr");
    memcachedClient.delete("Decr");
    System.out.println(memcachedClient.incr("Incr", 6, 12));
    System.out.println(memcachedClient.incr("Incr", 3));
    System.out.println(memcachedClient.incr("Incr", 2));
    System.out.println(memcachedClient.decr("Decr", 1, 6));
    System.out.println(memcachedClient.decr("Decr", 2));
}

爲了防止數據干擾,在測試開始前前調用 delete() 方法清除兩個 key 值。

輸出:

12
15
17
6
4

Incr 和 Decr 都有三個參數的方法,第一個參數指定遞增的 key 名稱,第二個參數指定遞增的幅度大小,第三個參數指定當 key 不存在的情況下的初始值,兩個參數的重載方法省略了第三個參數,默認指定爲 0。

Counter

Xmemcached 還提供了一個稱爲計數器的封裝,它封裝了 incr/decr 方法,使用它就可以類似 AtomicLong 那樣去操作計數,示例如下:

@Test
public void testCounter() throws Exception {
    MemcachedClient memcachedClient = memcachedUtil.getMemcachedClient();
    Counter counter=memcachedClient.getCounter("counter",10);
    System.out.println("counter="+counter.get());
    long c1 =counter.incrementAndGet();
    System.out.println("counter="+c1);
    long c2 =counter.decrementAndGet();
    System.out.println("counter="+c2);
    long c3 =counter.addAndGet(-10);
    System.out.println("counter="+c3);
}
  • memcachedClient.getCounter("counter",10),第一個參數爲計數器的 key,第二參數當 key 不存在時的默認值;
  • counter.incrementAndGet(),執行一次給計數器加 1;
  • counter.decrementAndGet(),執行一次給計數器減 1。

查看 counter.addAndGet(-10) 源碼(如下),發現 addAndGet() 會根據傳入的值的正負來判斷,選擇直接給對應的 key 加多少或者減多少,底層也是使用了 incr() 和 decr() 方法。

public long addAndGet(long delta) throws MemcachedException, InterruptedException, TimeoutException {
    return delta >= 0L ? this.memcachedClient.incr(this.key, delta, this.initialValue) : this.memcachedClient.decr(this.key, -delta, this.initialValue);
}

Counter 適合在高併發搶購場景下做併發控制。

CAS 操作

Memcached 是通過 CAS 協議實現原子更新,所謂原子更新就是 Compare and Set,原理類似樂觀鎖,每次請求存儲某個數據同時要附帶一個 CAS 值,Memcached 比對這個 CAS 值與當前存儲數據的 CAS 值是否相等,如果相等就讓新的數據覆蓋老的數據,如果不相等就認爲更新失敗,這在併發環境下特別有用。XMemcached 提供了對 CAS 協議的支持(無論是文本協議還是二進制協議),CAS 協議其實是分爲兩個步驟:獲取 CAS 值和嘗試更新,因此一個典型的使用場景如下:

GetsResponse<Integer> result = client.gets("a");
long cas = result.getCas(); 
//嘗試將 a 的值更新爲 2
if (!client.cas("a", 0, 2, cas)) {
    System.err.println("cas error");
}

首先通過 gets 方法獲取一個 GetsResponse,此對象包裝了存儲的數據和 CAS 值,然後通過 CAS 方法嘗試原子更新,如果失敗打印“cas error”。顯然,這樣的方式很繁瑣,並且如果你想嘗試多少次原子更新就需要一個循環來包裝這一段代碼,因此 XMemcached 提供了一個 CASOperation 接口包裝了這部分操作,允許你嘗試 N 次去原子更新某個 key 存儲的數據,無需顯式地調用 gets 獲取 CAS 值,上面的代碼簡化爲:

client.cas("a", 0, new CASOperation<Integer>() {
             public int getMaxTries() {
            return 1;
        }

        public Integer getNewValue(long currentCAS, Integer currentValue) {
                return 2;
        }
});

CASOpertion 接口只有兩個方法,一個是設置最大嘗試次數的 getMaxTries 方法,這裏是嘗試一次,如果嘗試超過這個次數沒有更新成功將拋出一個 TimeoutException,如果你想無限嘗試(理論上),可以將返回值設定爲 Integer.MAX_VALUE;另一個方法是根據當前獲得的 GetsResponse 來決定更新數據的 getNewValue 方法,如果更新成功,這個方法返回的值將存儲成功,其兩個參數是最新一次 gets 返回的 GetsResponse 結果。

設置超時時間

XMemcached 由於是基於 nio,因此通訊過程本身是異步的,client 發送一個請求給 Memcached,你是無法確定 Memcached 什麼時候返回這個應答,客戶端此時只有等待,因此還有個等待超時的概念在這裏。客戶端在發送請求後,開始等待應答,如果超過一定時間就認爲操作失敗,這個等待時間默認是 5 秒,也可以在獲取的時候配置超時時間。

value=client.get("hello",3000);

就是等待 3 秒超時,如果 3 秒超時就跑出 TimeutException,用戶需要自己處理這個異常。因爲等待是通過調用 CountDownLatch.await(timeout) 方法,所以用戶還需要處理中斷異常 InterruptException,最後的 MemcachedException 表示 Xmemcached 內部發生的異常,如解碼編碼錯誤、網絡斷開等異常情況。

更新緩存過期時間

經常有這樣的需求,就是希望更新緩存數據的超時時間(expire time),現在 Memcached 已經支持 touch 協議,只需要傳遞 key 就更新緩存的超時時間:

client.touch(key,new-expire-time);

有時候你希望獲取緩存數據並更新超時時間,這時候可以用 getAndTouch 方法(僅二進制協議支持):

client.getAndTouch(key,new-expire-time);

如果在使用過程中報以下錯誤:

Caused by: net.rubyeye.xmemcached.exception.UnknownCommandException: Response error,error message:Unknow command TOUCH,key=Touch
    at net.rubyeye.xmemcached.command.Command.decodeError(Command.java:250)

說明安裝的 Memcached 服務不支持 touch 命令,建議升級。

測試示例:

@Test
public void testTouch() throws Exception {
    memcachedClient.set("Touch", 2, "Touch Value");
    Thread.sleep(1000);
    memcachedClient.touch("Touch",6);
    Thread.sleep(2000);
    String value =memcachedClient.get("Touch",3000);
    System.out.println("Touch=" + value);
}

Memcached 集羣

Memcached 的分佈是通過客戶端實現的,客戶端根據 key 的哈希值得到將要存儲的 Memcached 節點,並將對應的 value 存儲到相應的節點。

XMemcached 同樣支持客戶端的分佈策略,默認分佈的策略是按照 key 的哈希值模以連接數得到的餘數,對應的連接就是將要存儲的節點。如果使用默認的分佈策略,不需要做任何配置或者編程。

XMemcached 同樣支持一致性哈希(Consistent Hash),通過編程設置:

MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("server1:11211 server2:11211 server3:11211"));
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
MemcachedClient client=builder.build();

XMemcached 還提供了額外的一種哈希算法——選舉散列,在某些場景下可以替代一致性哈希:

MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                                    AddrUtil.getAddresses("server1:11211 server2:11211 server3:11211"));
builder.setSessionLocator(new ElectionMemcachedSessionLocator());
MemcachedClient mc = builder.build();

在集羣的狀態下可以給每個服務設置不同的權重:

MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("localhost:12000 localhost:12001"),new int[]{1,3});
MemcachedClient memcachedClient=builder.build();

SASL 驗證

Memcached 1.4.3 開始支持 SASL 驗證客戶端,在服務器配置啓用 SASL 之後,客戶端需要通過授權驗證纔可以跟 Memcached 繼續交互,否則將被拒絕請求,XMemcached 1.2.5 開始支持這個特性。假設 Memcached 設置了 SASL 驗證,典型地使用 CRAM-MD 5 或者 PLAIN 的文本用戶名和密碼的驗證機制,假設用戶名爲 cacheuser,密碼爲 123456,那麼編程的方式如下:

MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                AddrUtil.getAddresses("localhost:11211"));
builder.addAuthInfo(AddrUtil.getOneAddress("localhost:11211"), AuthInfo
                .typical("cacheuser", "123456"));
// Must use binary protocol
builder.setCommandFactory(new BinaryCommandFactory());
MemcachedClient client=builder.build();

請注意,授權驗證僅支持二進制協議。

查看統計信息

Memcached 提供了統計協議用於查看統計信息:

Map<InetSocketAddress,Map<String,String>> result=client.getStats();

getStats 方法返回一個 map ,其中存儲了所有已經連接並且有效的 Memcached 節點返回的統計信息,你也可以統計具體的項目,如統計 items 項目:

Map<InetSocketAddress,Map<String,String>> result=client.getStatsByItem("items");

只要向 getStatsByItem 傳入需要統計的項目名稱即可,我們可以利用這個功能,來做 Memcached 狀態監控等。

總結

Memcached 是一款非常流行的緩存中間件,被廣泛應用在各場景中,使用緩存可以環境數據庫壓力,某些場景下使用緩存可以大大提高複用的 Tps 。 XMemcached 是 Memcached 的一個高性能 Nio 客戶端,支持 Memcached 底層各種操作,並且在 Memcached 協議的基礎上進行了封裝和完善,提供了連接池、集羣、數據壓縮、分佈式算法等高級功能,不論是完善度和性能各方面來看,XMemcached 都是目前最爲推薦的一款 Memcached 客戶端。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章