Spring Boot緩存技術(shù)-----F04

Spring Boot****緩存技術(shù)*

1.Ehcache Redis 的對比

Ehcache

在java項目廣泛的使用浅蚪。它是一個開源的准验、設(shè)計于提高在數(shù)據(jù)從RDBMS中取出來的高花費芍躏、高延遲采取的一種緩存方案历等。正因為Ehcache具有健壯性(基于java開發(fā))偿凭、被認證(具有apache 2.0 license)婶熬、充滿特色,所以被用于大型復(fù)雜分布式web application的各個節(jié)點中量窘。夠簡單就是Ehcache的一大特色雇寇,自然用起來 just so easy!

夠快

Ehcache 的發(fā)行有一段時長了,經(jīng)過幾年的努力和不計其數(shù)的性能測試,Ehcache 終被設(shè)計于 large, high concurrency systems锨侯。

夠簡單

開發(fā)者提供的接口非常簡單明了嫩海,從 Ehcache 的搭建到運用運行僅僅需要的是你寶貴的幾分鐘。其實很多編程者都不知道自己在用Ehcache识腿,Ehcache被廣泛的運用于其他的開源項目出革,比如:Hibernate造壮。

夠袖珍

關(guān)于這點的特性渡讼,官方給了一個很可愛的名字 small foot print,一般 Ehcache 的發(fā)布版本不會到2M耳璧, V2.2.3版本才668KB成箫,目前最新版的V3.8.0版本也不過才1754KB。

夠輕量

核心程序僅僅依賴 slf4j 這一個包旨枯,沒有之一蹬昌!

好擴展

Ehcache 提供了對大數(shù)據(jù)的內(nèi)存和硬盤的存儲,最近版本允許多實例攀隔、保存對象高靈活性皂贩、提供LRU、LFU昆汹、FIFO淘汰算法明刷,基礎(chǔ)屬性支持熱配置、支持的插件多满粗。

監(jiān)聽器

緩存管理器監(jiān)聽器 (CacheManagerListener)和 緩存監(jiān)聽器(CacheEvenListener)辈末,做一些統(tǒng)計或數(shù)據(jù)一致性廣播挺好用的。

Redis

支持持久化

Redis 的本地持久化支持兩種方式:RDB和AOF映皆。RDB 在redis.conf配置文件里配置持久化觸發(fā)器挤聘,AOF指的是Redis每增加一條記錄都會保存到持久化文件中(保存的是這條記錄的生成命令)。

豐富的數(shù)據(jù)類型

Redis 支持 String 捅彻、List组去、Set、Sorted Set步淹、hash 多種數(shù)據(jù)類型从隆。

高性能

內(nèi)存操作的級別是毫秒級的比硬盤操作秒級操作自然高效不少,減少了磁頭尋道贤旷、數(shù)據(jù)讀取广料、頁面交換這些高開銷的操作!這也是NOSQL冒出來的原因吧幼驶,應(yīng)該是高性能是基于RDBMS的衍生產(chǎn)品艾杏,雖然RDBMS也具有緩存結(jié)構(gòu),但是始終在應(yīng)用層面達不到我們的需求盅藻。

Replication

Redis 提供主從復(fù)制方案购桑,跟 MySql 一樣增量復(fù)制而且復(fù)制的實現(xiàn)都很相似畅铭,這個復(fù)制跟AOF有點類似復(fù)制的是新增記錄命令,主庫新增記錄將新增腳本發(fā)送給從庫勃蜘,從庫根據(jù)腳本生成記錄硕噩,這個過程非常快缭贡,就看網(wǎng)絡(luò)了炉擅,一般主從都是在同一個局域網(wǎng),所以可以說 Redis 的主從近似及時同步阳惹,同時它還支持一主多從谍失,動態(tài)添加從庫,從庫數(shù)量沒有限制莹汤。

更新快

Redis 到目前為止已經(jīng)發(fā)了大版本 5 個快鱼,小版本沒算過。Redis 作者是個非常積極的人纲岭,無論是郵件提問還是論壇發(fā)帖抹竹,他都能及時耐心的為你解答,維護度很高止潮。有人維護的話窃判,讓我們用的也省心和放心。目前作者對Redis 的主導(dǎo)開發(fā)方向是 Redis 的集群方向沽翔。

總結(jié)

Ehcache 直接在 jvm 虛擬機中緩存兢孝,速度快,效率高仅偎;但是緩存共享麻煩跨蟹,集群分布式應(yīng)用不方便。如果是單個應(yīng)用或者對緩存訪問要求很高的應(yīng)用橘沥,用 Ehcache窗轩。

