Java開發(fā)利器Guava Cache之使用篇

前言

提到緩存鱼冀,可能第一時間想到的就是Redis、Memcache等任连,這些都屬于是分布式緩存蚤吹,而在某些場景下我們可能并不需要分布式緩存,畢竟需要多引入維護(hù)一個中間件随抠,那么在數(shù)據(jù)量小裁着,且訪問頻繁,或者說一些不會變的靜態(tài)配置數(shù)據(jù)我們都可以考慮放置到本地緩存中拱她,那么我們平時是怎么做的呢二驰?相信大家在寫或者在讀有關(guān)本地緩存代碼時,都會看到如下實(shí)現(xiàn)方式:

private static final Map<K,V> LOCAL_CACHE = new ConcurrentHashMap<>();

的確這種方式簡單有效秉沼,但是帶來的弊端就是過于簡單桶雀,功能也就過于缺乏矿酵,而且如果使用不大,將帶來可怕的內(nèi)存溢出矗积,比如談起緩存全肮,那不得不提緩存淘汰策略緩存過期策略等棘捣,但是不要著急辜腺,強(qiáng)大的Guava工具庫已經(jīng)為我們提供了簡單有效的Guava Cache。

值得注意的是乍恐,請不要被強(qiáng)大的Guava Cache迷惑雙眼评疗,如果你的緩存場景用不到這些緩存的特性,那么ConcurrentHashMap或許是你最好的選擇

Guava Cache

官方地址:https://github.com/google/guava/wiki/CachesExplained

Guava Cache能力一覽

入門使用

key對應(yīng)的緩存值計算方式

緩存無非可能就是緩存那些耗時很長的計算(除了CPU型任務(wù)茵烈,I/O型也算)出來值壤巷,只有第一次從緩存中訪問指定key時,才會進(jìn)行真正的計算瞧毙,那么Guava Cache就提供三種緩存計算方式胧华,你也可以理解為緩存加載方式,它們分別是CacheLoader宙彪、Callable矩动、直接插入

CacheLoader

CacheLoader方式释漆,簡單點(diǎn)說就是計算方式作用于所有key悲没,也就是說通過CacheLoader方法創(chuàng)建的Cache,不管你訪問哪個key男图,它的計算方式都是同一個示姿,來看示例:

@Test
public void guavaCacheTest001(){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(2)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"真正計算了!");
                return "cache-"+key;
            }
        });

    System.out.println(loadingCache.getUnchecked("key1"));
    System.out.println(loadingCache.getUnchecked("key1"));

    System.out.println(loadingCache.getUnchecked("key2"));
    System.out.println(loadingCache.getUnchecked("key2"));
}

對應(yīng)輸出:

key1真正計算了逊笆!
cache-key1
cache-key1
key2真正計算了栈戳!
cache-key2
cache-key2

在這個例子中,我們通過給CacheBuilder的build方法傳入一個CacheLoad的匿名類难裆,該CacheLoad的load方法邏輯為當(dāng)獲取某個緩存key時子檀,如果該key緩存中不存在,則將計算其緩存值的計算方式乃戈。從輸出我們可以看到褂痰,只有緩存第一次訪問時才真正執(zhí)行了值的計算行為,并且每個緩存key的計算方式都一樣症虑。

Callable

當(dāng)對CacheLoader有了認(rèn)識之后缩歪,你可能會想:如果我針對不同的緩存key的計算緩存值方式并不一樣,那該怎么辦暗尽匪蝙!苟翻,別急,Callable為你保駕護(hù)航:

@Test
public void testCallable() throws ExecutionException {
    Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
    Object cacheKey1 = cache.get("key1", () -> {
        System.out.println("key1真正計算了");
        return "key1計算方式1";
    });
    System.out.println(cacheKey1);

    cacheKey1 = cache.get("key1",()->{
        System.out.println("key1真正計算了");
        return "key1計算方式1";
    });
    System.out.println(cacheKey1);

    Object cacheKey2 = cache.get("key2", () -> {
        System.out.println("key1真正計算了");
        return "key1計算方式2";
    });
    System.out.println(cacheKey2);

    cacheKey2 = cache.get("key2",()->{
        System.out.println("key1真正計算了");
        return "key1計算方式2";
    });
    System.out.println(cacheKey2);
}

