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

【編者按】本文作者為 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. 全局唯一增量計(jì)數(shù)器

我們先從一個(gè)相對簡單的用例開始吧:一個(gè)增量計(jì)數(shù)器,可顯示某網(wǎng)站受到多少次點(diǎn)擊丧没。Spring Data Redis 有兩個(gè)適用于這一實(shí)用程序的類:RedisAtomicIntegerRedisAtomicLong鹰椒。和 Java 并發(fā)包中的 AtomicIntegerAtomicLong 不同的是,這些 Spring 類能在多個(gè) JVM 中發(fā)揮作用呕童。

列表 3:全局唯一增量計(jì)數(shù)器

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

請注意整型溢出并謹(jǐn)記漆际,在這兩個(gè)類上進(jìn)行操作需要付出相對較高的代價(jià)。

2. 全局悲觀鎖

時(shí)不時(shí)的夺饲,用戶就得應(yīng)對服務(wù)器集群的爭用奸汇。假設(shè)你從一個(gè)服務(wù)器集群運(yùn)行一個(gè)預(yù)定作業(yè)。在沒有全局鎖的情況下往声,集群中的節(jié)點(diǎn)會發(fā)起冗余作業(yè)實(shí)例擂找。假設(shè)某個(gè)聊天室分區(qū)可容納 50 人。如果聊天室已滿浩销,就需要?jiǎng)?chuàng)建新的聊天室實(shí)例來容納另外 50 人贯涎。

如果檢測到聊天室已滿但沒有全局鎖,集群中的各個(gè)節(jié)點(diǎn)就會創(chuàng)建自有的聊天室實(shí)例慢洋,為整個(gè)系統(tǒng)帶來不可預(yù)知的因素塘雳。列表 4 介紹了應(yīng)當(dāng)如何充分利用 SETNXSET if Not eXists:如果不存在,則設(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ù)器,針對某個(gè)數(shù)據(jù)庫中的多個(gè)表查詢客戶指定更新內(nèi)容。如果盲目地查詢所有相應(yīng)的表以尋找潛在更新盈包,成本較高。為了避免這一做法醇王,可以嘗試在 Redis 中給每個(gè)客戶端保存一個(gè)整型作為臟指標(biāo)呢燥,整型的每個(gè)數(shù)位表示一個(gè)表。該表中存在客戶所需更新時(shí)寓娩,設(shè)置數(shù)位叛氨。輪詢期間,不會觸發(fā)對表的查詢棘伴,除非設(shè)置了相應(yīng)數(shù)位寞埠。就獲取并將這樣的位屏蔽設(shè)置為 STRING 而言,Redis 非常高效焊夸。

4. 排行榜(Leaderboard)

Redis 的 ZSET 數(shù)據(jù)結(jié)構(gòu)為游戲玩家排行榜提供了簡潔的解決方案仁连。ZSET 的工作方式有些類似于 Java 中的 PriorityQueue,各個(gè)對象均為經(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)拓挥,用來測試某元素是否某個(gè)集的一員蒋畜。可能會出現(xiàn)誤報(bào)匹配撞叽,但不會漏報(bào)姻成。查詢可返回“可能在集內(nèi)”或“肯定不在集內(nèi)”。

就在線服務(wù)和離線服務(wù)包括大數(shù)據(jù)分析等方面愿棋,布隆過濾器數(shù)據(jù)結(jié)構(gòu)都能派上很多用場科展。Facebook 利用布隆過濾器進(jìn)行輸入提示搜索,為用戶輸入的查詢提取朋友和朋友的朋友糠雨。Apache HBase 則利用布隆過濾器過濾掉不包含特殊行或列的 HFile 塊磁盤讀取才睹,使讀取速度得到明顯提升。Bitly 用布隆過濾器來避免將用戶重定向到惡意網(wǎng)站,而 Quara 則在訂閱后端執(zhí)行了一個(gè)切分的布隆過濾器琅攘,用來過濾掉之前查看過的內(nèi)容垮庐。在我自己的項(xiàng)目里,我用布隆過濾器追蹤用戶對各個(gè)主題的投票情況坞琴。