Redis 是通過 socket 訪問到緩存服務(wù),效率比 Ecache 低座咆,比數(shù)據(jù)庫要快很多痢艺,處理集群和分布式緩存方便,有成熟的方案介陶。如果是大型系統(tǒng)堤舒,存在緩存共享、分布式部署哺呜、緩存內(nèi)容很大的舌缤,建議用 Redis。

2.Ehcache

使用 Ehcache 緩存的步驟是:

  • 添加 Ehcache 組件和依賴
  • 添加 Ehcache 配置文件
  • 業(yè)務(wù)層使用 @Cacheable 處理緩存
  • 啟動類使用 @EnableCaching 開啟緩存

創(chuàng)建項目

image.png

添加組件依賴

我們需要在 pom.xml 中添加 cache 組件 和 ehcache 依賴

(ehcache 依賴可以不用添加,本案例添加是為了獲取 ehcache 配置文件模板

pom.xml

 <dependencies>
        <!-- cache 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- ehcache 依賴 -->
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
        <!-- thymeleaf 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- web 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis 組件 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!-- mysql 數(shù)據(jù)庫驅(qū)動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- druid 數(shù)據(jù)庫連接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>

        <!-- test 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- build標(biāo)簽 常用于添加插件及編譯配置 -->
    <build>
        <!-- 讀取配置文件 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                    <include>**/*.tld</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

ehcache 配置文件

我們可以參考 ehcache 項目源碼中的配置文件模板国撵。

image.png

diskStore

diskStore 元素是可選的陵吸,非必須的。如果不使用磁盤存儲介牙,只需要將 diskStore 注釋掉即可壮虫;如果使用,需要在 ehcache.xml 文件中的 ehcahce 元素下的定義一個 diskStore 元素并指定其 path 屬性环础。

path 屬性可以配置的目錄有:

user.home(用戶的家目錄)

user.dir(用戶當(dāng)前的工作目錄)

java.io.tmpdir(默認的臨時目錄)

ehcache.disk.store.dir(ehcache的配置目錄)

絕對路徑(如:D:\ehcache)

DiskStore 中驅(qū)除元素跟 MemoryStore 中驅(qū)除元素的規(guī)則是不一樣的囚似。當(dāng)往 DiskStore 中添加元素且此時DiskStore 中的容量已經(jīng)超出限制時將采用 LFU(最不常用)驅(qū)除規(guī)則將對應(yīng)的元素進行刪除,而且該驅(qū)除規(guī)則是不可配置的(通過 cache 中的 diskExpiryThreadIntervalSeconds 屬性完成)喳整。

緩存策略

name :緩存名稱

maxElementsInMemory :內(nèi)存緩存中最多可以存放的元素數(shù)量谆构,若放入 Cache 中的元素超過這個數(shù)值,則有以下兩種情況:

  1. 若 overflowToDisk=true框都,則會將 Cache 中多出的元素放入磁盤文件中
  1. 若 overflowToDisk=false,則根據(jù) memoryStoreEvictionPolicy 策略替換 Cache 中原有的元素

eternal :緩存中對象是否永久有效呵晨,是否永駐內(nèi)存魏保,true時將忽略timeToIdleSeconds和 timeToLiveSeconds

timeToIdleSeconds :設(shè)置對象的空閑時間

timeToLiveSeconds :設(shè)置對象的存活時間

overflowToDisk :內(nèi)容溢出是否寫入磁盤

memoryStoreEvictionPolicy :內(nèi)存存儲與釋放策略,即達到 maxElementsInMemory 限制時摸屠,Ehcache 會根據(jù)指定策略清理內(nèi)存谓罗。共有三種策略:

LRU(最近最少使用)

LFU(最常用的)

FIFO(先進先出)

ehcache 中緩存的 3 種清空策略:

  1. FIFO: first in first out,這個是大家最熟的季二,先進先出檩咱,不多講了
  2. LFU: Less Frequently Used,直白一點就是講一直以來最少被使用的胯舷。緩存的元素有一個 hit 屬性刻蚯, hit 值最小的將會被清出緩存。
  3. LRU: Least Recently Used桑嘶,最近最少使用的膀估,緩存的元素有一個時間戳矾缓,當(dāng)緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那么現(xiàn)有緩存元素中時間戳離當(dāng)前時間最遠的元素將被清出緩存即纲。

對于 EhCache 的配置文件也可以通過 application.properties 文件中使用 spring.cache.ehcache.config 屬性來指定,比如: spring.cache.ehcache.config=classpath:config/ehcache.xml

resources/ehcache.xml

<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>

    <!-- 默認緩存策略 -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>

    <!-- 自定義緩存策略 -->
    <cache name="userCache"
           maxElementsInMemory="10000"
           eternal="false"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </cache>

</ehcache>

properties 配置文件

# 配置數(shù)據(jù)庫驅(qū)動
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=shsxt
# 配置數(shù)據(jù)庫連接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 配置MyBatis數(shù)據(jù)返回類型別名(默認別名是類名)
mybatis.type-aliases-package=com.springboot.pojo
# 配置MyBatis Mapper映射文件
mybatis.mapper-locations=classpath:com/springboot/mapper/*.xml
# 配置SQL打印
# 指定某個包下的SQL打印
logging.level.com.springboot.mapper=debug
# 所有包下的SQL打印
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

添加 application.properties 全局配置文件帮哈,配置SQL打印司顿。

SQL****文件

CREATE TABLE `user` (
`id` INT (11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR (255) DEFAULT NULL,
`age` INT (11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

實體類

User.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Mapper 接口

UserMapper.java

public interface UserMapper {

    // 添加用戶
    int insertUser(User user);

    // 查詢用戶
    List<User> selectUserList();

    // 根據(jù)主鍵查詢用戶
    User selectUserById(Integer id);

    // 修改用戶
    int updateUser(User user);

    // 刪除用戶
    int deleteUser(Integer id);

}

映射配置文件

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 必須是接口的完全限定名 -->
<mapper namespace="com.springboot.mapper.UserMapper">

    <!-- id 必須和接口中的方法名一致 -->
    <insert id="insertUser" parameterType="user">
        insert into user (name, age) values (#{name}, #{age})
    </insert>

    <!-- 查詢所有用戶 -->
    <select id="selectUserList" resultType="user">
        select id, name, age from user;
    </select>

    <!-- 根據(jù)主鍵查詢用戶 -->
    <select id="selectUserById" resultType="user">
        select id, name, age from user where id = #{id};
    </select>

    <!-- 修改用戶 -->
    <update id="updateUser" parameterType="user">
        update user set name = #{name}, age = #{age} where id = #{id};
    </update>

    <!-- 刪除用戶 -->
    <delete id="deleteUser">
        delete from user where id = #{id}
    </delete>

</mapper>

業(yè)務(wù)層

業(yè)務(wù)層使用 @Cacheable 處理緩存

UserServiceI.java

public interface UserServiceI {

    int insertUser(User user);

    // 查詢用戶
    List<User> selectUserList();

    // 根據(jù)主鍵查詢用戶
    User selectUserById(Integer id);

    // 修改用戶
    int updateUser(User user);

    // 刪除用戶
    int deleteUser(Integer id);

}

UserServiceImpl.java

@Service
@Transactional
@CacheConfig(cacheNames = "userCache")
public class UserServiceImpl implements UserServiceI {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private CacheManager cacheManager;

    // 清除緩存中以 userCache 緩存策略緩存的對象
    @CacheEvict(allEntries = true)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

    @Cacheable
    @Override
    public List<User> selectUserList() {
        return userMapper.selectUserList();
    }

    @Cacheable(key = "#id")
    @Override
    public User selectUserById(Integer id) {
        return userMapper.selectUserById(id);
    }

    // 清除緩存中以 userCache 緩存策略緩存的對象
    @CacheEvict(allEntries = true)
    @Override
    public int updateUser(User user) {
        return userMapper.updateUser(user);
    }

    // 清除緩存中以 userCache 緩存策略緩存的對象
    //@CacheEvict(allEntries = true)
    @Override
    public int deleteUser(Integer id) {
        Cache cache = cacheManager.getCache("userCache");
        cache.clear();
        return userMapper.deleteUser(id);
    }

}


控制層

UserController.java

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserServiceI userService;

    /**
     * 頁面跳轉(zhuǎn)
     */
    @RequestMapping("/{page}")
    public String page(@PathVariable String page) {
        return page;
    }

    /**
     * 添加用戶
     */
    @PostMapping("/insertUser")
    public String insertUser(User user) {
        int result = userService.insertUser(user);
        return "success";
    }

    /**
     * 查詢用戶列表
     */
    @GetMapping("/selectUserList")
    public String selectUserList(Model model) {
        model.addAttribute("userList", userService.selectUserList());
        return "user-list";
    }

    /**
     * 修改用戶跳轉(zhuǎn)頁面
     */
    @GetMapping("/edit/{id}")
    public String edit(@PathVariable Integer id, Model model) {
        model.addAttribute("user", userService.selectUserById(id));
        return "updateUser";
    }

    /**
     * 修改用戶保存
     */
    @PostMapping("/updateUser")
    public String updateUser(User user) {
        userService.updateUser(user);
        return "success";
    }

    /**
     * 刪除用戶
     */
    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable Integer id) {
        userService.deleteUser(id);
        return "success";
    }

}


