【編者按】本文作者為 Xinyu Liu,詳細介紹了 Redis 的特性,并輔之以豐富的用例。在本文的第一部分,將重點概述 Redis 的方方面面。文章系國內(nèi) ITOM管理平臺 OneAPM編譯呈現(xiàn)。
建立在 Java企業(yè)版之上的多層體系結(jié)構(gòu)是強大的服務器端編程解決方案。作為一名從業(yè)多年的 Java 企業(yè)版開發(fā)人員,我最滿意的就是三層企業(yè)開發(fā)法:最下方是 JPA/Hibernate 持久層,中間是 Spring 或 EJB 應用層,最上方則是 web 層。對于較為復雜的用例,我用 BPM(業(yè)務流程管理)、一個類似于 Drools的規(guī)則引擎和一個集成框架(例如 Camel)集成了一個工作流驅(qū)動的解決方案。
但是,筆者最近接到一個任務,要設計一個擁有亞秒級響應延遲并能支持成千上萬名并發(fā)用戶的系統(tǒng)。我立即發(fā)現(xiàn)了自己常用的 Java 企業(yè)版棧區(qū)的局限性。基于關(guān)系數(shù)據(jù)庫管理系統(tǒng)的傳統(tǒng)型 web 應用程序,包括在 Hibernate/JPA 之上構(gòu)建的應用程序,都有二階延遲,擴展效果不佳。傳統(tǒng)的 Java 企業(yè)版持久性體系結(jié)構(gòu)無法滿足我當時設計的系統(tǒng)的性能和處理能力要求。然后我轉(zhuǎn)而嘗試 NoSQL,最后發(fā)現(xiàn)了 Redis。
作為一種內(nèi)存鍵值數(shù)據(jù)庫,Redis 打破了數(shù)據(jù)庫的傳統(tǒng)定義(將數(shù)據(jù)保存在硬盤上)。反之,使用 Redis 時可結(jié)合持久性的 NoSQL 數(shù)據(jù)庫,比如 MongoDB、HBase、Cassandra或 DynamoDB。Redis 以遠程緩存服務器見長,對易揮發(fā)數(shù)據(jù)來說是極快型數(shù)據(jù)庫。
在本文中,筆者會介紹一些有關(guān) Redis 的簡單用例和進階用例以及性能調(diào)優(yōu)情況。當然,我還會做個簡單概述,但我相信各位基本都了解 NoSQL 及其各種解決方案。
Spring Data Redis
Redis 幾乎擁有針對所有編程語言的各種客戶端庫,其中就包括Java。Jedis可能是最受歡迎的 Java 客戶端庫了。本文中的示例都基于 Spring Data Redis,我把它作為一個較高層次的包裝程序 API。Spring Data Redis 不僅配置方便,而且擁有各種友好的 API和實用插件。
Redis 概述
和大多數(shù) NoSQL 數(shù)據(jù)庫一樣,Redis 舍棄了表格、行列的關(guān)系概念。而事實上,Redis 是一種鍵值數(shù)據(jù)庫,利用獨特的字符串鍵值來存儲和檢索每條記錄。Redis 支持把以下內(nèi)置數(shù)據(jù)結(jié)構(gòu)作為所有記錄的值:
- STRING 保有單個字符串值。
- LIST、SET 和 HASH 從語義上來說與 Java 中的相同數(shù)據(jù)結(jié)構(gòu)相一致。
- ZSET 是由浮點分數(shù)安排的字符串列表,類似于 Java 中的 PriorityQueue。
不同于關(guān)系數(shù)據(jù)庫管理系統(tǒng)中的表,Redis 數(shù)據(jù)結(jié)構(gòu)是即時實例化的。如果用戶查詢的內(nèi)容不存在于 Redis 中,系統(tǒng)只會返回空值。雖然 Redis 不允許嵌套結(jié)構(gòu),但用戶可以執(zhí)行自定義的 Java 或 JSON 串行器/解串器,從而將 POJO 映射到字符串。通過這種方式,就可以把任意 Java bean 保存為 STRING,或者將其放置在 LIST、SET 中,等等。
性能和可擴展性
對于 Redis,人們注意到的第一個特點可能就是它的速度極快。根據(jù)記錄的大小和連接的數(shù)量,性能基準會有所不同,但延遲通常為單數(shù)位毫秒。在大多數(shù)用例中,Redis 每秒最多可支持 50000 次請求。如果用戶使用較高端的硬件,處理能力更可高達每秒 700000 次請求(但這一數(shù)值可能會被網(wǎng)卡帶寬扼制)。
作為一種內(nèi)存數(shù)據(jù)庫,Redis 的存儲容量有限; AWS EC2 中的最大實例為 r3.8xlarge,內(nèi)存 244 GB。由于數(shù)據(jù)結(jié)構(gòu)的索引和性能都經(jīng)過優(yōu)化,Redis 消耗的內(nèi)存比所存儲的數(shù)據(jù)量大得多。切分 Redis 有助于克服這一局限性。要把內(nèi)存數(shù)據(jù)備份到硬盤上,可以在預定作業(yè)中進行時間點轉(zhuǎn)儲,也可以根據(jù)需要運行dump 命令。
用 Spring 進行遠程數(shù)據(jù)緩存
要想提升應用程序服務器的性能,數(shù)據(jù)緩存可能是性價比最高的辦法了。利用 Spring 的緩存抽象注釋(@Cacheable、@CachePut、@CacheEvict、@Caching 和 @CacheConfig)可以毫不費力地啟用數(shù)據(jù)緩存。在 Spring 配置下,用戶還可以把 Ehcache、Memcached或 Redis 當作基本緩存服務器。
Encache 通常被配置成本地緩存層,具有嵌套結(jié)構(gòu),在應用的 JVM 上運行。 Memcached 和 Redis 都能作為獨立的緩存服務器運行。要想把 Redis 緩存集成到基于 Spring 的應用中,需要使用 Spring Data Redis的 RedisTemplate 和 RedisCacheManager。
在 Redis 中訪問已緩存的對象,耗時通常不到數(shù)毫秒,和關(guān)系數(shù)據(jù)庫查詢相比,這大幅提升了應用程序的性能。
延遲和收益
亞馬遜公司在很大程度上依賴緩存服務器來最大程度地減少其零售網(wǎng)站的延遲,該公司甚至曾經(jīng)發(fā)布過一份案例分析,其中記錄了延遲和收益之間的關(guān)系。
本地緩存與遠程緩存
在沒有網(wǎng)絡開銷的系統(tǒng)中,本地緩存快于遠程緩存。本地緩存的缺點是,同一個對象的多個拷貝在服務器集群中的各個不同節(jié)點之中會同步得更快。正因如此,本地緩存僅適用于靜態(tài)數(shù)據(jù),例如可容忍短期滯后和不一致現(xiàn)象的系統(tǒng)級設置。如果為易揮發(fā)的業(yè)務數(shù)據(jù)(例如用戶數(shù)據(jù)和交易數(shù)據(jù))使用本地緩存,很有可能會以運行應用程序服務器的單個實例而告終。
遠程緩存服務器就沒有這一局限性。在同一個鍵的情況下,可保證緩存服務器上的對象只有一個拷貝。只要用戶讓緩存中的對象及其數(shù)據(jù)庫值彼此保持同步,就無需處理過期數(shù)據(jù)。
列表 1 給出了一個 Spring 數(shù)據(jù)緩存的示例。
列表 1:在基于 Spring 的應用中啟用緩存
@Cacheable(value="User_CACHE_REPOSITORY", key = "#id")
public User get(Long id) {
return em.find(User.class, id);
}
@Caching(put = {@CachePut(value="USER_CACHE_REPOSITORY", key = "#user.getId()")})
public User update(User user) {
em.merge(user);
return user;
}
@Caching(evict = {@CacheEvict(value="USER_CACHE_REPOSITORY", key = "#user.getId()")}) public void delete(User user) {
em.remove(user);
}
@Caching(evict = {@CacheEvict(value="USER_CACHE_REPOSITORY", key = "#user.getId()")}) public void evictCache(User user) {
}
這里的讀取操作被 Spring 的 @Cacheable 注釋圍繞,作為 AOP 幕僚而執(zhí)行。Spring 中的存活時間設置也規(guī)定了這些對象可在緩存中停留的時間。調(diào)用 get() 方法后,Spring 就會試著先從遠程緩存讀取和返回對象。如果未找到對象,Spring 會執(zhí)行方法主體,然后將數(shù)據(jù)庫結(jié)果放在遠程緩存中,之后再返回結(jié)果。
但如果另一個過程(例如另一個服務器節(jié)點)甚至同一個 JVM 中的另一個線程在數(shù)據(jù)庫中更新了同一個對象,又會怎樣呢?如果只運用 @Cacheable 注釋,你可能會從遠程緩存服務器收到過期拷貝。
為了防止發(fā)生這種情況,可以給所有數(shù)據(jù)庫更新操作添加一個 @CachePut 注釋。每次調(diào)用這些方法時,返回值就會替換掉遠程緩存中原先的對象。在數(shù)據(jù)庫讀取和寫入上都更新緩存,可以讓緩存服務器和后臺數(shù)據(jù)之間的記錄保持同步。
容錯
聽起來簡直完美,對吧?事實當然不是這樣。利用列表 1 中的配置,負載較低時可能不會遇到任何問題,但隨著服務器集群上的負載逐漸增加,遠程緩存上就會出現(xiàn)過期數(shù)據(jù)。要做好準備應對服務器節(jié)點爭用甚至更糟的情況。即使成功寫入數(shù)據(jù)庫,最后也可能會因為網(wǎng)絡故障而使得緩存服務器 PUT 以失敗告終。另外,NoSQL 通常不支持在關(guān)系數(shù)據(jù)庫中存在完整事務語義,因為這會導致部分提交。為了讓代碼容錯,可以考慮給數(shù)據(jù)模型增加版本號,實現(xiàn)樂觀鎖。
在收到 OptimisticLockingFailureException 或 CurrentModificationException(具體取決于持久性解決方案)時,可以調(diào)用帶有 @CacheEvict 注釋的方法,從緩存中清除過期拷貝,然后重試同一個操作:
列表 2:解決緩存中的過期對象
try{
User user = userDao.get(id); // user fetched in cache server
userDao.update(user, oldname, newname);
}catch(ConcurrentModificationException ex) { // cached user object may be stale
userDao.evictCache(user);
user = userDao.get(id); // refresh user object
userDao.update(user, oldname, newname); // retry the same operation. Note it may still throw legitimate ConcurrentModificationException.}
結(jié)合 Elasticache 使用 Redis
Amazon Elasticache 是一款內(nèi)存緩存服務,可結(jié)合 Memcached 或Redis 作為緩存服務器使用。雖然Elasticache不在本文介紹范圍內(nèi),但筆者還是想給各位開發(fā)人員介紹一個結(jié)合 Redis 使用 Elasticache的技巧。對于大多數(shù) Redis 參數(shù),使用其默認值并無大礙,但 tcp-keepalive 和timeout的默認Redis設置并不會移除已無效的客戶連接,最后還會耗盡緩存服務上的套接口。結(jié)合Elasticache使用Redis時,務必每次都明確設置這兩個值。
在本文的第二部分,將介紹 Redis 的6大用例,敬請期待。
本文系 OneAPM工程師編譯整理。OneAPM 能為您提供端到端的 Java 應用性能解決方案,我們支持所有常見的 Java 框架及應用服務器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監(jiān)控從來沒有如此簡單。想閱讀更多技術(shù)文章,請訪問 OneAPM 官方技術(shù)博客。