SpringBoot-Redis 入門

SpringBoot-Redis 入門

Redis 的數(shù)據(jù)類型

String 字符串

  • string 是 redis 最基本的類型,一個(gè) key 對(duì)應(yīng)一個(gè) value茅主。
  • string 類型是二進(jìn)制安全的襟己。意思是 redis 的 string 可以包含任何數(shù)據(jù)探赫。比如 jpg 圖片或者序列化的對(duì)象 。
  • string 類型是 Redis 最基本的數(shù)據(jù)類型嘱能,一個(gè)鍵最大能存儲(chǔ) 512MB吝梅。
  • String 類型的操作參考

鏈表

  • redis 列表是簡(jiǎn)單的字符串列表,排序?yàn)椴迦氲捻樞蛉锹睢A斜淼淖畲箝L(zhǎng)度為 2^32-1苏携。
  • redis 的列表是使用鏈表實(shí)現(xiàn)的,這意味著对粪,即使列表中有上百萬(wàn)個(gè)元素右冻,增加一個(gè)元素到列表的頭部或尾部的操作都是在常量的時(shí)間完成。
  • 可以用列表獲取最新的內(nèi)容(像帖子著拭,微博等)纱扭,用 ltrim 很容易就會(huì)獲取最新的內(nèi)容,并移除舊的內(nèi)容儡遮。
  • 用列表可以實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式乳蛾,生產(chǎn)者調(diào)用 lpush 添加項(xiàng)到列表中,消費(fèi)者調(diào)用 rpop 從列表中提取鄙币,如果沒(méi)有元素肃叶,則輪詢?nèi)カ@取,或者使用 brpop 等待生產(chǎn)者添加項(xiàng)到列表中十嘿。
  • List 類型的操作參考

集合

  • redis 集合是無(wú)序的字符串集合因惭,集合中的值是唯一的,無(wú)序的绩衷”哪В可以對(duì)集合執(zhí)行很多操作,例如唇聘,測(cè)試元素是否存在版姑,對(duì)多個(gè)集合執(zhí)行交集、并集和差集等等迟郎。
  • 我們通嘲眨可以用集合存儲(chǔ)一些無(wú)關(guān)順序的,表達(dá)對(duì)象間關(guān)系的數(shù)據(jù)宪肖,例如用戶的角色表制,可以用 sismember 很容易就判斷用戶是否擁有某個(gè)角色健爬。
  • 在一些用到隨機(jī)值的場(chǎng)合是非常適合的,可以用 srandmember/spop 獲取/彈出一個(gè)隨機(jī)元素么介。
    同時(shí)娜遵,使用@EnableCaching 開(kāi)啟聲明式緩存支持,這樣就可以使用基于注解的緩存技術(shù)壤短。注解緩存是一個(gè)對(duì)緩存使用的抽象设拟,通過(guò)在代碼中添加下面的一些注解,達(dá)到緩存的效果久脯。
  • Set 類型的操作參考

ZSet 有序集合

  • 有序集合由唯一的纳胧,不重復(fù)的字符串元素組成。有序集合中的每個(gè)元素都關(guān)聯(lián)了一個(gè)浮點(diǎn)值帘撰,稱為分?jǐn)?shù)跑慕。可以把有序看成 hash 和集合的混合體摧找,分?jǐn)?shù)即為 hash 的 key核行。
  • 有序集合中的元素是按序存儲(chǔ)的,不是請(qǐng)求時(shí)才排序的蹬耘。
  • ZSet 類型的操作類型

Hash-哈希

  • redis 的哈希值是字符串字段和字符串之間的映射芝雪,是表示對(duì)象的完美數(shù)據(jù)類型。
  • 哈希中的字段數(shù)量沒(méi)有限制婆赠,所以可以在你的應(yīng)用程序以不同的方式來(lái)使用哈希绵脯。
  • Hash 類型的操作參考

關(guān)于 key 的設(shè)計(jì)

key 的存活時(shí)間:

