当前位置:   article > 正文

RuoYi-Vue-Plus (SpringCache、CacheManager、@Cacheable、缓存雪崩、击穿、穿透)

RuoYi-Vue-Plus (SpringCache、CacheManager、@Cacheable、缓存雪崩、击穿、穿透)

一、概述

        1、SpringCache是Spring提供的一个缓存框架,在Spring3.1版本开始支持将缓存添加到现有的spring应用程序中,在4.1开始,缓存已支持JSR-107注释和更多自定义的选项。

        2、SpringCache利用了AOP,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能了,做到了对代码侵入性做小。

        3、SpringCache框架还提供了CacheManager接口,可以实现降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine、Guava Cache、Ehcache。

二、SpringCache概念

接口:


1、Cache接口:缓存接口,定义缓存操作。实现有 如RedisCache、EhCacheCache、ConcurrentMapCache等

2、cacheResolver:指定获取解析器

3、CacheManager:缓存管理器,管理各种缓存(Cache)组件;如:RedisCacheManager,使用redis作为缓存。指定缓存管理器

注解:

1- @Cacheable:在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。

2- @CacheEvict:将一条或多条数据从缓存中删除。

3- @CachePut:将方法的返回值放到缓存中

4- @EnableCaching:开启缓存注解功能

5- @Caching:组合多个缓存注解;

6- @CacheConfig:统一配置@Cacheable中的value值

三、spring缓存整合redis 