視圖層

templates/register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注冊用戶</title>
</head>
<body>
    <form th:action="@{/user/insertUser}" method="post">
        姓名:<input name="name"/><br/>
        年齡:<input name="age"/><br/>
        <input type="submit" value="注冊"/>
    </form>
</body>
</html>

templates/success.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>提示</title>
</head>
<body>
    成功
</body>
</html>

templates/updateUser.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改用戶</title>
</head>
<body>
    <form th:action="@{/user/updateUser}" method="post">
        <input type="hidden" name="id" th:value="${user.id}"/>
        姓名:<input name="name" th:value="${user.name}"/><br/>
        年齡:<input name="age" th:value="${user.age}"/><br/>
        <input type="submit" value="修改"/>
    </form>
</body>
</html>

templates/user-list.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用戶列表</title>
</head>
<body>
    <table border="1" width="300px" cellspacing="0">
        <tr>
            <th>ID</th>
            <th>NAME</th>
            <th>AGE</th>
            <th>操作</th>
        </tr>
        <tr th:each="user : ${userList}">
            <td th:text="${user.id}"></td>
            <td th:text="${user.name}"></td>
            <td th:text="${user.age}"></td>
            <td>
                <a th:href="@{/user/edit/{id}(id=${user.id})}">修改</a>
                <a th:href="@{/user/deleteUser/{id}(id=${user.id})}">刪除</a>
            </td>
        </tr>
    </table>