無(wú)論什么時(shí)候,只要有可能就利用 key 超時(shí)的優(yōu)勢(shì)休里。一個(gè)很好的例子就是儲(chǔ)存一些諸如臨時(shí)認(rèn)證 key 之類的東西。當(dāng)你去查找一個(gè)授權(quán) key 時(shí)——以 OAUTH 為例——通常會(huì)得到一個(gè)超時(shí)時(shí)間赃承。
這樣在設(shè)置 key 的時(shí)候妙黍,設(shè)成同樣的超時(shí)時(shí)間,Redis 就會(huì)自動(dòng)為你清除瞧剖。

關(guān)系型數(shù)據(jù)庫(kù)的 redis

  1. 把表名轉(zhuǎn)換為 key 前綴 如, tag:
  2. 第 2 段放置用于區(qū)分區(qū) key 的字段--對(duì)應(yīng) mysql 中的主鍵的列名,如 userid
  3. 第 3 段放置主鍵值,如 2,3,4...., a , b ,c
  4. 第 4 段,寫(xiě)要存儲(chǔ)的列名

例:user:userid:9:username

RedisTemplate 常用操作集合

方法 Redis 類型 備注
opsForValue() String 對(duì) redis 字符串類型數(shù)據(jù)操作
opsForList() List 對(duì)鏈表類型的數(shù)據(jù)操作
opsForHash() Hash 對(duì) hash 類型的數(shù)據(jù)操作
opsForSet() Set 對(duì)無(wú)序集合類型的數(shù)據(jù)操作
opsForZSet() ZSet 對(duì)有序集合類型的數(shù)據(jù)操作

Serializer

目前已經(jīng)支持的序列化策略:

  • JdkSerializationRedisSerializer:POJO 對(duì)象的存取場(chǎng)景拭嫁,使用 JDK 本身序列化機(jī)制,將 pojo 類通過(guò) ObjectInputStream/ObjectOutputStream 進(jìn)行序列化操作抓于,最終 redis-server 中將存儲(chǔ)字節(jié)序列做粤。是目前最常用的序列化策略
  • StringRedisSerializer :Key 或者 value 為字符串的場(chǎng)景,根據(jù)指定的 charset 對(duì)數(shù)據(jù)的字節(jié)序列編碼成 string捉撮,是 “new String(bytes, charset)” 和 “string.getBytes(charset)” 的直接封裝怕品。是最輕量級(jí)和高效的策略。
  • JacksonJsonRedisSerializer:jackson-json 工具提供了 javabean 與 json 之間的轉(zhuǎn)換能力巾遭,可以將 pojo 實(shí)例序列化成 json 格式存儲(chǔ)在 redis 中肉康,也可以將 json 格式的數(shù)據(jù)轉(zhuǎn)換成 pojo 實(shí)例闯估。因?yàn)?jackson 工具在序列化和反序列化時(shí),需要明確指定 Class 類型吼和,因此此策略封裝起來(lái)稍微復(fù)雜涨薪。【需要 jackson-mapper-asl 工具支持】
  • OxmSerializer :提供了將 javabean 與 xml 之間的轉(zhuǎn)換能力炫乓,目前可用的三方支持包括 jaxb刚夺,apache-xmlbeans;redis 存儲(chǔ)的數(shù)據(jù)將是 xml 工具末捣。不過(guò)使用此策略侠姑,編程將會(huì)有些難度,而且效率最低塔粒;不建議使用结借。【需要 spring-oxm 模塊的支持】

其中 JdkSerializationRedisSerializerStringRedisSerializer 是最基礎(chǔ)的序列化策略卒茬,其中 “JacksonJsonRedisSerializer” 與 “OxmSerializer” 都是基于 String 存儲(chǔ)船老,因此它們是較為“高級(jí)”的序列化 (最終還是使用 string 解析以及構(gòu)建 java 對(duì)象)。

如果你的數(shù)據(jù)需要被第三方工具解析圃酵,那么數(shù)據(jù)應(yīng)該使用 StringRedisSerializer 而不是 JdkSerializationRedisSerializer柳畔。