RedisConfig 
类路径: com.ruoyi.framework.config.RedisConfig
1- spring 自动管理缓存机制
@EnableCaching //开启spring缓存,提升性能
  1. @Slf4j
  2. @Configuration
  3. @EnableCaching //1- spring 自动管理缓存机制 ,,提升性能
  4. @EnableConfigurationProperties(RedissonProperties.class)
  5. public class RedisConfig {
 2- 整合自定义缓存管理器
  1. /**
  2. * 2-自定义缓存管理器 整合spring-cache
  3. */
  4. @Bean
  5. public CacheManager cacheManager() {
  6. return new PlusSpringCacheManager();
  7. }

自定义 管理器PlusSpringCacheManager,实现CacheManager 接口,基于redssion操作缓存

  1. /**
  2. * Copyright (c) 2013-2021 Nikita Koksharov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.ruoyi.framework.manager;
  17. import com.ruoyi.common.utils.redis.RedisUtils;
  18. import org.redisson.api.RMap;
  19. import org.redisson.api.RMapCache;
  20. import org.redisson.spring.cache.CacheConfig;
  21. import org.redisson.spring.cache.RedissonCache;
  22. import org.springframework.boot.convert.DurationStyle;
  23. import org.springframework.cache.Cache;
  24. import org.springframework.cache.CacheManager;
  25. import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
  26. import org.springframework.util.StringUtils;
  27. import java.util.Collection;
  28. import java.util.Collections;
  29. import java.util.Map;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. import java.util.concurrent.ConcurrentMap;
  32. /**
  33. * A {@link org.springframework.cache.CacheManager} implementation
  34. * backed by Redisson instance.
  35. * <p>
  36. * 修改 RedissonSpringCacheManager 源码
  37. * 重写 cacheName 处理方法 支持多参数
  38. *
  39. * @author Nikita Koksharov
  40. *
  41. */
  42. @SuppressWarnings("unchecked")
  43. public class PlusSpringCacheManager implements CacheManager {
  44. //是否自动配置name
  45. private boolean dynamic = true;
  46. //是否允许null
  47. private boolean allowNullValues = true;
  48. //事务提交之后执行
  49. private boolean transactionAware = true;
  50. // 常用缓存配置 ttl; maxIdleTime; maxSize; 等
  51. Map<String, CacheConfig> configMap = new ConcurrentHashMap<>();
  52. // 缓存实例
  53. ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();
  54. /**
  55. * Creates CacheManager supplied by Redisson instance
  56. */
  57. public PlusSpringCacheManager() {
  58. }
  59. /**
  60. * Defines possibility of storing {@code null} values.
  61. * <p>
  62. * Default is <code>true</code>
  63. *
  64. * @param allowNullValues stores if <code>true</code>
  65. */
  66. public void setAllowNullValues(boolean allowNullValues) {
  67. this.allowNullValues = allowNullValues;
  68. }
  69. /**
  70. * Defines if cache aware of Spring-managed transactions.
  71. * If {@code true} put/evict operations are executed only for successful transaction in after-commit phase.
  72. * <p>
  73. * Default is <code>false</code>
  74. *
  75. * @param transactionAware cache is transaction aware if <code>true</code>
  76. */
  77. public void setTransactionAware(boolean transactionAware) {
  78. this.transactionAware = transactionAware;
  79. }
  80. /**
  81. * Defines 'fixed' cache names.
  82. * A new cache instance will not be created in dynamic for non-defined names.
  83. * <p>
  84. * `null` parameter setups dynamic mode
  85. *
  86. * @param names of caches
  87. */
  88. public void setCacheNames(Collection<String> names) {
  89. if (names != null) {
  90. for (String name : names) {
  91. getCache(name);
  92. }
  93. dynamic = false;
  94. } else {
  95. dynamic = true;
  96. }
  97. }
  98. /**
  99. * Set cache config mapped by cache name
  100. *
  101. * @param config object
  102. */
  103. public void setConfig(Map<String, ? extends CacheConfig> config) {
  104. this.configMap = (Map<String, CacheConfig>) config;
  105. }
  106. protected CacheConfig createDefaultConfig() {
  107. return new CacheConfig();
  108. }
  109. @Override
  110. public Cache getCache(String name) {
  111. // 重写 cacheName 支持多参数
  112. /**
  113. * 演示案例 : String DEMO_CACHE = "demo:cache#60s#10m#20";
  114. */
  115. String[] array = StringUtils.delimitedListToStringArray(name, "#");
  116. name = array[0];
  117. Cache cache = instanceMap.get(name);
  118. if (cache != null) {
  119. return cache;
  120. }
  121. //2- dynamic=false 不会动态生成
  122. if (!dynamic) {
  123. //return cache;
  124. return null;
  125. }
  126. CacheConfig config = configMap.get(name);
  127. if (config == null) {
  128. config = createDefaultConfig();
  129. configMap.put(name, config);
  130. }
  131. //setTTL
  132. if (array.length > 1) {
  133. config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
  134. }
  135. //setMaxIdleTime
  136. if (array.length > 2) {
  137. config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis());
  138. }
  139. //setMaxSize
  140. if (array.length > 3) {
  141. config.setMaxSize(Integer.parseInt(array[3]));
  142. }
  143. if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
  144. return createMap(name, config);
  145. }
  146. return createMapCache(name, config);
  147. }
  148. private Cache createMap(String name, CacheConfig config) {
  149. //1-获取缓存
  150. RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
  151. //2-没有过期时间传2个参数
  152. Cache cache = new RedissonCache(map, allowNullValues);
  153. // 3-事务提交 之后执行
  154. if (transactionAware) {
  155. cache = new TransactionAwareCacheDecorator(cache);
  156. }
  157. //4-不存在就添加
  158. Cache oldCache = instanceMap.putIfAbsent(name, cache);
  159. if (oldCache != null) {
  160. cache = oldCache;
  161. }
  162. return cache;
  163. }
  164. private Cache createMapCache(String name, CacheConfig config) {
  165. //1-获取缓存
  166. RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
  167. //2-有过期时间传3个参数 ,config 里面有 ttl、maxIdleTime、maxSize
  168. Cache cache = new RedissonCache(map, config, allowNullValues);
  169. // 3-事务提交 之后执行
  170. if (transactionAware) {
  171. cache = new TransactionAwareCacheDecorator(cache);
  172. }
  173. //4-不存在就添加
  174. Cache oldCache = instanceMap.putIfAbsent(name, cache);
  175. if (oldCache != null) {
  176. cache = oldCache;
  177. } else {
  178. map.setMaxSize(config.getMaxSize());
  179. }
  180. return cache;
  181. }
  182. //返回不可修改的集合
  183. @Override
  184. public Collection<String> getCacheNames() {
  185. return Collections.unmodifiableSet(configMap.keySet());
  186. }
  187. }
3-@Cacheable

以下Cacheable几个属性分别演示了如何使用:(支持SPEL表达式

  • cacheNames 
  • key 
  • sync
  • condition 
  • sync 
  1. /**
  2. * <简述>cacheNames: 指定名称 可以是数组
  3. * key: 支持spel表达式,可以获取参数
  4. * @author syf
  5. * @date 2024/5/7 11:03
  6. * @param id
  7. * @param pageQuery
  8. * @return java.lang.String
  9. */
  10. @Cacheable(cacheNames = "cache1", key = "#id + '_cache' + #pageQuery.pageNum")
  11. @GetMapping("test1")
  12. public String test1(String id, PageQuery pageQuery){
  13. return "ok";
  14. }
  15. /**
  16. * <简述> condition :符合条件进行缓存
  17. * #id != null :表示传入 id不为空才会缓存进入redis,id为空则不缓存
  18. * @author syf
  19. * @date 2024/5/7 11:03
  20. * @param id
  21. * @return java.lang.String
  22. */
  23. @Cacheable(cacheNames = "cache2", key = "#id + '_cache'" , condition = "#id != null")
  24. @GetMapping("test2")
  25. public String test2(String id){
  26. return "ok";
  27. }
  28. /**
  29. * <简述> unless 符合条件不缓存
  30. * #result == null :接口返回结果为空则不进行缓存
  31. * @author syf
  32. * @date 2024/5/7 11:03
  33. * @param id
  34. * @return java.lang.String
  35. */
  36. @Cacheable(cacheNames = "cache3", key = "#id + '_cache'" , unless = "#result == null")
  37. @GetMapping("test3")
  38. public String test3(String id){
  39. return null;
  40. }
  41. /**
  42. * <简述> sync = true
  43. * 同步阻塞:同时进来多个请求, 等待前面调用返回并缓存,才能回进入下个请求
  44. * 作用:防止缓存积存
  45. * @author syf
  46. * @date 2024/5/7 11:03
  47. * @param id
  48. * @return java.lang.String
  49. */
  50. @Cacheable(cacheNames = "cache4", key = "#id + '_cache'", sync = true)
  51. @GetMapping("test4")
  52. public String test4(String id){
  53. return null;
  54. }
  55. /**
  56. * <简述> 获取类中参数
  57. * 比较繁琐,一般是在实现类中传递登录参数,用spel获取
  58. * @author syf
  59. * @date 2024/5/7 11:03
  60. * @return java.lang.String
  61. */
  62. @Cacheable(cacheNames = "cache5", key = "T(com.ruoyi.common.helper.LoginHelper).getLoginUser().getLoginId()")
  63. @GetMapping("test5")
  64. public String test5(){
  65. LoginUser loginUser = LoginHelper.getLoginUser();
  66. return "ok";
  67. }
4- @CachePut

缓存更新

执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

  1. /**
  2. * <简述> 结果不为空进行更新
  3. * @author syf
  4. * @date 2024/5/7 11:03
  5. * @param id
  6. * @return java.lang.String
  7. */
  8. @CachePut(cacheNames = "cache2", key = "#id + '_cache'" , condition = "#result != null")
  9. @GetMapping("test2")
  10. public String test2(String id){
  11. boolean flag = doUpdate();
  12. return flag ? "ok" : null;
  13. }
5- @CacheEvict

缓存删除

执行该方法,并将缓存中结果删除。

allEntries  删除所有cacheNames = "cache4",下面缓存
beforeInvocation  默认false,方法执行之后有异常不执行。true:方法执行之后有异常,也执行
  1. /**
  2. * <简述> 删除缓存
  3. * @author syf
  4. * @date 2024/5/7 11:03
  5. * @param id
  6. * @return java.lang.String
  7. */
  8. @CacheEvict(cacheNames = "cache4", key = "#id + '_cache'")
  9. @GetMapping("test7")
  10. public String test7(String id){
  11. boolean flag = doDelete();
  12. return flag ? "ok" : null;
  13. }
  14. /**
  15. * <简述> 删除所有缓存
  16. * @author syf
  17. * @date 2024/5/7 11:03
  18. * @param id
  19. * @return java.lang.String
  20. */
  21. @CacheEvict(cacheNames = "cache4", allEntries = true)
  22. @GetMapping("test8")
  23. public String test8(String id){
  24. return null;
  25. }
  26. /**
  27. * <简述> beforeInvocation 无论是否有异常都执行操作
  28. * @author syf
  29. * @date 2024/5/7 11:03
  30. * @param id
  31. * @return java.lang.String
  32. */
  33. @CacheEvict(cacheNames = "cache4", beforeInvocation = true)
  34. @GetMapping("test9")
  35. public String test9(String id){
  36. return null;
  37. }
 6-@Caching:

指定多个Spring Cache相关的注解

三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

  1. @Caching(
  2. cacheable = {@Cacheable(value = "uer1",key = "#userName")},
  3. put = {@CachePut(value = "uer1", key = "#result.id"),
  4. @CachePut(value = "uer1", key = "#result.age")
  5. }
  6. )
  7. public User getStuByStr(String userName) {
  8. List<User> users= listMapper.selectByList(studentExample);
  9. return Optional.ofNullable(users).orElse(null).get(0);
  10. }

四、若依框架中缓存使用(自定义SpringCache 源码解读)、

CacheNames 缓存名称配置类:
类位置:com.ruoyi.common.constant.CacheNames
key 格式为: cacheNames#ttl#maxIdleTime#maxSize
/**
 * 缓存组名称常量
 * <p>
 * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
 * <p>
 * ttl 过期时间 如果设置为0则不过期 默认为0
 * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0   (超过maxIdleTime LRU算法自动清理)
 * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
 * <p>
 * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
 */

PlusSpringCacheManager 实现 CacheManager 接口,重写 getCache 方法,

就是配置了  :ttl、maxIdleTime、maxSize  三个参数吗,如下:

  1. @Override
  2. public Cache getCache(String name) {
  3. // 重写 cacheName 支持多参数
  4. /**
  5. * 1-演示案例 : String DEMO_CACHE = "demo:cache#60s#10m#20";
  6. */
  7. String[] array = StringUtils.delimitedListToStringArray(name, "#");
  8. name = array[0];
  9. Cache cache = instanceMap.get(name);
  10. if (cache != null) {
  11. return cache;
  12. }
  13. //2- dynamic=false 不会动态生成
  14. if (!dynamic) {
  15. //return cache;
  16. return null;
  17. }
  18. CacheConfig config = configMap.get(name);
  19. if (config == null) {
  20. config = createDefaultConfig();
  21. configMap.put(name, config);
  22. }
  23. //setTTL
  24. if (array.length > 1) {
  25. config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
  26. }
  27. //setMaxIdleTime
  28. if (array.length > 2) {
  29. config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis());
  30. }
  31. //setMaxSize
  32. if (array.length > 3) {
  33. config.setMaxSize(Integer.parseInt(array[3]));
  34. }
  35. if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
  36. return createMap(name, config);
  37. }
  38. return createMapCache(name, config);
  39. }

 重点:下面就是PlusSpringCacheManager ,操作缓存的地方