輸出:
key1真正計算了
key1計算方式1
key1計算方式1
key1真正計算了
key1計算方式2
key1計算方式2

從例子中可以看到骗污,在調(diào)用get的時候,可以傳入一個Callable來為此緩存key提供專門的緩存值計算方式沈条。

直接插入

這種方式計算緩存值的邏輯不再由Guava Cache管理需忿,而是調(diào)用方可以調(diào)用put(key,value) 直接將要緩存的值插入。

@Test
public void testDirectInsert() throws ExecutionException {
    Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
    cache.put("key1","cache-key1");
    System.out.println(cache.get("key1",()->"callable cache-key1"));
}

輸出:
cache-key1

緩存淘汰機(jī)制

有一個殘酷的事實(shí)就是蜡歹,往往我們沒有那么大的內(nèi)存去支撐我們的緩存屋厘,所以我們必須有效的利用起來我們這昂貴的內(nèi)存,即針對那些不常用的緩存及時剔除月而,那么Guava Cache為我們提供了三種緩存剔除機(jī)制:基于大小剔除汗洒、基于緩存時間剔除、基于引用剔除父款。

基于大小剔除

這里并不是指占用緩存大小溢谤,而是指緩存條目的數(shù)量,當(dāng)緩存key的數(shù)量達(dá)到指定數(shù)量時憨攒,將按照LRU針對緩存key進(jìn)行剔除世杀。

@Test
public void testSizeBasedEviction(){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(3)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"真正計算了");
                return "cached-" + key;
            }
        });

    System.out.println("第一次訪問");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("第二次訪問");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("開始剔除");
    loadingCache.getUnchecked("key4");

    System.out.println("第三次訪問");
    loadingCache.getUnchecked("key3");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key1");
}

輸出:
第一次訪問
key1真正計算了
key2真正計算了
key3真正計算了
第二次訪問
開始剔除
key4真正計算了
第三次訪問
key1真正計算了

在上面這個例子中,設(shè)置了最大緩存條目為3肝集,然后依次添加了三個緩存項瞻坝,并且依次進(jìn)行了訪問,可以看到當(dāng)?shù)谝淮卧L問時杏瞻,由于緩存中都沒值所刀,因此進(jìn)行了計算,第二次訪問時捞挥,由于緩存中都有值所以直接從緩存讀取浮创,到了開始剔除階段時,此時嘗試獲取之前沒訪問過的key4砌函,而由于最大緩存條目為3蒸矛,所以此時需要從緩存中剔除掉一個值,那么剔除誰呢胸嘴?遵循LRU算法雏掠,key1是最近最不常不使用的,所以剔除的就是key1了劣像,從我們第三次訪問輸出的結(jié)果就可以驗證乡话。

注意:如果maximumSize傳入0,則所有key都將不進(jìn)行緩存耳奕!

除了maximumSize指定緩存key最大數(shù)量绑青,也可以通過maximumWeight指定最大權(quán)重诬像,就是說,每個緩存的key都需要返回一個權(quán)重闸婴,如果所有緩存的key的權(quán)重之和大于了我們指定的最大權(quán)重瓜浸,那么將執(zhí)行LRU淘汰策略:

@Test
public void testWeightBasedEviction(){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumWeight(6).weigher((key,value)->{
        if (key.equals("key1")){
            return 1;
        }
        if (key.equals("key2")){
            return 2;
        }
        if (key.equals("key3")){
            return 3;
        }

        if (key.equals("key4")){
            return 1;
        }
        return 0;
    })
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"真正計算了");
                return "cached-" + key;
            }
        });

    System.out.println("第一次訪問");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("第二次訪問");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("開始剔除");
    loadingCache.getUnchecked("key4");
    loadingCache.getUnchecked("key3");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key1");
}