Redis Pipline

通過(guò) RedisTemplete 實(shí)現(xiàn) pipline 可以參考如下代碼:

public List<Object> queryAll() {
    return redisTemplate.executePipelined((RedisConnection redisConnection) -> {
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
        Set<String> keys = redisTemplate.keys("*");
        if (Objects.nonNull(keys)) {
            for (String key : keys) {
                redisConnection.get(stringSerializer.serialize(key));
            }
        }
        return null;
    });
}

需要注意的是 redisTemplate.executePipelined() 里面的方法返回值必須為 null.

原因是該方法的源碼如下:

public List<Object> executePipelined(final RedisCallback<?> action) {
    return executePipelined(action, valueSerializer);
}

public List<Object> executePipelined(final RedisCallback<?> action, final RedisSerializer<?> resultSerializer) {
    return execute(new RedisCallback<List<Object>>() {
        public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
            connection.openPipeline();
            boolean pipelinedClosed = false;
            try {
                Object result = action.doInRedis(connection);
                if (result != null) {
                    throw new InvalidDataAccessApiUsageException(
                            "Callback cannot return a non-null value as it gets overwritten by the pipeline");
                }
                List<Object> closePipeline = connection.closePipeline();
                pipelinedClosed = true;
                return deserializeMixedResults(closePipeline, resultSerializer, resultSerializer, resultSerializer);
            } finally {
                if (!pipelinedClosed) {
                    connection.closePipeline();
                }
            }
        }
    });
}

在代碼段中有如下的判斷:

Object result = action.doInRedis(connection);
if (result != null) {
    throw new InvalidDataAccessApiUsageException(
        "Callback cannot return a non-null value as it gets overwritten by the pipeline");
}

因此如果所傳入的方法如果不為空,則會(huì)拋出異常郭赐,導(dǎo)致程序運(yùn)行失敗薪韩。

注意:

  • doInRedis 中的 redis 操作不會(huì)立刻執(zhí)行
  • 所有 redis 操作會(huì)在 connection.closePipeline() 之后一并提交到 redis 并執(zhí)行,這是 pipeline 方式的優(yōu)勢(shì)
  • 所有操作的執(zhí)行結(jié)果為 executePipelined() 的返回值

RedisTemplete 執(zhí)行 lua 腳本

Redis 命令行運(yùn)行 Lua 腳本

假定我們有如下 lua 腳本:

--獲取KEY
local key1 = KEYS[1]
local key2 = KEYS[2]
 
-- 獲取ARGV[1],這里對(duì)應(yīng)到應(yīng)用端是一個(gè)List<Map>.
--  注意捌锭,這里接收到是的字符串俘陷,所以需要用csjon庫(kù)解碼成table類型
local receive_arg_json =  cjson.decode(ARGV[1])
 
--返回的變量
local result = {}
 
--打印日志到reids
--注意,這里的打印日志級(jí)別观谦,需要和redis.conf配置文件中的日志設(shè)置級(jí)別一致才行
redis.log(redis.LOG_DEBUG,key1)
redis.log(redis.LOG_DEBUG,key2)
redis.log(redis.LOG_DEBUG, ARGV[1],#ARGV[1])
 
--獲取ARGV內(nèi)的參數(shù)并打印
local expire = receive_arg_json.expire
local times = receive_arg_json.times
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))
 
--往redis設(shè)置值
redis.call("set",key1,times)
redis.call("incr",key2)
redis.call("expire",key2,expire)
 