上面调用了:createMap、createMapCache 2个方法对比:

1-逻辑:

createMapCache 多了个 setMaxSize判断,其他都一样

  1. else {
  2. map.setMaxSize(config.getMaxSize());
  3. }

2- 返回类型 

createMap 返回  RMap

createMapCache 返回 RMapCache

对比: RMapCache 继承了 RMap 多了对于ttl、maxIdleTime、maxSize 的配置

相同 :都是基于redisson,缓存到redis

  1. private Cache createMap(String name, CacheConfig config) {
  2. //1-获取缓存
  3. RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
  4. //2-没有过期时间传2个参数
  5. Cache cache = new RedissonCache(map, allowNullValues);
  6. // 3-事务提交 之后执行
  7. if (transactionAware) {
  8. cache = new TransactionAwareCacheDecorator(cache);
  9. }
  10. //4-不存在就添加
  11. Cache oldCache = instanceMap.putIfAbsent(name, cache);
  12. if (oldCache != null) {
  13. cache = oldCache;
  14. }
  15. return cache;
  16. }
  17. private Cache createMapCache(String name, CacheConfig config) {
  18. //1-获取缓存
  19. RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
  20. //2-有过期时间传3个参数 ,config 里面有 ttl、maxIdleTime、maxSize
  21. Cache cache = new RedissonCache(map, config, allowNullValues);
  22. // 3-事务提交 之后执行
  23. if (transactionAware) {
  24. cache = new TransactionAwareCacheDecorator(cache);
  25. }
  26. //4-不存在就添加
  27. Cache oldCache = instanceMap.putIfAbsent(name, cache);
  28. if (oldCache != null) {
  29. cache = oldCache;
  30. } else {
  31. map.setMaxSize(config.getMaxSize());
  32. }
  33. return cache;
  34. }

 上面  TransactionAwareCacheDecorator:

所执行的put操作,是在事务提交之后执行

  1. public void put(final Object key, @Nullable final Object value) {
  2. if (TransactionSynchronizationManager.isSynchronizationActive()) {
  3. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  4. public void afterCommit() {
  5. TransactionAwareCacheDecorator.this.targetCache.put(key, value);
  6. }
  7. });
  8. } else {
  9. this.targetCache.put(key, value);
  10. }
  11. }

 五、缓存工具类

private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);

