《Spring Data Redis 讓 NoSQL 快如閃電》要點(diǎn):
本文介紹了Spring Data Redis 讓 NoSQL 快如閃電,希望對您有用。如果有疑問,可以聯(lián)系我們。
【編者按】本文作者為 Xinyu Liu,文章的第一部分重點(diǎn)概述了 Redis 方方面面的特性.在第二部分,將介紹詳細(xì)的用例.文章系國內(nèi) ITOM 管理平臺 OneAPM 編譯呈現(xiàn).
把 Redis 當(dāng)作數(shù)據(jù)庫的用例
現(xiàn)在我們來看看在服務(wù)器端 Java 企業(yè)版系統(tǒng)中把 Redis 當(dāng)作數(shù)據(jù)庫的各種用法吧.無論用例的簡繁,Redis 都能幫助用戶優(yōu)化性能、處理能力和延遲,讓常規(guī) Java 企業(yè)版技術(shù)棧望而卻步.
1. 全局唯一增量計數(shù)器
我們先從一個相對簡單的用例開始吧:一個增量計數(shù)器,可顯示某網(wǎng)站受到多少次點(diǎn)擊.Spring Data Redis 有兩個適用于這一實(shí)用程序的類:RedisAtomicInteger
和 RedisAtomicLong
.和 Java 并發(fā)包中的AtomicInteger
和 AtomicLong
不同的是,這些 Spring 類能在多個 JVM 中發(fā)揮作用.
列表 3:全局唯一增量計數(shù)器
RedisAtomicLong counter =
new RedisAtomicLong("UNIQUE_COUNTER_NAME", redisTemplate.getConnectionFactory());
Long myCounter = counter.incrementAndGet();// return the incremented value
請注意整型溢出并謹(jǐn)記,在這兩個類上進(jìn)行操作需要付出相對較高的代價.
2. 全局悲觀鎖
時不時的,用戶就得應(yīng)對服務(wù)器集群的爭用.假設(shè)你從一個服務(wù)器集群運(yùn)行一個預(yù)定作業(yè).在沒有全局鎖的情況下,集群中的節(jié)點(diǎn)會發(fā)起冗余作業(yè)實(shí)例.假設(shè)某個聊天室分區(qū)可容納 50 人.如果聊天室已滿,就需要創(chuàng)建新的聊天室實(shí)例來容納另外 50 人.
如果檢測到聊天室已滿但沒有全局鎖,集群中的各個節(jié)點(diǎn)就會創(chuàng)建自有的聊天室實(shí)例,為整個系統(tǒng)帶來不可預(yù)知的因素.列表 4 介紹了應(yīng)當(dāng)如何充分利用 SETNX(SET if **N**ot e**X**ists:如果不存在,則設(shè)置)這一 Redis 命令來執(zhí)行全局悲觀鎖.
列表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;
}
});
}
如果使用關(guān)系數(shù)據(jù)庫,一旦最先生成鎖的程序意外退出,鎖就可能永遠(yuǎn)得不到釋放.Redis 的 EXPIRE
設(shè)置可確保在任何情況下釋放鎖.
3. 位屏蔽(Bit Mask)
假設(shè) web 客戶端需要輪詢一臺 web 服務(wù)器,針對某個數(shù)據(jù)庫中的多個表查詢客戶指定更新內(nèi)容.如果盲目地查詢所有相應(yīng)的表以尋找潛在更新,成本較高.為了避免這一做法,可以嘗試在 Redis 中給每個客戶端保存一個整型作為臟指標(biāo),整型的每個數(shù)位表示一個表.該表中存在客戶所需更新時,設(shè)置數(shù)位.輪詢期間,不會觸發(fā)對表的查詢,除非設(shè)置了相應(yīng)數(shù)位.就獲取并將這樣的位屏蔽設(shè)置為 STRING
而言,Redis 非常高效.
4. 排行榜(Leaderboard)
Redis 的 ZSET
數(shù)據(jù)結(jié)構(gòu)為游戲玩家排行榜提供了簡潔的解決方案.ZSET
的工作方式有些類似于 Java 中的 PriorityQueue
,各個對象均為經(jīng)過排序的數(shù)據(jù)結(jié)構(gòu),井井有條.可以依照分?jǐn)?shù)排出游戲玩家在排行榜上的位置.Redis 的 ZSET
定義了一份內(nèi)容豐富的命令列表,支持靈活有效的查詢.例如,ZRANGE(包括 ZREVRANGE)可返回有序集內(nèi)的指定范圍要素.
你可以使用這一命令列出排行榜前 100 名玩家.ZRANGEBYSCORE 返回指定分?jǐn)?shù)范圍內(nèi)的要素(例如列出得分為 1000 至 2000 之間的玩家),ZRNK 則返回有序集內(nèi)的要素的排名,諸如此類.
5. 布隆(Bloom)過濾器
布隆過濾器 (Bloom filter) 是一種空間利用率較高的概率數(shù)據(jù)結(jié)構(gòu),用來測試某元素是否某個集的一員.可能會出現(xiàn)誤報匹配,但不會漏報.查詢可返回“可能在集內(nèi)”或“肯定不在集內(nèi)”.
就在線服務(wù)和離線服務(wù)包括大數(shù)據(jù)分析等方面,布隆過濾器數(shù)據(jù)結(jié)構(gòu)都能派上很多用場.Facebook 利用布隆過濾器進(jìn)行輸入提示搜索,為用戶輸入的查詢提取朋友和朋友的朋友.Apache HBase 則利用布隆過濾器過濾掉不包含特殊行或列的 HFile 塊磁盤讀取,使讀取速度得到明顯提升.Bitly 用布隆過濾器來避免將用戶重定向到惡意網(wǎng)站,而 Quara 則在訂閱后端執(zhí)行了一個切分的布隆過濾器,用來過濾掉之前查看過的內(nèi)容.在我自己的項(xiàng)目里,我用布隆過濾器追蹤用戶對各個主題的投票情況.
借助出色的速度和處理能力,Redis 極好地融合了布隆過濾器.搜索 GitHub,就能發(fā)現(xiàn)很多 Redis 布隆過濾器項(xiàng)目,其中一些還支持可調(diào)諧精度.
6. 高效的全局通知:發(fā)布/訂閱渠道
Redis 發(fā)布/訂閱渠道的工作方式類似于一個扇出消息傳遞系統(tǒng),或 JMS 語義中的一個主題.JMS 主題和 Redis 發(fā)布/訂閱渠道的一個區(qū)別是,通過 Redis 發(fā)布的消息并不持久.消息被推送給所有相連的客戶端后,Redis 上就會刪除這一消息.換句話說,訂閱者必須一直在線才能接收新消息.Redis 發(fā)布/訂閱渠道的典型用例包括實(shí)時配置分布、簡單的聊天服務(wù)器等.
在 web 服務(wù)器集群中,每個節(jié)點(diǎn)都可以是 Redis 發(fā)布/訂閱渠道的一個訂閱者.發(fā)布到渠道上的消息也會被即時推送到所有相連節(jié)點(diǎn).這一消息可以是某種配置更改,也可以是針對所有在線用戶的全局通知.和恒定輪詢相比,這種推送溝通模式顯然極為高效.
Redis 性能優(yōu)化
Redis 非常強(qiáng)大,但也可以從整體上和根據(jù)特定編程場景做出進(jìn)一步優(yōu)化.可以考慮以下技巧.
存活時間
所有 Redis 數(shù)據(jù)結(jié)構(gòu)都具備存活時間 (TTL) 屬性.當(dāng)你設(shè)置這一屬性時,數(shù)據(jù)結(jié)構(gòu)會在過期后自動刪除.充分利用這一功能,可以讓 Redis 保持較低的內(nèi)存損耗.
管道技術(shù)
在一條哀求中向 Redis 發(fā)送多個命令,這種方法叫做管道技術(shù).這一技術(shù)節(jié)省了網(wǎng)絡(luò)往返的成本,這一點(diǎn)非常重要,因?yàn)榫W(wǎng)絡(luò)延遲可能比 Redis 延遲要高上好幾個量級.但這里存在一個陷阱:管道中的 Redis 命令列表必須預(yù)先確定,并且應(yīng)當(dāng)彼此獨(dú)立.如果一個命令的參數(shù)是由先前命令的結(jié)果計算得出,管道技術(shù)就不起作用.列表 5 給出了 Redis 管道技術(shù)的一個示例.
列表 5:管道技術(shù)
@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 一樣,副本集也是不對稱的,因?yàn)閺墓?jié)點(diǎn)是只讀的,以便共享讀取工作量.我在文章開頭提到過,也可以執(zhí)行切分來橫向擴(kuò)展 Redis 的處理能力和存儲容量.事實(shí)上,Redis 非常強(qiáng)大,據(jù)亞馬遜公司的內(nèi)部基準(zhǔn)顯示,類型 r3.4xlarge 的一個 EC2 實(shí)例每秒可輕松處理 100000 次哀求.傳說還有把每秒 700000 次哀求作為基準(zhǔn)的.對于中小型應(yīng)用程序,通常無需考慮 Redis 切分.(請參見這篇非常出色的文章《運(yùn)行中的 Redis》,進(jìn)一步了解 Redis 的性能優(yōu)化和切分.)
Redis 中的事務(wù)
Redis 并不像關(guān)系數(shù)據(jù)庫管理系統(tǒng)那樣能支持全面的 ACID 事務(wù),但其自有的事務(wù)也非常有效.從本質(zhì)上來說,Redis 事務(wù)是管道、樂觀鎖、確定提交和回滾的結(jié)合.其思想是執(zhí)行一個管道中的一個命令列表,然后觀察某一關(guān)鍵記錄的潛在更新(樂觀鎖).根據(jù)所觀察的記錄是否會被另一個進(jìn)程更新,該命令列表或整體確定提交,或完全回滾.
下面以某個拍賣網(wǎng)站上的賣方庫存為例.買方試圖從賣方處購買某件商品時,你負(fù)責(zé)觀察 Redis 事務(wù)內(nèi)的賣方庫存變化.同時,你要從同一個庫存中刪除此商品.事務(wù)關(guān)閉前,如果庫存被一個以上進(jìn)程觸及(例如,如果兩個買方同時購買了同一件商品),事務(wù)將回滾,否則事務(wù)會確定提交.回滾后可開始重試.
Spring Data Redis 中的事務(wù)陷阱
我在 Spring 的 RedisTemplate
類 redisTemplate.setEnableTransactionSupport(true);
中啟用 Redis 事務(wù)時得到一個慘痛的教訓(xùn):Redis 會在運(yùn)行幾天后開始返回垃圾數(shù)據(jù),導(dǎo)致數(shù)據(jù)嚴(yán)重?fù)p壞.StackOverflow上也報道了類似情況.
在運(yùn)行一個 monitor
命令后,我的團(tuán)隊(duì)發(fā)現(xiàn),在進(jìn)行 Redis 操作或 RedisCallback
后,Spring 并沒有自動關(guān)閉 Redis 連接,而事實(shí)上它是應(yīng)該關(guān)閉的.如果再次使用未關(guān)閉的連接,可能會從意想不到的 Redis 密鑰返回垃圾數(shù)據(jù).有意思的是,如果在 RedisTemplate
中把事務(wù)支持設(shè)為 false,這一問題就不會出現(xiàn)了.
我們發(fā)現(xiàn),我們可以先在 Spring 語境里配置一個 PlatformTransactionManager
(例如DataSourceTransactionManager
),然后再用 @Transactional
注釋來聲明 Redis 事務(wù)的范圍,讓 Spring 自動關(guān)閉 Redis 連接.
根據(jù)這一經(jīng)驗(yàn),我們相信,在 Spring 語境里配置兩個單獨(dú)的 RedisTemplate
是很好的做法:其中一個 RedisTemplates 的事務(wù)設(shè)為 false,用于大多數(shù) Redis 操作,另一個 RedisTemplates 的事務(wù)已激活,僅用于 Redis 事務(wù).當(dāng)然必須要聲明 PlatformTransactionManager
和 @Transactional
,以防返回垃圾數(shù)值.
另外,我們還發(fā)現(xiàn)了 Redis 事務(wù)和關(guān)系數(shù)據(jù)庫事務(wù)(在本例中,即 JDBC)相結(jié)合的不利之處.混合型事務(wù)的表現(xiàn)和預(yù)想的不太一樣.
結(jié)論
我希望通過這篇文章向其他 Java 企業(yè)開發(fā)師介紹 Redis 的強(qiáng)大之處,尤其是將 Redis 用作遠(yuǎn)程數(shù)據(jù)緩存和用于易揮發(fā)數(shù)據(jù)時.在這里我介紹了 Redis 的六個有效用例,分享了一些性能優(yōu)化技巧,還說明了我的 Glu Mobile 團(tuán)隊(duì)怎樣解決了 Spring Data Redis 事務(wù)配置不當(dāng)造成的垃圾數(shù)據(jù)問題.我希望這篇文章能夠激發(fā)你對 Redis NoSQL 的好奇心,讓你能夠受到啟發(fā),在自己的 Java 企業(yè)版系統(tǒng)里創(chuàng)造出一番天地.
本文系 OneAPM 工程師編譯整理.OneAPM 能為您提供端到端的 Java 應(yīng)用性能解決方案,我們支持所有常見的 Java 框架及應(yīng)用服務(wù)器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸,定位異常根本原因.分鐘級部署,即刻體驗(yàn),Java 監(jiān)控從來沒有如此簡單.想閱讀更多技術(shù)文章,請拜訪 OneAPM 官方技術(shù)博客.
《Spring Data Redis 讓 NoSQL 快如閃電》是否對您有啟發(fā),歡迎查看更多與《Spring Data Redis 讓 NoSQL 快如閃電》相關(guān)教程,學(xué)精學(xué)透。維易PHP學(xué)院為您提供精彩教程。
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/9239.html