--用一個(gè)臨時(shí)變量來(lái)存放json,json是要放入要返回的數(shù)組中的
local jsonRedisTemp={}
jsonRedisTemp[key1] = redis.call("get",key1)
jsonRedisTemp[key2] = redis.call("get", key2)
jsonRedisTemp["ttl"] = redis.call("ttl",key2)
redis.log(redis.LOG_DEBUG, cjson.encode(jsonRedisTemp))
 
 
result[1] = cjson.encode(jsonRedisTemp) --springboot redistemplate接收的是List,如果返回的數(shù)組內(nèi)容是json對(duì)象,需要將json對(duì)象轉(zhuǎn)成字符串,客戶端才能接收
result[2] = ARGV[1] --將源參數(shù)內(nèi)容一起返回
redis.log(redis.LOG_DEBUG,cjson.encode(result)) --打印返回的數(shù)組結(jié)果拉盾,這里返回需要以字符返回
 
return result

我們可以使用如下命令行查看執(zhí)行結(jié)果:

其基本命令結(jié)構(gòu)如下:

redis-cli [--ldb] --eval script [numkeys] key [key ...] , arg [arg ...]
  • --eval:告訴redis客戶端去加載Lua腳本,后面跟著的就是 lua 腳本的路徑
  • --ldb :進(jìn)行命令調(diào)試的必要參數(shù)
  • numkeys:指定后續(xù)參數(shù)有幾個(gè)key豁状∽狡可省略
  • key [key ...]:是要操作的鍵,可以指定多個(gè)泻红,在lua腳本中通過(guò)KEYS[1], KEYS[2]獲取
  • arg [arg ...]夭禽,參數(shù),在lua腳本中通過(guò)ARGV[1], ARGV[2]獲取谊路。

注意: KEYS和ARGV中間的 ',' 兩邊的空格讹躯,不能省略

針對(duì)本例中的 Lua 腳本其對(duì)應(yīng)的命令行如下:

bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"

其他的一些參數(shù)

  • -h 修改后的ip -a 修改后的密碼 -p 修改后的端口號(hào)

結(jié)果輸出為:

[root@VM_0_12_centos redis-4.0.8]# bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"
Lua debugging session started, please use:
quit    -- End the session.
restart -- Restart the script in debug mode again.
help    -- Show Lua script debugging commands.

* Stopped at 1, stop reason = step over
-> 1   local key1 = KEYS[1]
lua debugger> continue

1) "{\"rate.limiting:127.0.0.1\":\"1\",\"count\":\"10\",\"ttl\":10000}"
2) "{\"expire\":\"10000\",\"times\":\"10\"}"

使用 Java 運(yùn)行 Lua 腳本

實(shí)現(xiàn)代碼如下:

package cn.sjsdfg.redis.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by Joe on 2019/5/8.
 */