主要是获取 CacheManager 接口,提供对缓存CRUD操作 :
  1. public interface CacheManager {
  2. @Nullable
  3. Cache getCache(String name);
  4. Collection<String> getCacheNames();
  5. }
 
  1. package com.ruoyi.common.utils.redis;
  2. import com.ruoyi.common.utils.spring.SpringUtils;
  3. import lombok.AccessLevel;
  4. import lombok.NoArgsConstructor;
  5. import org.redisson.api.RMap;
  6. import org.springframework.cache.Cache;
  7. import org.springframework.cache.CacheManager;
  8. import java.util.Set;
  9. /**
  10. * 缓存操作工具类 {@link }
  11. *
  12. * @author Michelle.Chung
  13. * @date 2022/8/13
  14. */
  15. @NoArgsConstructor(access = AccessLevel.PRIVATE)
  16. @SuppressWarnings(value = {"unchecked"})
  17. public class CacheUtils {
  18. private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
  19. /**
  20. * 获取缓存组内所有的KEY
  21. *
  22. * @param cacheNames 缓存组名称
  23. */
  24. public static Set<Object> keys(String cacheNames) {
  25. RMap<Object, Object> rmap = (RMap<Object, Object>) CACHE_MANAGER.getCache(cacheNames).getNativeCache();
  26. return rmap.keySet();
  27. }
  28. /**
  29. * 获取缓存值
  30. *
  31. * @param cacheNames 缓存组名称
  32. * @param key 缓存key
  33. */
  34. public static <T> T get(String cacheNames, Object key) {
  35. Cache.ValueWrapper wrapper = CACHE_MANAGER.getCache(cacheNames).get(key);
  36. return wrapper != null ? (T) wrapper.get() : null;
  37. }
  38. /**
  39. * 保存缓存值
  40. *
  41. * @param cacheNames 缓存组名称
  42. * @param key 缓存key
  43. * @param value 缓存值
  44. */
  45. public static void put(String cacheNames, Object key, Object value) {
  46. CACHE_MANAGER.getCache(cacheNames).put(key, value);
  47. }
  48. /**
  49. * 删除缓存值
  50. *
  51. * @param cacheNames 缓存组名称
  52. * @param key 缓存key
  53. */
  54. public static void evict(String cacheNames, Object key) {
  55. CACHE_MANAGER.getCache(cacheNames).evict(key);
  56. }
  57. /**
  58. * 清空缓存值
  59. *
  60. * @param cacheNames 缓存组名称
  61. */
  62. public static void clear(String cacheNames) {
  63. CACHE_MANAGER.getCache(cacheNames).clear();
  64. }
  65. }

 

六、缓存雪崩

概念:

        缓存集中过期失效(大量key失效)。所有请求直接查询数据库了,而对数据库造成巨大压力,严重可能的会导致数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

解决:

  • 1、实现Redis的高可用、改为主从+哨兵集群模式
  • 2、允许的话,也可以设置热点数据不过期(或者不同业务设置不同过期时间 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500)
  • 3、开启Redis的RDB+AOF组合持久化策略,以便快速恢复

 

七、缓存击穿 

概念:

缓存击穿指的是热点key在某个特殊的场景时间内恰好失效了,恰好有大量并发请求过来了,导致大量的请求都打到数据库上,造成数据库极大的压力,这就是缓存击穿问题。

对比缓存雪崩:        

        雪崩大量key失效,击穿 某几个热点key失效

 解决:

互斥锁方案,保证同一时间只有一个业务线程去数据库获取数据填充到Redis中,更新缓存,未能获取互斥锁的请求,需要等待锁释放后重新读取缓存。获取成功直接返回结果获取失败则再次尝试获取锁,重复上述流程

若依框架中的实现:简单举例

        sync = true  同步阻塞:同时进来多个请求, 等待前面调用返回并缓存,才能回进入下个请求
  

  1. /**
  2. * <简述> sync = true
  3. * 同步阻塞:同时进来多个请求, 等待前面调用返回并缓存,才能回进入下个请求
  4. *
  5. * @author syf
  6. * @date 2024/5/7 11:03
  7. * @param id
  8. * @return java.lang.String
  9. */
  10. @Cacheable(cacheNames = "cache4", key = "#id + '_cache'", sync = true)
  11. @GetMapping("test4")
  12. public String test4(String id){
  13. return null;
  14. }

四、缓存穿透

概念:

用户在不断访问一个在缓存和数据库中都没有的数据,缓存无法命中,从而导致一直请求数据库,流量过大就会导致数据库的崩溃,这就是缓存穿透问题。

 解决:

  • 接口层增加校验,如用户鉴权等;
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
  • 将空结果(NULL)或默认查询结果存入到缓存中,并设置值过期时间。

布隆过滤器可以参考文章:

redis中布隆过滤器使用详解_redis布隆过滤器使用-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/w1014074794/article/details/129750865 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/568370
推荐阅读
相关标签
  

闽ICP备14008679号