借助出色的速度和處理能力哨查,Redis 極好地融合了布隆過濾器。搜索 GitHub剧辐,就能發(fā)現(xiàn)很多 Redis 布隆過濾器項(xiàng)目寒亥,其中一些還支持可調(diào)諧精度。

6. 高效的全局通知:發(fā)布/訂閱渠道

Redis 發(fā)布/訂閱渠道的工作方式類似于一個(gè)扇出消息傳遞系統(tǒng)荧关,或 JMS 語義中的一個(gè)主題溉奕。JMS 主題和 Redis 發(fā)布/訂閱渠道的一個(gè)區(qū)別是,通過 Redis 發(fā)布的消息并不持久忍啤。消息被推送給所有相連的客戶端后加勤,Redis 上就會刪除這一消息。換句話說同波,訂閱者必須一直在線才能接收新消息胸竞。Redis 發(fā)布/訂閱渠道的典型用例包括實(shí)時(shí)配置分布、簡單的聊天服務(wù)器等参萄。

在 web 服務(wù)器集群中卫枝,每個(gè)節(jié)點(diǎn)都可以是 Redis 發(fā)布/訂閱渠道的一個(gè)訂閱者。發(fā)布到渠道上的消息也會被即時(shí)推送到所有相連節(jié)點(diǎn)讹挎。這一消息可以是某種配置更改校赤,也可以是針對所有在線用戶的全局通知。和恒定輪詢相比筒溃,這種推送溝通模式顯然極為高效马篮。

Redis 性能優(yōu)化

Redis 非常強(qiáng)大,但也可以從整體上和根據(jù)特定編程場景做出進(jìn)一步優(yōu)化怜奖』氩猓可以考慮以下技巧。

存活時(shí)間

所有 Redis 數(shù)據(jù)結(jié)構(gòu)都具備存活時(shí)間 (TTL) 屬性歪玲。當(dāng)你設(shè)置這一屬性時(shí)迁央,數(shù)據(jù)結(jié)構(gòu)會在過期后自動刪除。充分利用這一功能滥崩,可以讓 Redis 保持較低的內(nèi)存損耗岖圈。

管道技術(shù)

在一條請求中向 Redis 發(fā)送多個(gè)命令,這種方法叫做管道技術(shù)钙皮。這一技術(shù)節(jié)省了網(wǎng)絡(luò)往返的成本蜂科,這一點(diǎn)非常重要顽决,因?yàn)榫W(wǎng)絡(luò)延遲可能比 Redis 延遲要高上好幾個(gè)量級。但這里存在一個(gè)陷阱:管道中的 Redis 命令列表必須預(yù)先確定导匣,并且應(yīng)當(dāng)彼此獨(dú)立才菠。如果一個(gè)命令的參數(shù)是由先前命令的結(jié)果計(jì)算得出,管道技術(shù)就不起作用贡定。列表 5 給出了 Redis 管道技術(shù)的一個(gè)示例赋访。

列表 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 的一個(gè) 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í)行一個(gè)管道中的一個(gè)命令列表厚宰,然后觀察某一關(guān)鍵記錄的潛在更新(樂觀鎖)。根據(jù)所觀察的記錄是否會被另一個(gè)進(jìn)程更新遂填,該命令列表或整體確定提交铲觉,或完全回滾。

下面以某個(gè)拍賣網(wǎng)站上的賣方庫存為例吓坚。買方試圖從賣方處購買某件商品時(shí)备燃,你負(fù)責(zé)觀察 Redis 事務(wù)內(nèi)的賣方庫存變化。同時(shí)凌唬,你要從同一個(gè)庫存中刪除此商品并齐。事務(wù)關(guān)閉前漏麦,如果庫存被一個(gè)以上進(jìn)程觸及(例如,如果兩個(gè)買方同時(shí)購買了同一件商品)况褪,事務(wù)將回滾撕贞,否則事務(wù)會確定提交〔舛猓回滾后可開始重試捏膨。