</body>
</html>

啟動類

啟動類使用 @EnableCaching 開啟緩存

App.java

@SpringBootApplication
// 掃描 mapper 接口和映射配置文件
@MapperScan("com.springboot.mapper")
// 開啟緩存
@EnableCaching
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}


測試

數(shù)據(jù)庫

image.png

訪問:http://localhost:8080/user/selectUserList

image.png

刪除數(shù)據(jù)庫


image.png

再次查詢,控制臺無任何打印消息盈蛮,依然可以查詢到數(shù)據(jù)废菱,緩存配置成功。

Cache 常用注解詳解

@Cacheable

value 、 cacheNames :兩個等同的參數(shù)( cacheNames 為Spring 4新增昙啄,作為 value 的別名)穆役,用于 指定緩存存儲的集合名。由于Spring 4中新增了 @CacheConfig 梳凛,因此在Spring 3中原本必須有的 value 屬性耿币,也成為非必需項了。

key :緩存對象存儲在Map集合中的key值韧拒,非必需淹接,缺省按照函數(shù)的所有參數(shù)組合作為 key 值,若自己配置需使用 SpEL 表達式叛溢,比如: @Cacheable(key = "#p0") :使用函數(shù)第一個參數(shù)作為緩存的 key 值塑悼,在查詢時如果key 存在,那么直接從緩存中將數(shù)據(jù)返回楷掉。更多關(guān)于SpEL表達式的詳細內(nèi)容可參考官方文檔厢蒜。

UserServiceImpl.java

// 對當(dāng)前查詢的結(jié)果做緩存處理,不配置 cacheNames 屬性時使用默認緩存策略
@Cacheable(cacheNames = "userCache")
@Override
public List<User> selectUserList() {
return userMapper.selectUserList();
} /
*
設(shè)置緩存的 key
#p0:使用第一個參數(shù)作為 key
#id:使用參數(shù) id 作為 key
#user.id:使用參數(shù) user 的 id 作為 key (user 是對象 id 是 user 的屬性)
*/
@Cacheable(cacheNames = "userCache", key = "#id")
@Override
public User selectUserById(Integer id) {
return userMapper.selectUserById(id);
}

@CacheConfig

@CacheConfig is a class-level annotation that allows to share the cache names.

主要用于配置某些類中會用到的一些共用的緩存配置烹植,比如本案例中我們多次使用到

@Cacheable(cacheNames= "userCache") 我們便可以在該類上添加 @CacheConfig(cacheNames = "userCache") 注解斑鸦,方法上只需要使用 @Cacheable 即可。如果在方法上使用別的緩存名稱草雕,那么依然以方法的緩存名稱為準(zhǔn)巷屿。

@CacheEvict

