【編者按】本文作者為 Xinyu Liu,詳細(xì)介紹了 Redis 的特性聂喇,并輔之以豐富的用例辖源。在本文的第一部分,將重點概述 Redis 的方方面面授帕。文章系國內(nèi) ITOM管理平臺 OneAPM編譯呈現(xiàn)同木。
建立在 Java企業(yè)版之上的多層體系結(jié)構(gòu)是強大的服務(wù)器端編程解決方案浮梢。作為一名從業(yè)多年的 Java 企業(yè)版開發(fā)人員跛十,我最滿意的就是三層企業(yè)開發(fā)法:最下方是 JPA/Hibernate 持久層,中間是 Spring 或 EJB 應(yīng)用層秕硝,最上方則是 web 層芥映。對于較為復(fù)雜的用例,我用 BPM(業(yè)務(wù)流程管理)、一個類似于 Drools的規(guī)則引擎和一個集成框架(例如 Camel)集成了一個工作流驅(qū)動的解決方案奈偏。
但是坞嘀,筆者最近接到一個任務(wù),要設(shè)計一個擁有亞秒級響應(yīng)延遲并能支持成千上萬名并發(fā)用戶的系統(tǒng)惊来。我立即發(fā)現(xiàn)了自己常用的 Java 企業(yè)版棧區(qū)的局限性丽涩。基于關(guān)系數(shù)據(jù)庫管理系統(tǒng)的傳統(tǒng)型 web 應(yīng)用程序裁蚁,包括在 Hibernate/JPA 之上構(gòu)建的應(yīng)用程序矢渊,都有二階延遲,擴展效果不佳枉证。傳統(tǒng)的 Java 企業(yè)版持久性體系結(jié)構(gòu)無法滿足我當(dāng)時設(shè)計的系統(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 以遠(yuǎn)程緩存服務(wù)器見長,對易揮發(fā)數(shù)據(jù)來說是極快型數(shù)據(jù)庫崎弃。
在本文中甘晤,筆者會介紹一些有關(guān) Redis 的簡單用例和進(jìn)階用例以及性能調(diào)優(yōu)情況。當(dāng)然饲做,我還會做個簡單概述线婚,但我相信各位基本都了解 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 是由浮點分?jǐn)?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ù)量姐霍,性能基準(zhǔn)會有所不同,但延遲通常為單數(shù)位毫秒典唇。在大多數(shù)用例中镊折,Redis 每秒最多可支持 50000 次請求。如果用戶使用較高端的硬件介衔,處理能力更可高達(dá)每秒 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ù)定作業(yè)中進(jìn)行時間點轉(zhuǎn)儲级野,也可以根據(jù)需要運行dump 命令。
用 Spring 進(jìn)行遠(yuǎn)程數(shù)據(jù)緩存
要想提升應(yīng)用程序服務(wù)器的性能阴幌,數(shù)據(jù)緩存可能是性價比最高的辦法了勺阐。利用 Spring 的緩存抽象注釋(@Cacheable、@CachePut矛双、@CacheEvict渊抽、@Caching 和 @CacheConfig)可以毫不費力地啟用數(shù)據(jù)緩存。在 Spring 配置下议忽,用戶還可以把 Ehcache懒闷、Memcached或 Redis 當(dāng)作基本緩存服務(wù)器。
Encache 通常被配置成本地緩存層栈幸,具有嵌套結(jié)構(gòu)愤估,在應(yīng)用的 JVM 上運行。 Memcached 和 Redis 都能作為獨立的緩存服務(wù)器運行速址。要想把 Redis 緩存集成到基于 Spring 的應(yīng)用中玩焰,需要使用 Spring Data Redis的 RedisTemplate 和 RedisCacheManager。
在 Redis 中訪問已緩存的對象芍锚,耗時通常不到數(shù)毫秒昔园,和關(guān)系數(shù)據(jù)庫查詢相比,這大幅提升了應(yīng)用程序的性能并炮。
延遲和收益
亞馬遜公司在很大程度上依賴緩存服務(wù)器來最大程度地減少其零售網(wǎng)站的延遲默刚,該公司甚至曾經(jīng)發(fā)布過一份案例分析,其中記錄了延遲和收益之間的關(guān)系逃魄。
本地緩存與遠(yuǎn)程緩存
在沒有網(wǎng)絡(luò)開銷的系統(tǒng)中荤西,本地緩存快于遠(yuǎn)程緩存。本地緩存的缺點是伍俘,同一個對象的多個拷貝在服務(wù)器集群中的各個不同節(jié)點之中會同步得更快邪锌。正因如此,本地緩存僅適用于靜態(tài)數(shù)據(jù)癌瘾,例如可容忍短期滯后和不一致現(xiàn)象的系統(tǒng)級設(shè)置秃流。如果為易揮發(fā)的業(yè)務(wù)數(shù)據(jù)(例如用戶數(shù)據(jù)和交易數(shù)據(jù))使用本地緩存,很有可能會以運行應(yīng)用程序服務(wù)器的單個實例而告終柳弄。
遠(yuǎn)程緩存服務(wù)器就沒有這一局限性舶胀。在同一個鍵的情況下,可保證緩存服務(wù)器上的對象只有一個拷貝碧注。只要用戶讓緩存中的對象及其數(shù)據(jù)庫值彼此保持同步嚣伐,就無需處理過期數(shù)據(jù)。
列表 1 給出了一個 Spring 數(shù)據(jù)緩存的示例萍丐。
列表 1:在基于 Spring 的應(yīng)用中啟用緩存
@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 中的存活時間設(shè)置也規(guī)定了這些對象可在緩存中停留的時間逝变。調(diào)用 get() 方法后基茵,Spring 就會試著先從遠(yuǎn)程緩存讀取和返回對象奋构。如果未找到對象,Spring 會執(zhí)行方法主體拱层,然后將數(shù)據(jù)庫結(jié)果放在遠(yuǎn)程緩存中弥臼,之后再返回結(jié)果。
但如果另一個過程(例如另一個服務(wù)器節(jié)點)甚至同一個 JVM 中的另一個線程在數(shù)據(jù)庫中更新了同一個對象根灯,又會怎樣呢径缅?如果只運用 @Cacheable 注釋,你可能會從遠(yuǎn)程緩存服務(wù)器收到過期拷貝烙肺。
為了防止發(fā)生這種情況纳猪,可以給所有數(shù)據(jù)庫更新操作添加一個 @CachePut 注釋。每次調(diào)用這些方法時桃笙,返回值就會替換掉遠(yuǎn)程緩存中原先的對象氏堤。在數(shù)據(jù)庫讀取和寫入上都更新緩存,可以讓緩存服務(wù)器和后臺數(shù)據(jù)之間的記錄保持同步搏明。
容錯
聽起來簡直完美丽猬,對吧?事實當(dāng)然不是這樣熏瞄。利用列表 1 中的配置脚祟,負(fù)載較低時可能不會遇到任何問題,但隨著服務(wù)器集群上的負(fù)載逐漸增加强饮,遠(yuǎn)程緩存上就會出現(xiàn)過期數(shù)據(jù)由桌。要做好準(zhǔn)備應(yīng)對服務(wù)器節(jié)點爭用甚至更糟的情況。即使成功寫入數(shù)據(jù)庫邮丰,最后也可能會因為網(wǎng)絡(luò)故障而使得緩存服務(wù)器 PUT 以失敗告終行您。另外,NoSQL 通常不支持在關(guān)系數(shù)據(jù)庫中存在完整事務(wù)語義剪廉,因為這會導(dǎo)致部分提交娃循。為了讓代碼容錯,可以考慮給數(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)存緩存服務(wù),可結(jié)合 Memcached 或Redis 作為緩存服務(wù)器使用跷究。雖然Elasticache不在本文介紹范圍內(nèi)姓迅,但筆者還是想給各位開發(fā)人員介紹一個結(jié)合 Redis 使用 Elasticache的技巧。對于大多數(shù) Redis 參數(shù),使用其默認(rèn)值并無大礙丁存,但 tcp-keepalive 和timeout的默認(rèn)Redis設(shè)置并不會移除已無效的客戶連接肩杈,最后還會耗盡緩存服務(wù)上的套接口。結(jié)合Elasticache使用Redis時解寝,務(wù)必每次都明確設(shè)置這兩個值扩然。
在本文的第二部分,將介紹 Redis 的6大用例编丘,敬請期待。
本文系 OneAPM工程師編譯整理彤悔。OneAPM 能為您提供端到端的 Java 應(yīng)用性能解決方案嘉抓,我們支持所有常見的 Java 框架及應(yīng)用服務(wù)器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸晕窑,定位異常根本原因抑片。分鐘級部署,即刻體驗杨赤,Java 監(jiān)控從來沒有如此簡單敞斋。想閱讀更多技術(shù)文章,請訪問 OneAPM 官方技術(shù)博客疾牲。