Spring Data Redis 中的事務(wù)陷阱

我在 Spring 的 RedisTemplateredisTemplate.setEnableTransactionSupport(true); 中啟用 Redis 事務(wù)時(shí)得到一個(gè)慘痛的教訓(xùn):Redis 會在運(yùn)行幾天后開始返回垃圾數(shù)據(jù),導(dǎo)致數(shù)據(jù)嚴(yán)重?fù)p壞食侮。StackOverflow 上也報(bào)道了類似情況号涯。

在運(yùn)行一個(gè) 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 語境里配置一個(gè) PlatformTransactionManager(例如 DataSourceTransactionManager)丝蹭,然后再用 @Transactional 注釋來聲明 Redis 事務(wù)的范圍,讓 Spring 自動關(guān)閉 Redis 連接坪蚁。

根據(jù)這一經(jīng)驗(yàn)奔穿,我們相信,在 Spring 語境里配置兩個(gè)單獨(dú)的 RedisTemplate 是很好的做法:其中一個(gè) RedisTemplates 的事務(wù)設(shè)為 false迅细,用于大多數(shù) Redis 操作巫橄,另一個(gè) 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ù)時(shí)结澄。在這里我介紹了 Redis 的六個(gè)有效用例哥谷,分享了一些性能優(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ù)文章,請?jiān)L問 OneAPM 官方技術(shù)博客刮刑。

本文轉(zhuǎn)自 OneAPM 官方博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喉祭,一起剝皮案震驚了整個(gè)濱河市养渴,隨后出現(xiàn)的幾起案子雷绢,更是在濱河造成了極大的恐慌,老刑警劉巖理卑,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翘紊,死亡現(xiàn)場離奇詭異,居然都是意外死亡藐唠,警方通過查閱死者的電腦和手機(jī)帆疟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宇立,“玉大人踪宠,你說我怎么就攤上這事÷栲冢” “怎么了柳琢?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長润脸。 經(jīng)常有香客問我柬脸,道長,這世上最難降的妖魔是什么毙驯? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任倒堕,我火速辦了婚禮,結(jié)果婚禮上爆价,老公的妹妹穿的比我還像新娘垦巴。我一直安慰自己媳搪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布骤宣。 她就那樣靜靜地躺著蛾号,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涯雅。 梳的紋絲不亂的頭發(fā)上鲜结,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天,我揣著相機(jī)與錄音活逆,去河邊找鬼精刷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蔗候,可吹牛的內(nèi)容都是我干的怒允。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼锈遥,長吁一口氣:“原來是場噩夢啊……” “哼纫事!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起所灸,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤丽惶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后爬立,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钾唬,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年侠驯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抡秆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吟策,死狀恐怖儒士,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情檩坚,我是刑警寧澤着撩,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站效床,受9級特大地震影響睹酌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剩檀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一憋沿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沪猴,春花似錦辐啄、人聲如沸采章。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悯舟。三九已至帚稠,卻和暖如春潦嘶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玫坛。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工岭参, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留反惕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓演侯,卻偏偏與公主長得像姿染,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子秒际,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 【編者按】本文作者為 Xinyu Liu悬赏,文章的第一部分重點(diǎn)概述了 Redis 方方面面的特性。在第二部分娄徊,將介紹...
    OneAPM_Official閱讀 46,249評論 0 7
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理闽颇,服務(wù)發(fā)現(xiàn),斷路器嵌莉,智...
    卡卡羅2017閱讀 134,716評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,867評論 6 342
  • 二十年來太匆忙 流連顧盼慕紅妝 情思輾轉(zhuǎn)自徜徉 大夢覺時(shí)淚兩行 繁花落盡獨(dú)彷徨 可憐癡人空神傷 舉杯痛飲更張狂 最...
    第貳雨樹霖風(fēng)閱讀 251評論 0 1
  • 平時(shí)不要太計(jì)較进萄,不要刻薄捻脖。
    文晟閱讀 275評論 0 0