一、缓存机制的原理
- 一个系统在面向用户使用的时候,当用户的数量不断增多,那么请求次数也会不断增多,当请求次数增多的时候,就会造成请求压力,而我们当前的所有数据查询都是从数据库MySQL中直接查询的,那么就可能会产生如下问题
- 频繁访问数据库,数据库访问压力大,系统性能下降,用户体验差
- 解决问题的方法
- 要解决上述提到的问题,就可以使用前面学习的Redis技术,通过Redis实现缓存机制,从而降低数据库的访问压力;提高系统的访问性能,从而提升用户体验
- 加入Redis后,在进行数据查询的时候,就需要先查询缓存,如果缓存中有数据,直接返回;如果没有相对应的数据,那么就去查询数据库,再将数据库查询的结果,缓存在Redis中
二、缓存短信验证码
环境搭建
- ①、在项目pom.xml文件中导入spring-data-redis的maven坐标
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
复制代码
- ②、在项目的配置文件中加入Redis相关配置(在Spring层级下)
- redis:
- jedis:
- pool:
- max-idle: 5 #最大链接数,连接池中最多有10个
- min-idle: 1 # 最大空闲数
- max-wait: 1000ms #连接池最大阻塞等待时间
- max-active: 10 #最大链接数
- host: 127.0.0.1
- port: 6379
- database: 2
- # password:
复制代码
2.1、思路分析
- 前面实现的移动端手机验证登录功能,随机生成的验证码是保存在HttpSession当中的。但是实际的业务场景中,一般验证码都是需要设置过期时间的,如果存在HttpSession中就无法设置过期时间,此时我们就需要对这一块的功能进行优化
- 可以将验证码缓存在Redis中,具体的实现思路如下
- ①、在服务端UserController中注入RedisTemplate对象,用于操作Redis
- ②、在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分组
- ③、在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
2.2、代码改造
- ①、在UserController中注入RedisTemplate对象,用于操作Redis
- @Autowired
- private RedisTemplate<String, String> redisTemplate;
复制代码
- ②、在UserController的sendMsg方法中,将生成的验证码保存到Redis中(为了测试方便,这里是直接生成了固定的验证码,没有调用真实的生成验证码的API)
- // 将登录账号的信息存储在redis中
- // 获取字符串的客户端
- ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
- // 存储验证码,让验证码失效时间是一分钟
- valueOperations.set("SMS_" + user.getPhone(), code, 1, TimeUnit.MINUTES);
复制代码
- ③、在登录校验的代码中实现从Redis中取出数据
- // 2. 获取正确的验证码
- // String verifyCode = (String) session.getAttribute("SMS_" + inputPhone);
- // 从redis中获取正确的验证码
- String verifyCode = redisTemplate.opsForValue().get("SMS_" + inputPhone);
复制代码
2.3、功能测试
- ①、访问前端,获取验证码

- 通过控制台的日志,可以看到生成的代码