輸出:
第一次訪問
key1真正計算了
key2真正計算了
key3真正計算了
第二次訪問
開始剔除
key4真正計算了
key1真正計算了

這個就不多解釋了吧,自己根據(jù)輸出想想...

基于時間剔除

Guava Cache針對CacheBuilder提供了兩個方法:expireAfterAccess(long, TimeUnit)expireAfterWrite(long, TimeUnit)

  • expireAfterAccess
    顧名思義固灵,當(dāng)某個緩存key自最后一次訪問(讀取或者寫入)超過指定時間后磷杏,那么這個緩存key將失效。
@Test
public void testExpiredAfterAccess() throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterAccess(3,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"真正計算了");
                return "cached-" + key;
            }
        });

    System.out.println("第一次訪問(寫入)");
    loadingCache.getUnchecked("key1");

    System.out.println("第二次訪問");
    loadingCache.getUnchecked("key1");

    TimeUnit.SECONDS.sleep(3);
    System.out.println("過3秒后訪問");
    loadingCache.getUnchecked("key1");
}

輸出:
第一次訪問(寫入)
key1真正計算了
第二次訪問
過3秒后訪問
key1真正計算了

這個例子中庇楞,我們設(shè)置了緩存自最近一次訪問(或?qū)懭耄┏^3秒后榜配,將失效,通過輸出也可以看到確實(shí) 如此吕晌。

  • expireAfterWrite
    顧名思義蛋褥,當(dāng)緩存key自最近一次寫入(注意,這就是和expireAfterAccess的區(qū)別睛驳,expireAfterWrite強(qiáng)調(diào)寫烙心,不關(guān)心讀)超過一定時間則過期剔除:
@Test
public void testExpiredAfterWrite() throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterWrite(3,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"真正計算了");
                return "cached-" + key;
            }
        });
    for (int i = 0; i < 4; i++) {
        System.out.println(new Date());
        loadingCache.getUnchecked("key1"); //首次執(zhí)行的時候,為寫入
        TimeUnit.SECONDS.sleep(1);
    }
}

輸出:
Sat Oct 02 20:06:47 CST 2021
key1真正計算了
Sat Oct 02 20:06:48 CST 2021
Sat Oct 02 20:06:49 CST 2021
Sat Oct 02 20:06:50 CST 2021
key1真正計算了

同樣乏沸,這里根據(jù)程序和輸出應(yīng)該可以理解啦弃理!

基于引用剔除

Java有四大引用,強(qiáng)屎蜓、軟痘昌、弱、虛炬转、如果對這幾個引用不是很了解的可以先去看看我這篇文章:??Java四種引用類型:強(qiáng)辆苔、軟、弱扼劈、虛

Guava Cache提供了基于引用的剔除策略驻啤,看到這里,你是否想起來了ThreadLocal如何防止內(nèi)存泄露呢荐吵?骑冗,如果不知道沒關(guān)系,繼續(xù)看我上面貼的引用文章先煎。Guava Cache提供了三種基于引用剔除的策略:

  • CacheBuilder.weakKeys()
    當(dāng)我們使用了weakKeys() 后贼涩,Guava cache將以弱引用 的方式去存儲緩存key,那么根據(jù)弱引用的定義:當(dāng)發(fā)生垃圾回收時,不管當(dāng)前系統(tǒng)資源是否充足薯蝎,弱引用都會被回收遥倦,直接上例子:
@Test
public void testWeakKeys() throws InterruptedException {
    LoadingCache<MyKey, String> loadingCache = CacheBuilder.newBuilder().weakKeys()
        .build(new CacheLoader<MyKey, String>() {
            @Override
            public String load(MyKey key) throws Exception {
                System.out.println(key.getKey()+"真正計算了");
                return "cached-" + key.getKey();
            }
        });

    MyKey key = new MyKey("key1");
    System.out.println("第一次訪問");
    loadingCache.getUnchecked(key);
    System.out.println(loadingCache.asMap());

    System.out.println("第二次訪問");
    loadingCache.getUnchecked(key);
    System.out.println(loadingCache.asMap());

    System.out.println("key失去強(qiáng)引用GC后訪問");
    key = null;
    System.gc();
    TimeUnit.SECONDS.sleep(3);
    System.out.println(loadingCache.asMap());

}

