SpringBoot+Redis實現分布式緩存的方法步驟
背景
緩存(Cache):指將程序或系統(tǒng)中常用的數據對象存儲在像內存這樣特定的介質中,以避免在每次程序調用時,重新創(chuàng)建或組織數據所帶來的性能損耗,從而提高了系統(tǒng)的整體運行速度。
以目前的系統(tǒng)架構來說,用戶的請求一般會先經過緩存系統(tǒng),如果緩存中沒有相關的數據,就會在其他系統(tǒng)中查詢到相應的數據并保存在緩存中,最后返回給調用方。
本地緩存:指程序級別的緩存組件,它的特點是本地緩存和應用程序會運行在同一個進程中,所以本地緩存的操作會非??欤驗樵谕粋€進程內也意味著不會有網絡上的延遲和開銷。
本地緩存適用于單節(jié)點非集群的應用場景,它的優(yōu)點是快,缺點是多程序無法共享緩存,比如分布式用戶 Session 會話信息保存,由于每次用戶訪問的服務器可能是不同的,如果不能共享緩存,那么就意味著每次的請求操作都有可能被系統(tǒng)阻止,因為會話信息只保存在某一個服務器上,當請求沒有被轉發(fā)到這臺存儲了用戶信息的服務器時,就會被認為是非登錄的違規(guī)操作。
除此之外,無法共享緩存可能會造成系統(tǒng)資源的浪費,這是因為每個系統(tǒng)都單獨維護了一份屬于自己的緩存,而同一份緩存有可能被多個系統(tǒng)單獨進行存儲,從而浪費了系統(tǒng)資源。
分布式緩存:指將應用系統(tǒng)和緩存組件進行分離的緩存機制,這樣多個應用系統(tǒng)就可以共享一套緩存數據了,它的特點是共享緩存服務和可集群部署,為緩存系統(tǒng)提供了高可用的運行環(huán)境,以及緩存共享的程序運行機制。
本地緩存可以使用EhCache 和 Google 的 Guava來實現,而分布式緩存可以使用 Redis 或 Memcached 來實現。
由于 Redis 本身就是獨立的緩存系統(tǒng),因此可以作為第三方來提供共享的數據緩存,而 Redis 的分布式支持主從、哨兵和集群的模式,所以它就可以支持分布式的緩存,而 Memcached 的情況也是類似的。
分布式緩存常見文件及解決方案
分布式緩存設計的核心問題是以哪種方式進行緩存預熱和緩存更新, 以及如何優(yōu)雅解決緩存雪崩、緩存穿透、緩存降級等問題。這些問題在不 同的應用場景下有不同的解決方案。
緩存預熱: 緩存預熱指在用戶請求數據前先將數據加載到緩存系統(tǒng)中,用戶查詢 事先被預熱的緩存數據,以提高系統(tǒng)查詢效率。緩存預熱一般有系統(tǒng)啟動 加載、定時加載等方式。
緩存更新: 緩存更新指在數據發(fā)生變化后及時將變化后的數據更新到緩存中。常 見的緩存更新策略有以下4種。
- 定時更新:定時將底層數據庫內的數據更新到緩存中,該方法比較 簡單,適合需要緩存的數據量不是很大的應用場景。
- 過期更新:定時將緩存中過期的數據更新為最新數據并更新緩存的 過期時間。
- 寫請求更新:在用戶有寫請求時先寫數據庫同時更新緩存,這適用 于用戶對緩存數據和數據庫的數據有實時強一致性要求的情況。
- 讀請求更新:在用戶有讀請求時,先判斷該請求數據的緩存是否存 在或過期,如果不存在或已過期,則進行底層數據庫查詢并將查詢結果更 新到緩存中,同時將查詢結果返回給用戶。
緩存淘汰策略 在緩存數據過多時需要使用某種淘汰算法決定淘汰哪些數據。常用的 淘汰算法有以下幾種。
- FIFO(First In First Out,先進先出):判斷被存儲的時間,離 目前最遠的數據優(yōu)先被淘汰。
- LRU(Least Recently Used,最近最少使用):判斷緩存最近被使 用的時間,距離當前時間最遠的數據優(yōu)先被淘汰。
- LFU(Least Frequently Used,最不經常使用):在一段時間內, 被使用次數最少的緩存優(yōu)先被淘汰。
緩存雪崩
緩存雪崩指在同一時刻由于大量緩存失效,導致大量原本應該訪問緩 存的請求都去查詢數據庫,而對數據庫的CPU和內存造成巨大壓力,嚴重的 話會導致數據庫宕機,從而形成一系列連鎖反應,使整個系統(tǒng)崩潰。
解決方案:
- 請求加鎖:對于并發(fā)量不是很多的應用,使用請求加鎖排隊的方案 防止過多請求數據庫。
- 失效更新:為每一個緩存數據都增加過期標記來記錄緩存數據是否 失效,如果緩存標記失效,則更新數據緩存。
- 緩存數據的過期時間設置隨機:為不同的數據設置不同的緩存失效時間,防止在同一時刻有大量的數據失效。
- 如果緩存數據庫是分布式部署,將熱點數據均勻分布在不同的緩存數據庫中。
- 設置熱點數據永遠不過期。
緩存穿透
緩存穿透指由于緩存系統(tǒng)故障或者用戶頻繁查詢系統(tǒng)中不存在(在系 統(tǒng)中不存在,在自然數據庫和緩存中都不存在)的數據,而這時請求穿過 緩存不斷被發(fā)送到數據庫,導致數據庫過載,進而引發(fā)一連串并發(fā)問題。 比如用戶發(fā)起一個userName為zhangsan的請求,而在系統(tǒng)中并沒有名 為zhangsan的用戶,這樣就導致每次查詢時在緩存中都找不到該數據,然 后去數據庫中再查詢一遍。由于zhangsan用戶本身在系統(tǒng)中不存在,自然 返回空,導致請求穿過緩存頻繁查詢數據庫,在用戶頻繁發(fā)送該請求時將 導致數據庫系統(tǒng)負載增大,從而可能引發(fā)其他問題。常用的解決緩存穿透 問題的方法有布隆過濾器和cache null策略。
解決方案:
- 接口層增加校驗,如用戶鑒權校驗,id做基礎校驗,id<=0的直接攔截,一定不存在的不去查詢數據庫。
- 布隆過濾器:指將所有可能存在的數據都映射到一個足夠大的 Bitmap中,在用戶發(fā)起請求時首先經過布隆過濾器的攔截,一個一定不存 在的數據會被這個布隆過濾器攔截,從而避免對底層存儲系統(tǒng)帶來查詢上 的壓力。
- cache null策略:指如果一個查詢返回的結果為null(可能是數據 不存在,也可能是系統(tǒng)故障),我們仍然緩存這個null結果,但它的過期 時間會很短,通常不超過 5 分鐘;在用戶再次請求該數據時直接返回 null,而不會繼續(xù)訪問數據庫,從而有效保障數據庫的安全。其實cache null策略的核心原理是:在緩存中記錄一個短暫的(數據過期時間內)數 據在系統(tǒng)中是否存在的狀態(tài),如果不存在,則直接返回null,不再查詢數 據庫,從而避免緩存穿透到數據庫上。
緩存擊穿
緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由于并發(fā)用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力
解決方案:
- 設置熱點數據永遠不過期。
- 加互斥鎖。
緩存降級
緩存降級指由于訪問量劇增導致服務出現問題(如響應時間慢或不響 應)時,優(yōu)先保障核心業(yè)務的運行,減少或關閉非核心業(yè)務對資源的使 用。
服務降級策略:
- 寫降級:在寫請求增大時,可以只進行Cache的更新,然后將數據 異步更新到數據庫中,保證最終一致性即可,即將寫請求從數據庫降級為 Cache。
- 讀降級:在數據庫服務負載過高或數據庫系統(tǒng)故障時,可以只對 Cache進行讀取并將結果返回給用戶,在數據庫服務正常后再去查詢數據 庫,即將讀請求從數據庫降級為Cache。這種方式適用于對數據實時性要求 不高的場景,保障了在系統(tǒng)發(fā)生故障的情況下用戶依然能夠訪問到數據, 只是訪問到的數據相對有延遲。
一、在pom中添加依賴
<!--springboot redis依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>二、在配置文件中配置Redis連接
# Redis數據庫索引(默認為0) spring.redis.database=0 # Redis服務器地址 spring.redis.host=127.0.0.1 # Redis服務器連接端口 spring.redis.port=6379 # Redis服務器連接密碼(默認為空) spring.redis.password=123456 # 連接池最大連接數(使用負值表示沒有限制) spring.redis.jedis.pool.max-idle=10 # 連接池最大阻塞等待時間(使用負值表示沒有限制) spring.redis.jedis.pool.max-wait=3000 # 連接池中的最小空閑連接 spring.redis.jedis.pool.min-idle=0 # 連接超時時間(毫秒) spring.redis.timeout=3000
三、編寫Redis配置文件
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* RedisTemplate相關配置
* 使redis支持插入對象
*
* @param factory
* @return 方法緩存 Methods the cache
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置連接工廠
template.setConnectionFactory(factory);
// 序列化和反序列化redis的value值(默認使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修飾符范圍,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化輸入的類型,類必須是非final修飾的,final修飾的類,比如String,Integer等會跑出異常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSerializer.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSerializer);
// 使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 設置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSerializer);
template.afterPropertiesSet();
return template;
}
}四、測試Redis緩存
/**
* @Author: oyc
* @Description: redis 測試控制類
* @Since 2020年5月12日 23:35:05
*/
@RestController
@RequestMapping("/redis")
public class OyRedisController {
/**
* 依賴注入,注入redisTemplate
*/
@Autowired
private RedisTemplate redisTemplate;
/**
* 測試redis string add
*/
@GetMapping("/string/add")
public String addStringKeyValue(@RequestParam(value = "key", defaultValue = "key1") String key, @RequestParam(value = "value", defaultValue = "redis value") String value) {
redisTemplate.opsForValue().set(key, value);
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 測試redis string add
*/
@GetMapping("/object/add")
public Object addObjectKeyValue(@RequestParam(value = "key", defaultValue = "key1") String key) {
OyUser user = new OyUser(1, "宋江", "18", "male");
redisTemplate.opsForValue().set(key, user);
return redisTemplate.opsForValue().get(key);
}
/**
* 測試redis string get
*/
@GetMapping("/string/get")
public Object getStringByKey(@RequestParam(value = "key", defaultValue = "key1") String key) {
return redisTemplate.opsForValue().get(key);
}
}4.1 測試key、value 為string
4.1.1 使用接口往redis添加記錄:

4.1.2 使用IDEA的redis客戶端插件查看記錄:

4.1.3 使用接口獲取緩存數據:

4.2 測試key為string,value 為對象


五、工具類
上面案例都是直接用RedisTemplate操作Redis,操作比較復雜,借鑒網友寫了一個RedisUtils工具類,RedisUtils交給Spring容器實例化,使用時直接注解注入,使用方便簡單,減少使用難度。
/**
* @Description: redisTemplate封裝
* Redis支持五種數據類型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
* @Author: oyc
* @Since 2020年5月12日 23:41:08
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//==========================基本操作===============================
/**
* 指定緩存失效時間
*
* @param key 鍵
* @param time 時間(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根據key 獲取過期時間
*
* @param key 鍵 不能為null
* @return 時間(秒) 返回0代表為永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判斷key是否存在
*
* @param key 鍵
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 刪除緩存
*
* @param key 可以傳一個值 或多個
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 模糊查詢獲取key值
*
* @param pattern
* @return
*/
public Set keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 使用Redis的消息隊列
*
* @param channel
* @param message 消息內容
*/
public void convertAndSend(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
//============================String=============================
/**
* 普通緩存獲取
*
* @param key 鍵
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通緩存放入
*
* @param key 鍵
* @param value 值
* @return true成功 false失敗
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通緩存放入并設置時間
*
* @param key 鍵
* @param value 值
* @param time 時間(秒) time要大于0 如果time小于等于0 將設置無限期
* @return true成功 false 失敗
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 遞增
*
* @param key 鍵
* @param delta 要增加幾(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞增因子必須大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 遞減
*
* @param key 鍵
* @param delta 要減少幾(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞減因子必須大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================hash=================================
/**
* 向一張hash表中放入數據,如果不存在將創(chuàng)建
*
* @param key 鍵
* @param item 項
* @param value 值
* @return true 成功 false失敗
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一張hash表中放入數據,如果不存在將創(chuàng)建
*
* @param key 鍵
* @param item 項
* @param value 值
* @param time 時間(秒) 注意:如果已存在的hash表有時間,這里將會替換原有的時間
* @return true 成功 false失敗
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashGet
*
* @param key 鍵 不能為null
* @param item 項 不能為null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 刪除hash表中的值
*
* @param key 鍵 不能為null
* @param item 項 可以使多個 不能為null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 獲取hashKey對應的所有鍵值
*
* @param key 鍵
* @return 對應的多個鍵值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 鍵
* @param map 對應多個鍵值
* @return true 成功 false 失敗
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并設置時間
*
* @param key 鍵
* @param map 對應多個鍵值
* @param time 時間(秒)
* @return true成功 false失敗
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 判斷hash表中是否有該項的值
*
* @param key 鍵 不能為null
* @param item 項 不能為null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash遞增 如果不存在,就會創(chuàng)建一個 并把新增后的值返回
*
* @param key 鍵
* @param item 項
* @param by 要增加幾(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash遞減
*
* @param key 鍵
* @param item 項
* @param by 要減少記(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根據key獲取Set中的所有值
*
* @param key 鍵
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根據value從一個set中查詢,是否存在
*
* @param key 鍵
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 將數據放入set緩存
*
* @param key 鍵
* @param values 值 可以是多個
* @return 成功個數
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 將set數據放入緩存
*
* @param key 鍵
* @param time 時間(秒)
* @param values 值 可以是多個
* @return 成功個數
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 獲取set緩存的長度
*
* @param key 鍵
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值為value的
*
* @param key 鍵
* @param values 值 可以是多個
* @return 移除的個數
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 獲取list緩存的內容
*
* @param key 鍵
* @param start 開始
* @param end 結束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 獲取list緩存的長度
*
* @param key 鍵
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通過索引 獲取list中的值
*
* @param key 鍵
* @param index 索引 index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 將list放入緩存
*
* @param key 鍵
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
*
* @param key 鍵
* @param value 值
* @param time 時間(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
*
* @param key 鍵
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
*
* @param key 鍵
* @param value 值
* @param time 時間(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根據索引修改list中的某條數據
*
* @param key 鍵
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N個值為value
*
* @param key 鍵
* @param count 移除多少個
* @param value 值
* @return 移除的個數
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
源碼: https://github.com/oycyqr/springboot-learning-demo/tree/master/springboot-redis
到此這篇關于SpringBoot+Redis 實現分布式緩存的方法步驟的文章就介紹到這了,更多相關SpringBoot Redis 分布式緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
ThreadPoolExecutor中的submit()方法詳細講解
在使用線程池的時候,發(fā)現除了execute()方法可以執(zhí)行任務外,還發(fā)現有一個方法submit()可以執(zhí)行任務,本文就詳細的介紹一下ThreadPoolExecutor中的submit()方法,具有一定的參考價值,感興趣的可以了解一下2022-04-04
java.net.SocketTimeoutException: Read timed o
本文主要介紹了java.net.SocketTimeoutException: Read timed out異常的解決,可能是因為網絡延遲、服務器響應慢或連接不穩(wěn)定等原因造成的,下面就一起來介紹一下,感興趣的可以了解一下2024-05-05
Druid基本配置及內置監(jiān)控使用_動力節(jié)點Java學院整理
這篇文章主要介紹了Druid基本配置及內置監(jiān)控使用,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08