配置于函數(shù)上,通常用在寫操作方法上墩虹,用來從緩存中移除相應(yīng)數(shù)據(jù)嘱巾。除了同 @Cacheable 一樣的參數(shù)之外,它還有下面兩個參數(shù):

allEntries :非必需诫钓,默認為 false旬昭。當(dāng)為 true 時,會移除所有數(shù)據(jù)尖坤;

beforeInvocation :非必需稳懒,默認為 false,會在調(diào)用方法之后移除數(shù)據(jù)慢味。當(dāng)為 true 時场梆,會在調(diào)用方法之前移除數(shù)據(jù)。

// 清除緩存中以 userCache 緩存策略緩存的對象
@CacheEvict(cacheNames = "userCache", allEntries = true)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
} 
// 如果該類配置了@CacheConfig(cacheNames = "userCache")纯路,可以簡寫
@CacheEvict(allEntries = true)
@Override
public int updateUser(User user) {
return userMapper.updateUser(user);
} 
// 如果該類配置了@CacheConfig(cacheNames = "userCache")或油,可以簡寫
@CacheEvict(allEntries = true)
@Override
public int deleteUserById(Integer id) {
return userMapper.deleteUserById(id);
}

當(dāng)我們執(zhí)行寫操作時,緩存的內(nèi)容會被清除驰唬,查詢時會重新查詢關(guān)系數(shù)據(jù)庫再次放入緩存顶岸。

CacheManager

我們還可以通過 CacheManager 對象來管理緩存腔彰。

UserServiceImpl.java

@Autowired
private CacheManager cacheManager;
@Override
public int deleteUserById(Integer id) {
// 清除緩存中以 userCache 緩存策略緩存的對象
cacheManager.getCache("userCache").clear();
return userMapper.deleteUserById(id);
}j

3.Redis

Redis 是一個開源的使用 ANSI C語言編寫、支持網(wǎng)絡(luò)辖佣、可基于內(nèi)存亦可持久化的日志型霹抛、Key-Value數(shù)據(jù)庫,并提供多種語言的API卷谈。no-sql 型的數(shù)據(jù)庫杯拐。

2008年,意大利一家創(chuàng)業(yè)公司Merzia的創(chuàng)始人Salvatore Sanfilippo為了避免MySQL的低性能世蔗,親自定做一個數(shù)據(jù)庫端逼,并于2009年開發(fā)完成,這個就是Redis污淋。

從2010年3月15日起顶滩,Redis的開發(fā)工作由VMware主持。

從2013年5月開始寸爆,Redis的開發(fā)由Pivotal贊助礁鲁。

安裝****Redis

下載地址:http://redis.io/

將 redis.tar.gz 上傳至服務(wù)器而昨,解壓 tar zxvf redis.tar.gz 救氯;

安裝依賴: yum -y install gcc-c++ autoconf automake ;

創(chuàng)建安裝目錄 mkdir -p /usr/local/redis 歌憨;

切換至解壓目錄 cd redis ;預(yù)編譯 make 墩衙;

安裝 make PREFIX=/usr/local/redis install 务嫡;

安裝成功如下圖:


image.png

redis-cli:客戶端

redis-server:服務(wù)端

修改配置文件并啟動

復(fù)制解壓目錄下 redis.conf 至安裝目錄 /usr/local/redis/bin

修改 redis.conf漆改,將 daemonize 修改為 yes(后臺啟動)心铃;

注釋掉 bind127.0.0.1 使所有的 ip 訪問 redis,若是想指定多個 ip 訪問挫剑,并不是全部的 ip 訪問去扣,可以 bind設(shè)置;

添加訪問認證 requirepass root 樊破;

處理防火墻愉棱;

啟動時,指定配置文件路徑即可 bin/redis-server bin/redis.conf 哲戚;

安裝可視化客戶端訪問:

image.png

Spring Data Redis

Spring Data Redis 是 Spring 大家族的一部分奔滑,提供了在 Srping 應(yīng)用中通過簡單的配置訪問 Redis 服務(wù),對Reids 底層開發(fā)包(Jedis顺少,JRedis朋其,RJC)進行了高度封裝王浴,RedisTemplate 提供了 Redis 各種操作、異常處理及序列化梅猿,支持發(fā)布訂閱氓辣,對 Redis Sentinel 和 Redis Cluster 支持,并對 Spring 3.1 cache進行了實現(xiàn)袱蚓。

案例中我們分別講解 Lettuce 和 Jedis 兩種實現(xiàn)钞啸, Lettuce 和 Jedis 的都是連接 Redis Server 的客戶端程序。

