Spring Data Redis 讓 NoSQL 快如閃電(2) 頂 原

【編者按】本文作者爲 Xinyu Liu,文章的第一部分重點概述了 Redis 方方面面的特性。在第二部分,將介紹詳細的用例。文章系國內 ITOM 管理平臺 OneAPM 編譯呈現。

##把 Redis 當作數據庫的用例 現在我們來看看在服務器端 Java 企業版系統中把 Redis 當作數據庫的各種用法吧。無論用例的簡繁,Redis 都能幫助用戶優化性能、處理能力和延遲,讓常規 Java 企業版技術棧望而卻步。

###1. 全局唯一增量計數器 我們先從一個相對簡單的用例開始吧:一個增量計數器,可顯示某網站受到多少次點擊。Spring Data Redis 有兩個適用於這一實用程序的類:RedisAtomicIntegerRedisAtomicLong。和 Java 併發包中的 AtomicIntegerAtomicLong 不同的是,這些 Spring 類能在多個 JVM 中發揮作用。

列表 3:全局唯一增量計數器

RedisAtomicLong counter = 
    new RedisAtomicLong("UNIQUE_COUNTER_NAME", redisTemplate.getConnectionFactory()); 
Long myCounter = counter.incrementAndGet();// return the incremented value

請注意整型溢出並謹記,在這兩個類上進行操作需要付出相對較高的代價。

###2. 全局悲觀鎖 時不時的,用戶就得應對服務器集羣的爭用。假設你從一個服務器集羣運行一個預定作業。在沒有全局鎖的情況下,集羣中的節點會發起冗餘作業實例。假設某個聊天室分區可容納 50 人。如果聊天室已滿,就需要創建新的聊天室實例來容納另外 50 人。

如果檢測到聊天室已滿但沒有全局鎖,集羣中的各個節點就會創建自有的聊天室實例,爲整個系統帶來不可預知的因素。列表 4 介紹了應當如何充分利用 SETNXSET if Not eXists:如果不存在,則設置)這一 Redis 命令來執行全局悲觀鎖。

列表4:全局悲觀鎖

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 設置可確保在任何情況下釋放鎖。

###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 的 RedisTemplateredisTemplate.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 官方博客

原文地址:http://www.javaworld.com/article/3062899/big-data/lightning-fast-nosql-with-spring-data-redis.html?page=2

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