@Data
private static class MyKey{
    String key;

    public MyKey(String key) {
        this.key = key;
    }
}
  • CacheBuilder.weakValues()
    有了CacheBuilder.weakKeys()的基礎(chǔ),CacheBuilder.weakValues()的作用想必照貓畫虎應(yīng)該也知道了吧占锯?換湯不換藥袒哥,這次針對的是緩存值缩筛!

  • CacheBuilder.softValues()
    有了CacheBuilder.weakValues()的基礎(chǔ),CacheBuilder.softValues()的作用相比照貓畫虎應(yīng)該也知道了吧堡称?對瞎抛,你真棒,就是之前的弱引用換為了軟引用却紧,軟引用相比弱引用桐臊,被回收的條件就苛刻點(diǎn):當(dāng)發(fā)生垃圾回收時,只有當(dāng)系統(tǒng)資源不足時啄寡,才會回收!哩照。

主動剔除

上面講了被動剔除策略挺物,那么除了被動,我們也可以主動調(diào)用方法去清除緩存飘弧。

  • Cache.invalidate(key)
  • Cache.invalidateAll(keys)
  • Cache.invalidateAll()

緩存失效監(jiān)聽器

有時候我們希望當(dāng)緩存失效被剔除的時候识藤,可以做一些善后事情,此時次伶,我們就可以通過CacheBuilder.removalListener(RemovalListener) 來指定一個緩存失效監(jiān)聽器痴昧,當(dāng)緩存失效時,將回調(diào)我們的監(jiān)聽器:

@Test
public void testRemovalListener() throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().removalListener(notification -> {
        System.out.println(String
            .format("緩存 %s 因為 %s 失效了冠王,它的value是%s", notification.getKey(), notification.getCause(),
                notification.getValue()));
    }).expireAfterAccess(3, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            System.out.println(key + "真正計算了");
            return "cached-" + key;
        }
    });

    System.out.println("第一次訪問(寫入)");
    loadingCache.getUnchecked("key1");

    System.out.println("第二次訪問");
    loadingCache.getUnchecked("key1");
    TimeUnit.SECONDS.sleep(3);

    System.out.println("3秒后");
    loadingCache.getUnchecked("key1");
}

輸出:
第一次訪問(寫入)
key1真正計算了
第二次訪問
3秒后
緩存 key1 因為 EXPIRED 失效了赶撰,它的value是cached-key1
key1真正計算了

Guava Cache什么進(jìn)行清理動作?

這個其實(shí)在上節(jié)實(shí)驗緩存剔除監(jiān)聽器的時候我就發(fā)現(xiàn)一個問題:如果緩存失效后柱彻,我不再進(jìn)行任何操作豪娜,那么這個緩存監(jiān)聽器就得不到調(diào)用!哟楷,從這里就可以看出瘤载,Guava cache并不是自己主動去清理那些失效緩存的,而是當(dāng)我們對緩存進(jìn)行了操作時卖擅,才會進(jìn)行檢查清理以及其他動作鸣奔。那么為什么呢?想想啊惩阶,如果要主動清除挎狸,那肯定要有一個一直運(yùn)行的后臺線程去執(zhí)行清理,多了個線程出來断楷,那么意味著不再是單線程程序了伟叛,涉及多線程就要考慮加鎖資源保護(hù)了,這無疑會消耗我們資源脐嫂,影響性能统刮,而主動清除又不是必須的紊遵,等你操作了再清除,一點(diǎn)也不晚侥蒙!