- ②、通过Redis的图形化界面工具查看Redis中的数据
- ③、在登录界面填写验证码登录完成后,查看Redis中的数据是否删除
三、缓存菜品信息
3.1、思路分析
- 之前项目中已经实现了移动端菜品查看的功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件(categoryId)进行数据库查询操作。
- 在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长;针对这个问题,可以对此方法进行缓存优化,提高系统的性能
- 那么应该缓存多少分数据呢?是所有的菜品缓存一份,还是需要根据分类的不同,缓存多份?
- 很显然,在前端点击一个分类的时候,展示的就是这个分类下的菜品,其他菜品无需展示
- 所以,这里面我们在缓存时,可以根据菜品的分类,缓存多分数据,页面在提交查询请求的时候,就查询该分类下的菜品缓存数据
- 具体实现思路
- ①、修改业务层的list方法,先从Redis中获取分类对应的菜品数据,如果有则直接返回,无需查询数据库;如果没有,则查询数据库,并将查询到的菜品数据存入Redis
- ②、修改DishController的save和update方法,加入清理缓存的逻辑
注意事项
- 在使用缓存的过程当中,要注意保证数据库中的数据和缓存中的数据保持一致
- 如果数据库中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况
3.2、代码改造
3.2.1、查询菜品缓存
在增加缓存之前,需要对存储进Redis中的数据进行一个简单的设计,如下所示
数据类型key值value值Stringdish_菜品分类的id菜品的List集合(List)
- ①、在DishServiceImpl中注入RedisTemplate
- @Autowired
- private RedisTemplate<String, String> redisTemplate;
复制代码
- ②、在list方法中,查询数据库之前,先查询缓存,如果缓存有数据,则直接返回
- // 根据分类id查询菜品列表数据
- @Override
- public List<DishDto> selectByCategoryIdAndStatus(Long categoryId, Integer status) {
- // 0. 首先先判断Redis中是否存在缓存
- // 获取redis操作字符串的客户端
- ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
- List<DishDto> dishDtoList = JSON.parseObject(valueOperations.get("dish_" + categoryId + "_" + status), List.class);
- // 如果redis中不存在这个缓存,则查询数据库,并且将查询到的结果存储到缓存中
- if (dishDtoList == null) {
- // 1. 调用 dao 层对象执行sql语句查询数据
- List<Dish> dishList = dishMapper.selectByCategoryIdAndStatus(categoryId, status);
- // 2. 遍历dishList,查询其相对应的口味数据表
- dishDtoList = dishList.stream().map(dish -> {
- // 查询对应的口味表
- List<DishFlavor> dishFlavorList = dishFlavorMapper.selectByDishId(dish.getId());
- DishDto dishDto = new DishDto();
- // 将数据封装到dishDto中
- dishDto.setFlavors(dishFlavorList);
- // 将基本属性复制给dishDto
- BeanUtils.copyProperties(dish, dishDto);
- return dishDto;
- }).collect(Collectors.toList());
- // 把查询到的数据,存储到Redis中
- // 把dishDtoList对象转换为Json格式
- String dishJson = JSON.toJSONString(dishDtoList);
- valueOperations.set("dish_" + categoryId, dishJson, 2, TimeUnit.DAYS);
- }
- // 返回数据
- return dishDtoList;
- }
复制代码
3.2.2、清理菜品缓存
为了保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据
所以,需要在菜品的增删改中清空缓存数据
- 清理菜品缓存的方式有两种
- A、清理所有分类下的菜品缓存
- //清理所有菜品的缓存数据
- Set keys = redisTemplhate.keys("dish_*"); //获取所有以dish_xxx开头的key
- redisTemplate.delete(keys); //删除这些key
复制代码
- B、清理当前添加菜品分类下的缓存
- //清理某个分类下面的菜品缓存数据
- String key = "dish_" + dishDto.getCategoryId();
- redisTemplate.delete(key);
复制代码
- 两者的优劣(需要结合实际的业务场景考虑)
- 对于这次的修改操作,用户可以修改菜品的分类,如果用户修改了了菜品的分类,那么原来的分类下将少一个菜品,新的分类下将多一个菜品,这样的话,两个分类的菜品列表数据都发生了变化
- 即此时的情况不能只是删除某一个分类的菜品缓存
- 所以,在本次的系统中推荐使用第一种方法清理菜品缓存
- 这里清理缓存的操作比较简单,就不演示了,只需要在数据发生变更后的代码后面添加一个删除缓存的代码即可,如下所示
3.3、功能测试
- ①、访问移动端,根据分类查询菜品列表,然后再检查Redis的缓存数据是否存在
- ②、当对菜品进行增删改的时候,查询Redis中的缓存数据,是否被清除
四、Spring Cache
4.1、Spring Cache介绍
- Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码
- Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口
- 针对不同的缓存技术需要实现不同的CacheManager,如下表所示
- CacheManager描述EhCacheCacheManager使用EhCache作为缓存技术GuavaCacheManager使用Google的GuavaCache作为缓存技术RedisCacheManager使用Redis作为缓存技术spring 自己也搞了一套缓存技术,默认的缓存
spring缓存是缓存在Map集合中
4.2、Spring Cache注解
- 在SpringCache中提供了很多缓存操作的注解,常见的几个如下所示
- 注解说明@EnableCaching开启缓存注解功能@Cacheable在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,则调用方法并将方法返回值放到缓存中@CachePut将方法的返回值或者参数放到缓存中@CacheEvict将一条或多条数据从缓存中删除
- 在SpringBoot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可
- 例如使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标,同时在配置文件中配置Redis的相关配置即可
4.3、Spring Cache入门案例
- 接下来,我们可以通过一个入门案例演示以下SpringCache的常见用法。上面提到,SpringCache可以集成不同的缓存技术,如Redis、Ehcache甚至我们可以使用Map来缓存数据,接下来我们在演示的时候,就先通过一个Map来缓存数据,最后我们再换成Redis来缓存
4.3.1、环境准备
- ①、数据库准备
- /*
- SQLyog Ultimate v11.33 (64 bit)
- MySQL - 5.5.40 : Database - cache_demo
- *********************************************************************
- */
- /*!40101 SET NAMES utf8 */;
- /*!40101 SET SQL_MODE=''*/;
- /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
- /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
- /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
- /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
- CREATE DATABASE /*!32312 IF NOT EXISTS*/`cache_demo` /*!40100 DEFAULT CHARACTER SET utf8 */;
- USE `cache_demo`;
- /*Table structure for table `user` */
- DROP TABLE IF EXISTS `user`;
- CREATE TABLE `user` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `name` varchar(255) DEFAULT NULL,
- `age` int(11) DEFAULT NULL,
- `address` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
- /*Data for the table `user` */
- LOCK TABLES `user` WRITE;
- UNLOCK TABLES;
- /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
- /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
- /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
- /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
复制代码
- ②、导入基本工程
- 基本工程的创建,这里就不演示了,只是一个User表的增删改查操作