因為 Spring Boot2.0 之后癞松,底層默認不再采用 Jedis 作為實現(xiàn)了爽撒。而是采用效率更高,線程更安全的 Lettuce客戶端响蓉。

Jedis 是一個優(yōu)秀的基于 Java 語言的 Redis 客戶端硕勿,但是,其不足也很明顯:Jedis 在實現(xiàn)上是直接連接 Redis-Server枫甲,在多個線程間共享一個 Jedis 實例時是線程不安全的源武,如果想要在多線程場景下使用 Jedis,需要使用連接池想幻,每個線程都使用自己的 Jedis 實例粱栖,當(dāng)連接數(shù)量增多時,會消耗較多的物理資源脏毯。

Lettuce 則完全克服了其線程不安全的缺點:Lettuce 是基于 Netty 的連接實例(StatefulRedisConnection)闹究,

Lettuce 是一個可伸縮的線程安全的 Redis 客戶端,支持同步食店、異步和響應(yīng)式模式渣淤。多個線程可以共享一個連接實例,而不必擔(dān)心多線程并發(fā)問題吉嫩。它基于優(yōu)秀 Netty NIO 框架構(gòu)建价认,支持 Redis 的高級功能,如Sentinel自娩,集群用踩,流水線,自動重新連接和 Redis 數(shù)據(jù)模型忙迁。

3.1Lettuce

我們先來講 Spring Boot2.x 版本以后的默認方式 Lettuce脐彩。

創(chuàng)建項目

創(chuàng)建Spring Boot項目,選擇 Web 組件 Redis 組件动漾。需要手動添加 commons-pool2 對象池依賴丁屎。 !
image.png

pom.xml

<!-- spring data redis 組件 -->
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 對象池依賴 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- web 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
            <!-- test 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



properties 配置文件

application.properties

# 最大連接數(shù),默認8
spring.redis.lettuce.pool.max-active=1024
# 最大連接阻塞等待時間旱眯,單位毫秒晨川,默認-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空閑連接证九,默認8
spring.redis.lettuce.pool.max-idle=200
# 最小空閑連接,默認0
spring.redis.lettuce.pool.min-idle=5
# 連接超時時間
spring.redis.timeout=10000
# Redis服務(wù)器地址
spring.redis.host=192.168.190.10
# Redis服務(wù)器端口
spring.redis.port=6379
# Redis服務(wù)器密碼
spring.redis.password=root
# 選擇哪個庫共虑,默認0庫
spring.redis.database=0


自定義模板

默認情況下的模板 RedisTemplate<Object, Object> 愧怜,默認序列化使用的是 JdkSerializationRedisSerializer ,存儲二進制字節(jié)碼妈拌。這時需要自定義模板拥坛,當(dāng)自定義模板后又想存儲String 字符串時,可以使用 StringRedisTemplate 的方式尘分,他們倆并不沖突猜惋。

序列化問題:

要把 domain object 做為 key-value 對保存在 redis 中,就必須要解決對象的序列化問題培愁。Spring Data Redis給我們提供了一些現(xiàn)成的方案:

JdkSerializationRedisSerializer 使用JDK提供的序列化功能著摔。 優(yōu)點是反序列化時不需要提供類型信息(class),但缺點是序列化后的結(jié)果非常龐大定续,是JSON格式的5倍左右谍咆,這樣就會消耗 Redis 服務(wù)器的大量內(nèi)存。

Jackson2JsonRedisSerializer 使用 Jackson 庫將對象序列化為JSON字符串私股。優(yōu)點是速度快摹察,序列化后的字符串短小精悍。但缺點也非常致命倡鲸,那就是此類的構(gòu)造函數(shù)中有一個類型參數(shù)供嚎,必須提供要序列化對象的類型信息(.class對象)。 通過查看源代碼峭状,發(fā)現(xiàn)其只在反序列化過程中用到了類型信息查坪。

GenericJackson2JsonRedisSerializer 通用型序列化,這種序列化方式不用自己手動指定對象的Class宁炫。

RedisConfigForLettuce.java

@Configuration
public class RedisConfigForLettuce {
// 重寫 RedisTemplate 序列化
@Bean
public RedisTemplate<String, Object> redisTemplate(
LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 為 String 類型 key 設(shè)置序列化器
template.setKeySerializer(new StringRedisSerializer());
// 為 String 類型 value 設(shè)置序列化器
template.setValueSerializer(new
GenericJackson2JsonRedisSerializer());
// 為 Hash 類型 key 設(shè)置序列化器
template.setHashKeySerializer(new StringRedisSerializer());
// 為 Hash 類型 value 設(shè)置序列化器
template.setHashValueSerializer(new
GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

debug 模式運行 RedisConnectionFactory 信息如下:

image.png

實體類

user.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String username;
    private Integer age;

    public User() {
    }

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                '}';
    }

}

測試類

SpringbootRedisApplicationTests.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {App.class})
public class SpringbootRedisApplicationTests {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testSet() {
        User user = new User();
        user.setId(1);
        user.setUsername("張三");
        user.setAge(18);
        redisTemplate.opsForValue().set("user:" + user.getId(), user);