當(dāng)然Guava cache也提供給我們主動清理的方法:Cache.cleanUp(),那么有了這個方法之后暗膜,是否主動清理的操作就交給了我們,由我們自己去權(quán)衡鞭衩。

緩存刷新

CacheBuilder中提供了refreshAfterWrite 用來指定緩存key寫入多久后重新進(jìn)行計算并緩存:

@Test
public void testRefresh() throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().refreshAfterWrite(1,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key + "真正計算了");
                return "cached-" + key;
            }
        });
    for (int i = 0; i < 3; i++) {
        loadingCache.getUnchecked("key1");
        TimeUnit.SECONDS.sleep(2);
    }
}

輸出
key1真正計算了
key1真正計算了
key1真正計算了

在這個例子中学搜,我們指定緩存key寫入后,超過1秒就會刷新论衍,然后我們每隔2秒訪問一次緩存key,可以看到每次都得到了重新計算瑞佩!

小結(jié)

本文通過大量代碼案例詳細(xì)介紹了Guava Cache的使用,當(dāng)然你以為會止步于此嗎坯台?由于篇幅的原因炬丸,本文為使用篇,接下來將推出原理篇蜒蕾,我們的目的是從這些大佬的源碼設(shè)計中吸取精華稠炬,所謂知己知彼~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咪啡,隨后出現(xiàn)的幾起案子首启,更是在濱河造成了極大的恐慌,老刑警劉巖撤摸,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅桃,死亡現(xiàn)場離奇詭異,居然都是意外死亡准夷,警方通過查閱死者的電腦和手機(jī)疾嗅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冕象,“玉大人代承,你說我怎么就攤上這事〗グ纾” “怎么了论悴?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長墓律。 經(jīng)常有香客問我膀估,道長,這世上最難降的妖魔是什么耻讽? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任察纯,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饼记。我一直安慰自己香伴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布具则。 她就那樣靜靜地躺著即纲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪博肋。 梳的紋絲不亂的頭發(fā)上低斋,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天,我揣著相機(jī)與錄音匪凡,去河邊找鬼膊畴。 笑死,一個胖子當(dāng)著我的面吹牛病游,可吹牛的內(nèi)容都是我干的唇跨。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼礁遵,長吁一口氣:“原來是場噩夢啊……” “哼轻绞!你這毒婦竟也來了采记?” 一聲冷哼從身側(cè)響起佣耐,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唧龄,沒想到半個月后兼砖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡既棺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年讽挟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丸冕。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡耽梅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胖烛,到底是詐尸還是另有隱情眼姐,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布佩番,位于F島的核電站众旗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏趟畏。R本人自食惡果不足惜贡歧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧利朵,春花似錦律想、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姥份。三九已至澈歉,卻和暖如春屿衅,著一層夾襖步出監(jiān)牢的瞬間涤久,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔗彤,地道東北人然遏。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓待侵,卻偏偏與公主長得像秧倾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子中狂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361

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

  • 緩存分為本地緩存和遠(yuǎn)端緩存胃榕。常見的遠(yuǎn)端緩存有Redis瞄摊,MongoDB苦掘;本地緩存一般使用map的方式保存在本地內(nèi)存...
    hehehehe閱讀 340評論 0 0
  • Guava Cache以下的特性: automatic loading of entries into the c...
    小鋤禾閱讀 8,622評論 2 11
  • 概述 緩存是日常開發(fā)中經(jīng)常應(yīng)用到的一種技術(shù)手段鹤啡,合理的利用緩存可以極大的改善應(yīng)用程序的性能递瑰。Guava官方對Cac...
    小陳阿飛閱讀 1,790評論 0 0
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者抖部,不喜歡去冒險慎颗,但是人生放棄了冒險,也就放棄了無數(shù)的可能傲宜。 ...
    yichen大刀閱讀 6,059評論 0 4
  • 公元:2019年11月28日19時42分農(nóng)歷:二零一九年 十一月 初三日 戌時干支:己亥乙亥己巳甲戌當(dāng)月節(jié)氣:立冬...
    石放閱讀 6,889評論 0 2