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

來源:互聯網
上載者:User

標籤:

【編者按】本文作者為 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 介紹了應當如何充分利用 SETNX(SET if **N**ot e**X**ists:如果不存在,則設定)這一 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 用戶端需要輪詢一台 網頁伺服器,針對某個資料庫中的多個表查詢客戶指定更新內容。如果盲目地查詢所有相應的表以尋找潛在更新,成本較高。為了避免這一做法,可以嘗試在 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 發布/訂閱渠道的典型用例包括即時配置分布、簡單的聊天伺服器等。

在 網頁伺服器叢集中,每個節點都可以是 Redis 發布/訂閱渠道的一個訂閱者。發布到渠道上的訊息也會被即時推送到所有相連節點。這一訊息可以是某種配置更改,也可以是針對所有線上使用者的全域通知。和恒定輪詢相比,這種推送溝通模式顯然極為高效。

Redis 效能最佳化

Redis 非常強大,但也可以從整體上和根據特定編程情境做出進一步最佳化。可以考慮以下技巧。

存活時間

所有 Redis 資料結構都具備存活時間 (TTL) 屬性。當你設定這一屬性時,資料結構會在到期後自動刪除。充分利用這一功能,可以讓 Redis 保持較低的記憶體損耗。

管道技術

在一條請求中向 Redis 發送多個命令,這種方法叫做管道技術。這一技術節省了網路往返的成本,這一點非常重要,因為網路延遲可能比 Redis 延遲要高上好幾個量級。但這裡存在一個陷阱:管道中的 Redis 命令列表必須預先確定,並且應當彼此獨立。如果一個命令的參數是由先前命令的結果計算得出,管道技術就不起作用。列表 5 給出了 Redis 管道技術的一個樣本。

列表 5:管道技術

@Overridepublic 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

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.