        User u = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(u);
    }

    @Test
    public void testGet() {
        User user = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(user);
    }

}

結(jié)果

image.png
3.2Jedis

創(chuàng)建項目

創(chuàng)建Spring Boot項目,選擇 Web 組件 Redis 組件氮凝。需要手動添加 commons-pool2 對象池依賴羔巢,排除 Lettuce依賴,添加 Jedis 依賴罩阵。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot</groupId>
    <artifactId>springboot-redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-redis</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!--
                1.x 的版本默認采用的連接池技術(shù)是 Jedis竿秆,
                2.0 以上版本默認連接池是 Lettuce,
                如果采用 Jedis,需要排除 Lettuce 的依賴稿壁。
             -->
         <exclusions>
             <exclusion>
                      <groupId>io.lettuce</groupId>
                         <artifactId>lettuce-core</artifactId>
               </exclusion>
          </exclusions>
        </dependency>
        <!-- jedis 依賴 -->
      <dependency>
     <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
     </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>


properties 配置文件

application.properties

# 最大連接數(shù)幽钢,默認8
spring.redis.lettuce.pool.max-active=1024
# 最大連接阻塞等待時間,單位毫秒傅是,默認-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空閑連接匪燕,默認8
spring.redis.lettuce.pool.max-idle=200
# 最小空閑連接蕾羊,默認0
spring.redis.lettuce.pool.min-idle=5
# 連接超時時間
spring.redis.timeout=10000
# Redis服務(wù)器地址
spring.redis.host=192.168.190.10
# Redis服務(wù)器端口
spring.redis.port=6379
# Redis服務(wù)器密碼
spring.redis.password=root
# 選擇哪個庫,默認0庫
spring.redis.database=0


自定義模板

RedisConfigForJedis.java

//@Configuration
public class RedisConfigForJedis {

    // 重寫 RedisTemplate 序列化
    //@Bean

    public RedisTemplate<String, Object> redisTemplate(
            JedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 為 String 類型 key 設(shè)置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 為 String 類型 value 設(shè)置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 為 Hash 類型 key 設(shè)置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 為 Hash 類型 value 設(shè)置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }


}


debug 模式運行 RedisConnectionFactory 信息如下:

image.png

實體類

User.java

測試類

SpringbootRedisApplicationTests.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {App.class})
public class SpringbootRedisApplicationTests {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testSet() {
        User user = new User();
        user.setId(1);
        user.setUsername("張三");
        user.setAge(18);
        redisTemplate.opsForValue().set("user:" + user.getId(), user);

        User u = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(u);
    }

    @Test
    public void testGet() {
        User user = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(user);
    }

}


結(jié)果

image.png
3.3Sentinel

Redis 哨兵是為 Redis 提供一個高可靠解決方案帽驯,Redis 主節(jié)點掛掉會自動幫我們提升從為主龟再,對一定程序上的錯誤可以不需要人工干預(yù)自行解決。哨兵功能還有監(jiān)視尼变、事件通知利凑、配置功能等。以下是哨兵的功能列表:

監(jiān)控:不間斷的檢查主從服務(wù)是否如預(yù)期一樣正常工作嫌术;

事件通知:對被監(jiān)視的 Redis 實例的異常哀澈,能通知系統(tǒng)管理員,或者以API接口通知其他應(yīng)用程序度气;

智能援救:當(dāng)被監(jiān)視的主服務(wù)異常時割按,哨兵會智能的把某個從服務(wù)提升為主服務(wù),同時其他從服務(wù)與新的主服務(wù)之間的關(guān)系將得到重新的配置蚯嫌。應(yīng)用程序?qū)⑼ㄟ^redis服務(wù)端重新得到新的主服務(wù)的地址并重新建立連接哲虾;

配置服務(wù):客戶端可連接哨兵的接口,獲得主從服務(wù)的相關(guān)信息择示,如果發(fā)生改變束凑,哨兵新通知客戶端。

Spring Boot 也提供了對于哨兵連接的配置栅盲,關(guān)于哨兵主從服務(wù)汪诉,我們先來看看本案例使用的環(huán)境。


image.png

properties 配置文件

application.properties

# 最大連接數(shù)谈秫,默認8
spring.redis.lettuce.pool.max-active=1024
# 最大連接阻塞等待時間扒寄,單位毫秒,默認-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空閑連接拟烫,默認8
spring.redis.lettuce.pool.max-idle=200
# 最小空閑連接该编,默認0
spring.redis.lettuce.pool.min-idle=5
# 連接超時時間
spring.redis.timeout=10000
# Redis服務(wù)器地址
spring.redis.host=192.168.18.10
# Redis服務(wù)器端口,哨兵模式下不一定非要配置為主節(jié)點硕淑,只要是主從環(huán)境中任何一個節(jié)點即可
spring.redis.port=6379
# Redis服務(wù)器密碼
spring.redis.password=root
# 選擇哪個庫课竣,默認0庫
spring.redis.database=0
# 哨兵主從服務(wù)
# 主節(jié)點名稱
spring.redis.sentinel.master=mymaster
# 主從服務(wù)器地址
spring.redis.sentinel.nodes=192.168.18.10:26379,192.168.18.10:26380,192.168.18.10:26381

@Bean

除了使用配置文件或者使用 @Bean 配置 Sentinel

Lettuce 配置 Sentinel

@Configuration
public class RedisConfigForLettuce {

    /**
     * Lettuce
     */
    //@Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master("mymaster")// 主節(jié)點名稱
                .sentinel("192.168.10.10", 26379)
                .sentinel("192.168.10.10", 26380)
                .sentinel("192.168.10.10", 26381);
        sentinelConfig.setPassword("root");// 設(shè)置密碼
        return new LettuceConnectionFactory(sentinelConfig);
    }

    // 重寫 RedisTemplate 序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 為 String 類型 key 設(shè)置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 為 String 類型 value 設(shè)置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 為 Hash 類型 key 設(shè)置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 為 Hash 類型 value 設(shè)置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}


Jedis 配置 Sentinel

package com.springboot.config;

//@Configuration
public class RedisConfigForJedis {

    /**
     * Jedis
     */
    //@Bean
    /*
    public RedisConnectionFactory jedisConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master("mymaster")// 主節(jié)點名稱
                .sentinel("192.168.10.10", 26379)
                .sentinel("192.168.10.10", 26380)
                .sentinel("192.168.10.10", 26381);
        sentinelConfig.setPassword("root");
        return new JedisConnectionFactory(sentinelConfig);
    }
     */

    // 重寫 RedisTemplate 序列化
    //@Bean
    /*
    public RedisTemplate<String, Object> redisTemplate(
            JedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 為 String 類型 key 設(shè)置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 為 String 類型 value 設(shè)置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 為 Hash 類型 key 設(shè)置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 為 Hash 類型 value 設(shè)置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    */

}


測試類

測試同上

不需要配置主節(jié)點,哨兵可以可以自動查找

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市置媳,隨后出現(xiàn)的幾起案子于樟,更是在濱河造成了極大的恐慌,老刑警劉巖拇囊,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迂曲,死亡現(xiàn)場離奇詭異,居然都是意外死亡寥袭,警方通過查閱死者的電腦和手機路捧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門关霸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鬓长,你說我怎么就攤上這事谒拴。” “怎么了涉波?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵英上,是天一觀的道長。 經(jīng)常有香客問我啤覆,道長苍日,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任窗声,我火速辦了婚禮相恃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笨觅。我一直安慰自己拦耐,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布见剩。 她就那樣靜靜地躺著杀糯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苍苞。 梳的紋絲不亂的頭發(fā)上固翰,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天,我揣著相機與錄音羹呵,去河邊找鬼骂际。 笑死,一個胖子當(dāng)著我的面吹牛冈欢,可吹牛的內(nèi)容都是我干的歉铝。 我是一名探鬼主播,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼凑耻,長吁一口氣:“原來是場噩夢啊……” “哼犯戏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拳话,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎种吸,沒想到半個月后弃衍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡坚俗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年镜盯,在試婚紗的時候發(fā)現(xiàn)自己被綠了岸裙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡速缆,死狀恐怖降允,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艺糜,我是刑警寧澤剧董,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站破停,受9級特大地震影響翅楼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜真慢,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一毅臊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧黑界,春花似錦管嬉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至童社,卻和暖如春求厕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扰楼。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工呀癣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弦赖。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓项栏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蹬竖。 傳聞我的和親對象是個殘疾皇子沼沈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,587評論 2 350

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