【編者按】本文做者爲 Xinyu Liu,文章的第一部分重點概述了 Redis 方方面面的特性。在第二部分,將介紹詳細的用例。文章系國內 ITOM 管理平臺 OneAPM 編譯呈現。html
##把 Redis 看成數據庫的用例 如今咱們來看看在服務器端 Java 企業版系統中把 Redis 看成數據庫的各類用法吧。不管用例的簡繁,Redis 都能幫助用戶優化性能、處理能力和延遲,讓常規 Java 企業版技術棧望而卻步。java
###1. 全局惟一增量計數器 咱們先從一個相對簡單的用例開始吧:一個增量計數器,可顯示某網站受到多少次點擊。Spring Data Redis 有兩個適用於這一實用程序的類:RedisAtomicInteger
和 RedisAtomicLong
。和 Java 併發包中的 AtomicInteger
和 AtomicLong
不一樣的是,這些 Spring 類能在多個 JVM 中發揮做用。git
列表 3:全局惟一增量計數器github
RedisAtomicLong counter = new RedisAtomicLong("UNIQUE_COUNTER_NAME", redisTemplate.getConnectionFactory()); Long myCounter = counter.incrementAndGet();// return the incremented value
請注意整型溢出並謹記,在這兩個類上進行操做須要付出相對較高的代價。web
###2. 全局悲觀鎖 時不時的,用戶就得應對服務器集羣的爭用。假設你從一個服務器集羣運行一個預約做業。在沒有全局鎖的狀況下,集羣中的節點會發起冗餘做業實例。假設某個聊天室分區可容納 50 人。若是聊天室已滿,就須要建立新的聊天室實例來容納另外 50 人。redis
若是檢測到聊天室已滿但沒有全局鎖,集羣中的各個節點就會建立自有的聊天室實例,爲整個系統帶來不可預知的因素。列表 4 介紹了應當如何充分利用 SETNX(SET if Not eXists:若是不存在,則設置)這一 Redis 命令來執行全局悲觀鎖。spring
列表4:全局悲觀鎖sql
public String aquirePessimisticLockWithTimeout(String lockName, int acquireTimeout, int lockTimeout) { if (StringUtils.isBlank(lockName) || lockTimeout <= 0) return null; final String lockKey = lockName; String identifier = UUID.randomUUID().toString(); Calendar atoCal = Calendar.getInstance(); atoCal.add(Calendar.SECOND, acquireTimeout); Date atoTime = atoCal.getTime(); while (true) { // try to acquire the lock if (redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.setNX( redisTemplate.getStringSerializer().serialize(lockKey), redisTemplate.getStringSerializer().serialize(identifier)); } })) { // successfully acquired the lock, set expiration of the lock redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.expire(redisTemplate .getStringSerializer().serialize(lockKey), lockTimeout); } }); return identifier; } else { // fail to acquire the lock // set expiration of the lock in case ttl is not set yet. if (null == redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.ttl(redisTemplate .getStringSerializer().serialize(lockKey)); } })) { // set expiration of the lock redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.expire(redisTemplate .getStringSerializer().serialize(lockKey), lockTimeout); } }); } if (acquireTimeout < 0) // no wait return null; else { try { Thread.sleep(100l); // wait 100 milliseconds before retry } catch (InterruptedException ex) { } } if (new Date().after(atoTime)) break; } } return null; } public void releasePessimisticLockWithTimeout(String lockName, String identifier) { if (StringUtils.isBlank(lockName) || StringUtils.isBlank(identifier)) return; final String lockKey = lockName; redisTemplate.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { byte[] ctn = connection.get(redisTemplate .getStringSerializer().serialize(lockKey)); if(ctn!=null && identifier.equals(redisTemplate.getStringSerializer().deserialize(ctn))) connection.del(redisTemplate.getStringSerializer().serialize(lockKey)); return null; } }); }
若是使用關係數據庫,一旦最早生成鎖的程序意外退出,鎖就可能永遠得不到釋放。Redis 的 EXPIRE
設置可確保在任何狀況下釋放鎖。mongodb
###3. 位屏蔽(Bit Mask) 假設 web 客戶端須要輪詢一臺 web 服務器,針對某個數據庫中的多個表查詢客戶指定更新內容。若是盲目地查詢全部相應的表以尋找潛在更新,成本較高。爲了不這一作法,能夠嘗試在 Redis 中給每一個客戶端保存一個整型做爲髒指標,整型的每一個數位表示一個表。該表中存在客戶所需更新時,設置數位。輪詢期間,不會觸發對錶的查詢,除非設置了相應數位。就獲取並將這樣的位屏蔽設置爲 STRING
而言,Redis 很是高效。數據庫
###4. 排行榜(Leaderboard) Redis 的 ZSET
數據結構爲遊戲玩家排行榜提供了簡潔的解決方案。ZSET
的工做方式有些相似於 Java 中的 PriorityQueue
,各個對象均爲通過排序的數據結構,層次分明。能夠按照分數排出遊戲玩家在排行榜上的位置。Redis 的 ZSET
定義了一分內容豐富的命令列表,支持靈活有效的查詢。例如,ZRANGE(包括 ZREVRANGE)可返回有序集內的指定範圍要素。
你可使用這一命令列出排行榜前 100 名玩家。ZRANGEBYSCORE 返回指定分數範圍內的要素(例如列出得分爲 1000 至 2000 之間的玩家),ZRNK 則返回有序集內的要素的排名,諸如此類。
###5. 布隆(Bloom)過濾器 布隆過濾器 (Bloom filter) 是一種空間利用率較高的機率數據結構,用來測試某元素是否某個集的一員。可能會出現誤報匹配,但不會漏報。查詢可返回「可能在集內」或「確定不在集內」。
就在線服務和離線服務包括大數據分析等方面,布隆過濾器數據結構都能派上不少用場。Facebook 利用布隆過濾器進行輸入提示搜索,爲用戶輸入的查詢提取朋友和朋友的朋友。Apache HBase 則利用布隆過濾器過濾掉不包含特殊行或列的 HFile 塊磁盤讀取,使讀取速度獲得明顯提高。Bitly 用布隆過濾器來避免將用戶重定向到惡意網站,而 Quara 則在訂閱後端執行了一個切分的布隆過濾器,用來過濾掉以前查看過的內容。在我本身的項目裏,我用布隆過濾器追蹤用戶對各個主題的投票狀況。
藉助出色的速度和處理能力,Redis 極好地融合了布隆過濾器。搜索 GitHub,就能發現不少 Redis 布隆過濾器項目,其中一些還支持可調諧精度。
###6. 高效的全局通知:發佈/訂閱渠道 Redis 發佈/訂閱渠道的工做方式相似於一個扇出消息傳遞系統,或 JMS 語義中的一個主題。JMS 主題和 Redis 發佈/訂閱渠道的一個區別是,經過 Redis 發佈的消息並不持久。消息被推送給全部相連的客戶端後,Redis 上就會刪除這一消息。換句話說,訂閱者必須一直在線才能接收新消息。Redis 發佈/訂閱渠道的典型用例包括實時配置分佈、簡單的聊天服務器等。
在 web 服務器集羣中,每一個節點均可以是 Redis 發佈/訂閱渠道的一個訂閱者。發佈到渠道上的消息也會被即時推送到全部相連節點。這一消息能夠是某種配置更改,也能夠是針對全部在線用戶的全局通知。和恆定輪詢相比,這種推送溝通模式顯然極爲高效。
##Redis 性能優化 Redis 很是強大,但也能夠從總體上和根據特定編程場景作出進一步優化。能夠考慮如下技巧。
###存活時間 全部 Redis 數據結構都具有存活時間 (TTL) 屬性。當你設置這一屬性時,數據結構會在過時後自動刪除。充分利用這一功能,可讓 Redis 保持較低的內存損耗。
###管道技術 在一條請求中向 Redis 發送多個命令,這種方法叫作管道技術。這一技術節省了網絡往返的成本,這一點很是重要,由於網絡延遲可能比 Redis 延遲要高上好幾個量級。但這裏存在一個陷阱:管道中的 Redis 命令列表必須預先肯定,而且應當彼此獨立。若是一個命令的參數是由先前命令的結果計算得出,管道技術就不起做用。列表 5 給出了 Redis 管道技術的一個示例。
列表 5:管道技術
@Override public List<LeaderboardEntry> fetchLeaderboard(String key, String... playerIds) { final List<LeaderboardEntry> entries = new ArrayList<>(); redisTemplate.executePipelined(new RedisCallback<Object>() { // enable Redis Pipeline @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { for(String playerId : playerIds) { Long rank = connection.zRevRank(key.getBytes(), playerId.getBytes()); Double score = connection.zScore(key.getBytes(), playerId.getBytes()); LeaderboardEntry entry = new LeaderboardEntry(playerId, score!=null?score.intValue():-1, rank!=null?rank.intValue():-1); entries.add(entry); } return null; } }); return entries; }
###副本集和切分 Redis 支持主從副本配置。和 MongoDB 同樣,副本集也是不對稱的,由於從節點是隻讀的,以便共享讀取工做量。我在文章開頭提到過,也能夠執行切分來橫向擴展 Redis 的處理能力和存儲容量。事實上,Redis 很是強大,據亞馬遜公司的內部基準顯示,類型 r3.4xlarge 的一個 EC2 實例每秒可輕鬆處理 100000 次請求。傳說還有把每秒 700000 次請求做爲基準的。對於中小型應用程序,一般無需考慮 Redis 切分。(請參見這篇很是出色的文章《運行中的 Redis》,進一步瞭解 Redis 的性能優化和切分。)
##Redis 中的事務 Redis 並不像關係數據庫管理系統那樣能支持全面的 ACID 事務,但其自有的事務也很是有效。從本質上來講,Redis 事務是管道、樂觀鎖、肯定提交和回滾的結合。其思想是執行一個管道中的一個命令列表,而後觀察某一關鍵記錄的潛在更新(樂觀鎖)。根據所觀察的記錄是否會被另外一個進程更新,該命令列表或總體肯定提交,或徹底回滾。
下面以某個拍賣網站上的賣方庫存爲例。買方試圖從賣方處購買某件商品時,你負責觀察 Redis 事務內的賣方庫存變化。同時,你要從同一個庫存中刪除此商品。事務關閉前,若是庫存被一個以上進程觸及(例如,若是兩個買方同時購買了同一件商品),事務將回滾,不然事務會肯定提交。回滾後可開始重試。
###Spring Data Redis 中的事務陷阱 我在 Spring 的 RedisTemplate
類 redisTemplate.setEnableTransactionSupport(true)
; 中啓用 Redis 事務時獲得一個慘痛的教訓:Redis 會在運行幾天後開始返回垃圾數據,致使數據嚴重損壞。StackOverflow 上也報道了相似狀況。
在運行一個 monitor
命令後,個人團隊發現,在進行 Redis 操做或 RedisCallback
後,Spring 並無自動關閉 Redis 鏈接,而事實上它是應該關閉的。若是再次使用未關閉的鏈接,可能會從意想不到的 Redis 密鑰返回垃圾數據。有意思的是,若是在 RedisTemplate
中把事務支持設爲 false,這一問題就不會出現了。
咱們發現,咱們能夠先在 Spring 語境裏配置一個 PlatformTransactionManager
(例如 DataSourceTransactionManager
),而後再用 @Transactional
註釋來聲明 Redis 事務的範圍,讓 Spring 自動關閉 Redis 鏈接。
根據這一經驗,咱們相信,在 Spring 語境裏配置兩個單獨的 RedisTemplate
是很好的作法:其中一個 RedisTemplates 的事務設爲 false,用於大多數 Redis 操做,另外一個 RedisTemplates 的事務已激活,僅用於 Redis 事務。固然必需要聲明 PlatformTransactionManager
和 @Transactional
,以防返回垃圾數值。
另外,咱們還發現了 Redis 事務和關係數據庫事務(在本例中,即 JDBC)相結合的不利之處。混合型事務的表現和預想的不太同樣。
##結論 我但願經過這篇文章向其餘 Java 企業開發師介紹 Redis 的強大之處,尤爲是將 Redis 用做遠程數據緩存和用於易揮發數據時。在這裏我介紹了 Redis 的六個有效用例,分享了一些性能優化技巧,還說明了個人 Glu Mobile 團隊怎樣解決了 Spring Data Redis 事務配置不當形成的垃圾數據問題。我但願這篇文章可以激發你對 Redis NoSQL 的好奇心,讓你可以受到啓發,在本身的 Java 企業版系統裏創造出一番天地。
本文系 OneAPM 工程師編譯整理。OneAPM 能爲您提供端到端的 Java 應用性能解決方案,咱們支持全部常見的 Java 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本緣由。分鐘級部署,即刻體驗,Java 監控歷來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客。
本文轉自 OneAPM 官方博客