@Service
public class LuaScriptService {
    @Autowired
    @Qualifier("customRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    private DefaultRedisScript<List> getRedisScript;

    @PostConstruct
    public void init(){
        getRedisScript = new DefaultRedisScript<List>();
        getRedisScript.setResultType(List.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/LimitLoadTimes.lua")));
    }

    public void redisAddScriptExec(){
        /**
         * List設(shè)置lua的KEYS
         */
        List<String> keyList = new ArrayList<>();
        keyList.add("count");
        keyList.add("rate.limiting:127.0.0.1");

        /**
         * 用Mpa設(shè)置Lua的ARGV[1]
         */
        Map<String,Object> argvMap = new HashMap<String,Object>();
        argvMap.put("expire", 10000);
        argvMap.put("times", 10);

        /**
         * 調(diào)用腳本并執(zhí)行
         */
        List result = redisTemplate.execute(getRedisScript, keyList, argvMap);
        System.out.println(result);
    }
}

測(cè)試代碼在 cn.sjsdfg.redis.service.LuaScriptServiceTest#testRedisAddScriptExec,其輸出為:

[{rate.limiting:127.0.0.1=3, count=10, ttl=10000}, {times=10, expire=10000}]

與前面直接執(zhí)行 lua 腳本的輸出結(jié)果一致。

注意

  1. Lua腳本可以在redis單機(jī)模式蜀撑、主從模式挤巡、Sentinel集群模式下正常使用,但是無(wú)法在分片集群模式下使用酷麦。(腳本操作的key可能不在同一個(gè)分片)
  2. Lua腳本中盡量避免使用循環(huán)操作(可能引發(fā)死循環(huán)問(wèn)題)矿卑,盡量避免長(zhǎng)時(shí)間運(yùn)行。
  3. redis在執(zhí)行l(wèi)ua腳本時(shí)沃饶,默認(rèn)最長(zhǎng)運(yùn)行時(shí)間時(shí)5秒母廷,當(dāng)腳本運(yùn)行時(shí)間超過(guò)這一限制后,Redis將開(kāi)始接受其他命令但不會(huì)執(zhí)行(以確保腳本的原子性糊肤,因?yàn)榇藭r(shí)腳本并沒(méi)有被終止)琴昆,而是會(huì)返回“BUSY”錯(cuò)誤。

spring-data-redis 和 jedis 版本對(duì)應(yīng)收集總結(jié)

如果不使用對(duì)飲版本的 Jedis馆揉,在項(xiàng)目構(gòu)建的時(shí)候必定會(huì)出現(xiàn) java.lang.NoClassFoundException业舍。

Jedis 代碼重構(gòu)變革很大

spring-data-redis 版本 jedis 版本 備注
1.5.2.RELEASE 2.7.3
1.6.0.RELEASE 2.7.2 2.7.3
1.6.2.RELEASE 2.8.0
1.8.1.RELEASE 2.9.0
1.8.4.RELEASE 2.9.0
2.1.x.RELEASE 2.9.0

參考資料

連接 Redis 工具

github

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末升酣,一起剝皮案震驚了整個(gè)濱河市舷暮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌噩茄,老刑警劉巖下面,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異绩聘,居然都是意外死亡沥割,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門凿菩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)机杜,“玉大人,你說(shuō)我怎么就攤上這事衅谷〔媛” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵会喝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我玩郊,道長(zhǎng)肢执,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任译红,我火速辦了婚禮预茄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己耻陕,他們只是感情好拙徽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著诗宣,像睡著了一般膘怕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上召庞,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天岛心,我揣著相機(jī)與錄音,去河邊找鬼篮灼。 笑死忘古,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诅诱。 我是一名探鬼主播髓堪,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼娘荡!你這毒婦竟也來(lái)了干旁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤它改,失蹤者是張志新(化名)和其女友劉穎疤孕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體央拖,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祭阀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鲜戒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片专控。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖遏餐,靈堂內(nèi)的尸體忽然破棺而出伦腐,到底是詐尸還是另有隱情,我是刑警寧澤失都,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布柏蘑,位于F島的核電站,受9級(jí)特大地震影響粹庞,放射性物質(zhì)發(fā)生泄漏咳焚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一庞溜、第九天 我趴在偏房一處隱蔽的房頂上張望革半。 院中可真熱鬧,春花似錦、人聲如沸又官。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)六敬。三九已至碘赖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間觉阅,已是汗流浹背崖疤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留典勇,地道東北人劫哼。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像割笙,于是被迫代替她去往敵國(guó)和親权烧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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

  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,764評(píng)論 0 38
  • 原帖地址:http://www.reibang.com/p/2f14bc570563 redis概述 Redis...
    onlyHalfSoul閱讀 2,159評(píng)論 0 28
  • Redis 簡(jiǎn)介 Redis 是完全開(kāi)源免費(fèi)的伤溉,遵守BSD協(xié)議般码,是一個(gè)高性能的key-value數(shù)據(jù)庫(kù)。 Redi...
    奮斗的小鳥(niǎo)GO閱讀 442評(píng)論 0 2
  • 吐槽一下今天小師姐沒(méi)看書(shū)乱顾!
    大綱_6599閱讀 108評(píng)論 1 0
  • 《南方姑娘》——趙雷 (點(diǎn)擊鏈接板祝,聽(tīng)著音樂(lè)看文字。) 有這么一個(gè)姑娘啊走净,她愛(ài)吃栗子券时。在圖書(shū)館自習(xí)時(shí)吃,去夫子廟每次...
    王咕嚕閱讀 317評(píng)論 1 3