Spring Boot 操作 Memcache
1 第4-1課:Spring Boot 操作 Memcache
《精通 Spring Boot 42 講》共分五大部分,第四部分主要講解 Spring Boot 和中介軟體的使用,共 10 課,中介軟體是網際網路公司支撐高併發業務的必備元件,常用的元件有快取、訊息中介軟體、NoSQL 資料庫、定時任務等。常用的快取中介軟體有 Memcache 和 Redis ,快取主要支撐業務架構中高速讀寫;常用的訊息中介軟體有 ActiveMQ 、RabbitMQ ,使用訊息中介軟體的意義是,儘快地完成主線交易,其他非實時業務非同步或者解耦完成;最主流的 NoSQL 有 MongoDB、 ElasticSearch,前者主要是解決分散式儲存和檢索的問題,後者主要解決分散式文件檢索的解決方案;定時任務常常使用開源框架 Quartz。以上的這些內容我們都會在第四部分進行學習。
在常見的企業架構中,隨著公司業務高速發展,最先出現瓶頸的是資料庫,這個時候很多企業就會考慮使用快取來緩解資料庫的壓力,這是快取使用最多的場景之一;另外在高併發搶購、分散式 Session 等場景下,也會使用快取來提高系統的高可用性。常用的快取中介軟體有 Memcache 和 Redis,今天我們先來學習 Memcache 的使用。
1.1 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 是伺服器端的主程式名稱。
1.1.1 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 未提供任何安全策略
- 不支援持久化
1.1.2 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
1.2 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 實現的,因此在實現上結構相對清楚,分層比較明晰。
1.3 快速上手
上面介紹了這麼多,最需要關注的是 XMemcached 是最佳的選擇,下面我們先用一個示例,來感受一下 Spring Boot 使用 Xmemcached 整合 Memcache。
1.3.1 新增配置
新增依賴包:
<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")
publicclass XMemcachedProperties
{
private
String servers;
private
int
poolSize;
private
long
opTimeout;
//省略 getter/setter
}
1.3.2 啟動載入
利用 @Configuration 註解,在啟動時對 Memcached 進行初始化。
@Configuration
publicclass MemcachedBuilder
{
protected
static
Logger logger = LoggerFactory.getLogger(MemcachedBuilder.class);
@Resource
private
XMemcachedProperties xMemcachedProperties;
@Bean
public MemcachedClient getMemcachedClient()
{
MemcachedClient memcachedClient =
null;
try
{
MemcachedClientBuilder builder =
newXMemcachedClientBuilder(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 即可。
1.3.3 進行測試
我們建立一個 MemcachedTests 類,來測試 Memcached 配置資訊是否配置正確。
@RunWith(SpringRunner.class)
@SpringBootTest
publicclass 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 配置、設定和獲取值成功。
1.4 XMemcached 語法介紹
XMemcached 有非常豐富的語法來支援,我們對快取使用的各種場景,接下來一一介紹。
1.4.1 常用操作
除過上面的 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",
newStringTranscoder());
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,不用返回應答,因此這個方法不會等待應答直接返回,比較適合於批量處理。
1.4.2 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。
1.4.3 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 適合在高併發搶購場景下做併發控制。
1.4.4 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,
newCASOperation<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 結果。
1.4.5 設定超時時間
XMemcached 由於是基於 nio,因此通訊過程本身是非同步的,client 傳送一個請求給 Memcached,你是無法確定 Memcached 什麼時候返回這個應答,客戶端此時只有等待,因此還有個等待超時的概念在這裡。客戶端在傳送請求後,開始等待應答,如果超過一定時間就認為操作失敗,這個等待時間預設是 5 秒,也可以在獲取的時候配置超時時間。
value=client.
get(
"hello",
3000);
就是等待 3 秒超時,如果 3 秒超時就跑出 TimeutException,使用者需要自己處理這個異常。因為等待是通過呼叫 CountDownLatch.await(timeout) 方法,所以使用者還需要處理中斷異常 InterruptException,最後的 MemcachedException 表示 Xmemcached 內部發生的異常,如解碼編碼錯誤、網路斷開等異常情況。
1.4.6 更新快取過期時間
經常有這樣的需求,就是希望更新快取資料的超時時間(expire time),現在 Memcached 已經支援 touch 協議,只需要傳遞 key 就更新快取的超時時間:
client.touch(key,
new-expire-time);
有時候你希望獲取快取資料並更新超時時間,這時候可以用 getAndTouch 方法(僅二進位制協議支援):
client.getAndTouch(key,
new-expire-time);
如果在使用過程中報以下錯誤:
Causedby: net.rubyeye.xmemcached.exception.UnknownCommandException: Response
error,
errormessage: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);
}
1.4.7 Memcached 叢集
Memcached 的分佈是通過客戶端實現的,客戶端根據 key 的雜湊值得到將要儲存的 Memcached 節點,並將對應的 value 儲存到相應的節點。
XMemcached 同樣支援客戶端的分佈策略,預設分佈的策略是按照 key 的雜湊值模以連線數得到的餘數,對應的連線就是將要儲存的節點。如果使用預設的分佈策略,不需要做任何配置或者程式設計。
XMemcached 同樣支援一致性雜湊(Consistent Hash),通過程式設計設定:
MemcachedClientBuilder builder =
newXMemcachedClientBuilder(AddrUtil.getAddresses(
"server1:11211 server2:11211 server3:11211"));
builder.setSessionLocator(
newKetamaMemcachedSessionLocator());
MemcachedClient client=builder.build();
XMemcached 還提供了額外的一種雜湊演算法——選舉雜湊,在某些場景下可以替代一致性雜湊:
MemcachedClientBuilder builder =
newXMemcachedClientBuilder(
AddrUtil.getAddresses(
"server1:11211 server2:11211 server3:11211"));
builder.setSessionLocator(
newElectionMemcachedSessionLocator());
MemcachedClient mc = builder.build();
在叢集的狀態下可以給每個服務設定不同的權重:
MemcachedClientBuilder builder =
newXMemcachedClientBuilder(AddrUtil.getAddresses(
"localhost:12000 localhost:12001"),
newint
[]{
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 =
newXMemcachedClientBuilder(
AddrUtil.getAddresses(
"localhost:11211"));
builder.addAuthInfo(AddrUtil.getOneAddress(
"localhost:11211"), AuthInfo
.typical(
"cacheuser",
"123456"));
// Must use binary protocol
builder.setCommandFactory(
newBinaryCommandFactory());
MemcachedClient client=builder.build();
請注意,授權驗證僅支援二進位制協議。
1.4.8 檢視統計資訊
Memcached 提供了統計協議用於檢視統計資訊:
Map<InetSocketAddress,
Map<
String,
String>> result=client.getStats();
getStats 方法返回一個 map ,其中儲存了所有已經連線並且有效的 Memcached 節點返回的統計資訊,你也可以統計具體的專案,如統計 items 專案:
Map<InetSocketAddress,
Map<
String,
String>> result=client.getStatsByItem(
"items");
只要向 getStatsByItem 傳入需要統計的專案名稱即可,我們可以利用這個功能,來做 Memcached 狀態監控等。
1.5 總結
Memcached 是一款非常流行的快取中介軟體,被廣泛應用在各場景中,使用快取可以環境資料庫壓力,某些場景下使用快取可以大大提高複用的 Tps 。 XMemcached 是 Memcached 的一個高效能 Nio 客戶端,支援 Memcached 底層各種操作,並且在 Memcached 協議的基礎上進行了封裝和完善,提供了連線池、叢集、資料壓縮、分散式演算法等高階功能,不論是完善度和效能各方面來看,XMemcached 都是目前最為推薦的一款 Memcached 客戶端。