- ③、注入CacheManager
- 我们可以在UserController注入一个CacheManager,在Debug时,我们可以通过CacheManger跟踪缓存中数据的变化

- 我们可以进入CacheManger接口的源码中查看,默认的实现有几种,如下图所示

- 而在上述的几个实现中,默认使用的是ConcurrentMapCacheManger,稍后我们可以通过断点的形式跟踪缓存数据的变化
- ④、启动类加上@EnableCaching注解
- 在启动类加上该注解,就代表当前项目开启缓存注解功能

4.3.2、@CachePut注解
- @CachePut注解说明
- 作用
- value
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
- ①、在save方法上加上注解@CachePut
- 当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,如下所示
- /**
- * CachePut:将方法返回值放入缓存
- * value:缓存的名称,每个缓存名称下面可以有多个key
- * key:缓存的key
- */
- @CachePut(value = "userCache", key = "#user.id")
- @PostMapping
- public User save(@RequestBody User user){
- userService.save(user);
- return user;
- }
复制代码key的写法如下:
- #user.id
- #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key
- #user.name
- #user指的是方法形参的名称, name指的是user的name属性 ,也就是使用user的name属性作为key
- #result.id
- #result代表方法返回值,该表达式 代表以返回对象的id属性作为key
- #result.name
- #result代表方法返回值,该表达式 代表以返回对象的name属性作为key
- ②、使用Postman进行功能测试
- 启动服务,通过postman请求访问UserController的方法, 然后通过断点(Debug)的形式跟踪缓存数据
- 第一次访问时,缓存中的数据是空的,因为save方法执行完毕后才会缓存数据
- 第二次访问时,我们通过debug可以看到已经有一条数据了,就是上次保存的数据,已经缓存了,缓存的key就是用户的id
- PS
- 上述的演示,最终的数据,实际上是缓存在ConcurrentHashMap中,那么当我们的服务器重启之后,缓存中的数据就会丢失。 后面使用了Redis来缓存就不存在这样的问题
4.3.3、@CacheEvict注解
- @CacheEvict注解说明
- 作用
- value
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
- ①、在delete方法上加@CacheEvict注解
- 当我们在删除数据库user表数据的时候,需要删除缓存中对应的数据,此时就可以使用@CacheEvict注解,如下所示
- /**
- * CacheEvict:清理指定缓存
- * value:缓存的名称,每个缓存名称下面可以有多个key
- * key:缓存的key
- */
- @CacheEvict(value = "userCache", key = "#p0")
- //#p0 代表第一个参数
- //@CacheEvict(value = "userCache",key = "#root.args[0]") //#root.args[0] 代表第一个参数
- //@CacheEvict(value = "userCache",key = "#id") //#id 代表变量名为id的参数
- @DeleteMapping("/{id}")
- public void delete(@PathVariable Long id){
- userService.removeById(id);
- }
复制代码
- ②、使用Postman进行功能测试
- 测试缓存的删除,先访问save方法任意次,保存n条数据到数据库的同时,也保存到缓存中,最终可以通过debug看到缓存中的数据信息,然后可以通过Postman方法delete方法,进行缓存的删除
- 删除数据的时候,通过debug可以看到已经缓存的4条数据
- 当执行完delete操作后,我们再保存一条数据,在保存的时候debug查看之前的缓存是否已经被删除
- ③、在update方法上加注解@CacheEvict
- 在更新数据之后,数据库的数据已经发生了变更,我们需要将缓存中对应的数据删除掉,避免出现数据库数据与缓存数据不一致的情况
- //@CacheEvict(value = "userCache",key = "#p0.id") //第一个参数的id属性
- //@CacheEvict(value = "userCache",key = "#user.id") //参数名为user参数的id属性
- //@CacheEvict(value = "userCache",key = "#root.args[0].id") //第一个参数的id属性
- @CacheEvict(value = "userCache",key = "#result.id") //返回值的id属性
- @PutMapping
- public User update(@RequestBody User user){
- userService.updateById(user);
- return user;
- }
复制代码
- 加上注解之后,重启服务,然后使用Postman进行测试,测试步骤和方法跟上述①、②差不多,这里就不再演示
4.3.4、@Cacheable注解
- @Cacheable注解说明
- 作用
- 在方法执行前,Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放入到缓存中
- value
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
- ①、在getById方法上加@Cacheable注解
- @Cacheable(value = "user",key = "#id")
- @GetMapping("/{id}")
- public User getById(@PathVariable Long id){
- User user = userService.findById(id);
- return user;
- }
复制代码
- ②、使用Postman进行功能测试
- 重启服务,然后通过debug断点跟踪程序执行;可以发现,第一次访问,会请求Controller的方法,查询数据库。后面再查询相同的id,就直接获取到数据库,不用再查询数据库了,就说明缓存已经生效
- 在测试的时候,查询一个数据库中不存在的id值,第一次查询缓存中没有,也会查询数据库。第二次查询的时候,会发现,不再查询数据库了,而是直接返回,那也就是说如果根据id没有查询到数据,那么会自动缓存一个null值,可以通过debug进行验证一下
- 这时候就会出现一个问题,能不能查询到的值不为null值的时候再进行缓存,如果为null值,则不进行缓存呢?
- ③、缓存非null值
- 在@Cacheable注解中,提供了两个属性分别为:condition、unless
- condition
- unless
- 表示满足条件则不缓存,与上述的condition是反向的
- 具体实现方法如下所示
- /**
- * 注意: @Cacheable把方法的返回值缓存起来, 即使方法返回值为null也会被缓存,如果需要改变这个结果:
- * condition : 符合指定条件则缓存,这个属性不建议使用,因为condition这个属性不能使用result。
- * unless : 不符合指定条件则缓存
- */
- // @Cacheable 执行方法前先判断缓存是否存在指定id的user,
- // 如果存在不会执行方法,直接返回缓存中数据即可。如果不存在才会返回缓存数据
- @Cacheable(value = "user",key = "#id",unless = "#result==null")
- @GetMapping("/{id}")
- public User getById(@PathVariable Long id){
- User user = userService.findById(id);
- return user;
- }
复制代码
- 这里这能使用unless,因为condition属性无法获取到结果#result
4.4.、Spring Cache集成Redis
- 在使用上述默认的ConcurrentHashMap做缓存时,服务重启之后,之前缓存的数据就全部丢失了,操作起来并不友好。在项目中使用,我们会选择使用redis来做缓存,主要需要操作以下几步
- ①、添加依赖
- org.springframework.boot spring-boot-starter-cache
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
复制代码
- ②、配置application.yaml配置文件(Spring层级下)
- redis:
- host: 127.0.0.1
- database: 2
- cache:
- redis:
- time-to-live: 1800000 #单位毫秒,设置缓存过期时间,可选
复制代码
- ③、测试
五、缓存套餐数据
5.1、思路分析
- 前面已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作
- 在高并发的情况下,频繁地查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能
5.2、代码改造
- ①、导入SpringCache和Redis相关的maven坐标(spring data redis之前已经导入过了)
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
复制代码
- ②、在application.yml配置文件中配置缓存数据的过期时间
- cache:
- redis:
- time-to-live: 1800000 # 设置缓存数据过期时间
复制代码
- ③、在启动类上加入@EnableCaching注解,开启缓存注解功能
- package com.coolman;
- import org.mybatis.spring.annotation.MapperScan;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.boot.web.servlet.ServletComponentScan;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.transaction.annotation.EnableTransactionManagement;
- @SpringBootApplication
- @MapperScan(basePackages = "com.coolman.mapper")
- @ServletComponentScan(basePackages = "com.coolman.filters")
- @EnableTransactionManagement //开启对事务管理的支持
- @EnableCaching
- public class ReggieApplication {
- public static void main(String[] args) {
- SpringApplication.run(ReggieApplication.class, args);
- }
- }
复制代码
- ④、在SetmealServiceImpl的selectByCategoryIdAndStatus方法上加入@Cacheable注解
- 在进行套餐数据查询时,需要根据分类ID和套餐的状态进行查询,所以在缓存数据的时候,可以将套餐分类id和套餐状态组合起来作为key,如下所示
- 1627182182_1(1627182182是分类id,1是状态)
- @Override
- @Cacheable(value = "setmeal", key = "#categoryId + '_' + #status")
- public List<SetMeal> selectByCategoryIdAndStatus(Long categoryId, Integer status) {
- return setMealMapper.selectByCategoryIdAndStatus(categoryId, status);
- }
复制代码
- ⑤、在SetmealServiceImpl的数据变更方法上加入@CacheEvict注解
- 为了保证数据库中数据与缓存数据的一致性,在添加套餐或者删除套餐数据之后,需要清空当前套餐缓存的全部数据
- 那么@CacheEvict注解如何清除某一份缓存下所有的数据呢?这里可以指定@CacheEvict中的一个属性allEnties,将其设置为true即可,其含义为setmeal名称空间下面的所有key都删除
- @CacheEvict(value = "setmeal", allEntries = true) // allEntries = true 代表了setmeal名称空间下面的所有key都删除
- public void update(SetMealDto setMealDto) {
- // 补全数据,更新时间
- setMealDto.setUpdateTime(LocalDateTime.now());
- // 更新setmeal表的数据
- setMealMapper.updateByIds(setMealDto, new Long[]{setMealDto.getId()});
- // 更新setmeal_dish表的数据
- // 先删除,再修改
- // 原因: 前端返回的数据是一个集合,有多个id,同时其中的数据也不是固定不变的,且是一对多关系,处理起来非常麻烦
- // 给setmeal_dish补全数据
- List<SetMealDish> setmealDishes = setMealDto.getSetmealDishes();
- for (SetMealDish setmealDish : setmealDishes) {
- // 设置创建人和创建时间
- setmealDish.setCreateUser(setMealDto.getCreateUser());
- setmealDish.setCreateTime(setMealDto.getCreateTime());
- // 设置更新时间和更新人
- setmealDish.setUpdateUser(setMealDto.getUpdateUser());
- setmealDish.setUpdateTime(LocalDateTime.now());
- // 设置setmeal_id
- setmealDish.setSetmealId(setMealDto.getId().toString());
- // 设置sort
- setmealDish.setSort(0);
- }
- // 根据id批量删除数据
- ArrayList<String> ids = new ArrayList<>();
- ids.add(setMealDto.getId().toString());
- setMealDishMapper.deleteByIds(ids);
- // 批量插入数据
- setMealDishMapper.batchInsert(setmealDishes);
- }
复制代码- 上述代码只是修改功能的一个方法,其他有数据变更的操作,一般都要清理缓存,以保证数据库的数据和缓存的数据一致
5.3、功能测试
- 代码编写完成之后,重启工程,然后访问后台管理系统,对套餐数据进行新增以及删除, 然后通过Redis的图形化界面工具,查看Redis中的套餐缓存是否已经被删除
- ①、第一次查询套餐列表,查看Redis中是否存储相对应的数据
- ②、第二次查询套餐列表,查看服务端终端输出的是否有SQL语句,验证是否是从Redis中读取数据
- ③、执行数据变更操作(这里测试使用添加功能),查看Redis中的缓存数据是否删除


- 这里有部分dish值出现null字符的原因是之前在没单纯使用RedisTemplate对象的时候没有删除测试代码,所以不管传入的status值是否为null值,其都会存储到缓存中,不过在这里无伤大雅,仅作为测试,在实际应用场景中,非常不建议同时使用RedisTemplate和Spring redis data
- 到这里,项目的缓存优化结束!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |