SpringCloud(Hoxton.SR3)基础篇:第四章、Hystrix请求熔断与服务降级

一、Hystrix简介:

  我们知道大量请求会阻塞在Tomcat服务器上,影响其它整个服务.在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败.高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险.

  Spring Cloud Netflix Hystrix就是隔离措施的一种实现,可以设置在某种超时或者失败情形下断开依赖调用或者返回指定逻辑,从而提高分布式系统的稳定性.生活中举个例子,如电力过载保护器,当电流过大的的时候,出问题,过载器会自动断开,从而保护电器不受烧坏。因此Hystrix请求熔断的机制跟电力过载保护器的原理很类似。

 

如下图所示:

 

 

Hystrix设计原则

  1. 防止单个服务的故障,耗尽整个系统服务的容器(比如tomcat)的线程资源,避免分布式环境里大量级联失败。通过第三方客户端访问(通常是通过网络)依赖服务出现失败、拒绝、超时或短路时执行回退逻辑
  2. 用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复
  3. 提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求
  4. 将所有请求外部系统(或请求依赖服务)封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待

Hystrix特性

  1. 请求熔断: 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN).这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
  2. 服务降级:Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.告知后面的请求服务不可用了,不要再来了。
  3. 依赖隔离(采用舱壁模式,Docker就是舱壁模式的一种):在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池.比如说,一个服务调用两外两个服务,你如果调用两个服务都用一个线程池,那么如果一个服务卡在哪里,资源没被释放后面的请求又来了,导致后面的请求都卡在哪里等待,导致你依赖的A服务把你卡在哪里,耗尽了资源,也导致了你另外一个B服务也不可用了。这时如果依赖隔离,某一个服务调用A B两个服务,如果这时我有100个线程可用,我给A服务分配50个,给B服务分配50个,这样就算A服务挂了,我的B服务依然可以用。
  4. 请求缓存:比如一个请求过来请求我userId=1的数据,你后面的请求也过来请求同样的数据,这时我不会继续走原来的那条请求链路了,而是把第一次请求缓存过了,把第一次的请求结果返回给后面的请求。
  5. 请求合并:我依赖于某一个服务,我要调用N次,比如说查数据库的时候,我发了N条请求发了N条SQL然后拿到一堆结果,这时候我们可以把多个请求合并成一个请求,发送一个查询多条数据的SQL的请求,这样我们只需查询一次数据库,提升了效率。

Hystrixl流程图如下:

  

 

 Hystrix流程说明:

  1:每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.
  2:执行execute()/queue做同步或异步调用.
  4:判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤5.
  5:判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤6.
  6:调用HystrixCommand的run方法.运行依赖逻辑
  6a:依赖逻辑调用超时,进入步骤8.
  7:判断逻辑是否调用成功
  7a:返回成功调用结果
  7b:调用出错,进入步骤8.
  8:计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态.
  9:getFallback()降级逻辑.以下四种情况将触发getFallback调用:
    (1):run()方法抛出非HystrixBadRequestException异常。
    (2):run()方法调用超时
    (3):熔断器开启拦截调用
    (4):线程池/队列/信号量是否跑满
  9a:没有实现getFallback的Command将直接抛出异常
  9b:fallback降级逻辑调用成功直接返回
  9c:降级逻辑调用失败抛出异常
  10:返回执行成功结果

 

 二、Hystrix入门案例:

 

  这里接着前面的Ribbon进行Hystrix集成。说白了你想对一个请求进行熔断,必然不能让客户直接去调用那个请求,你必然要要对别人的请求进行包装一层和拦截,才能做点手脚,比如进行熔断,所以说要在Ribbon上动手脚。因为它是请求发起的地方。
我们刚开始请求一个服务,为了负载均衡进行了拦截一次,现在我们要进行熔断,所以必须跟Ribbon集成一次,再进行请求拦截来熔断。

  1.引入Hystrix相关的依赖如下依赖所示:

<!-- hystrix依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  2.在启动类中加入@EnableCircuitBreaker注解,表示允许断路器。如下代码所示:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
//该注解表明应用既作为eureka实例又为eureka client 可以发现注册的服务
@EnableEurekaClient
//在启动该微服务的时候就能去加载我们的自定义Ribbon配置类
@RibbonClient(name = "provider-user")
//Hystrix启动类注解,允许断路器
@EnableCircuitBreaker
public class ConsumerMovieRibbonApplication {
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    public static void main(String[] args) {
        SpringApplication.run(ConsumerMovieRibbonApplication.class, args);
    }
}

  3.在Controller端代码加上@HystrixCommand注解

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.qxj.cloud.entity.User;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 注解方式使用
     * 
     * @param id
     * @return
     */
    @RequestMapping(value = "/movie/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @HystrixCommand(fallbackMethod = "findByIdFallback")
    public User findById(@PathVariable Long id) {
        // 微服务的虚拟id http://provider-user
        User user = this.restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
        return user;
    }

    // findById的fallback方法
    public User findByIdFallback(Long id) {
        User user = new User();
        user.setId(0L);
        return user;
    }
}

  4.application.yml配置,使用Ribbon的yml配置

 

server:
  port: 8010
spring:
  application:
      name: consumer-movie-ribbon-with-hystrix
#eureka客户端连接配置
eureka:
   client:
      healthcheck:
         enabled: true
      service-url:
      #注册中心地址
         defaultZone: http://user:password123@localhost:8761/eureka
   instance:
      #将ip注册到eureka上
      prefer-ip-address: true
      #微服务向eureka注册实例名${spring.cloud.client.ip-address} 表示ip地址 spring2.0以上为ip-address
      instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}

 

  5.启动微服务

  先启动eureka注册中心,和provider-user微服务,接着再把Ribbon微服务启动起来,在浏览器上输入http://localhost:8010/movie/3

   停止provider-user微服务,再次在浏览器请求http://localhost:8010/movie/3

 

  到这里简单演示了用Hystrix的注解@HystrixCommand(fallbackMethod = "findByIdFallback"),来实现熔断和服务降级。这只是表面的东西而已,根本不清楚他背后的原理

 三、灵活的熔断和服务降级

  1.Hystrix给我们提供了HystrixCommand类,让我们去继承它,去实现灵活的熔断和服务降级。实现类如下

import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.qxj.cloud.entity.User;

/**
 * 自定义HystrixCommand实现
 * @author computer
 *
 */
public class UserServiceCommand extends HystrixCommand<User>{
    
    private RestTemplate restTemplate;
    private Long id;
    
    //自定义构造函数
    public UserServiceCommand(String group,RestTemplate restTemplate,Long id) {
        super(HystrixCommandGroupKey.Factory.asKey(group));
        this.restTemplate = restTemplate;
        this.id = id;
    }

    @Override
    protected User run() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return this.restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
    }

    @Override
    protected User getFallback() {
        User user = new User();
        user.setId(0L);
        return user;
    }
}

  2.Controller层的代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.qxj.cloud.entity.User;
import com.qxj.cloud.service.UserServiceCommand;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 自定义实现类使用HystrixCommand
     * 
     * @param id
     * @return
     */
    @RequestMapping(value = "/movieHystrixDiy/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    public User findByIdDiy(@PathVariable Long id) {
        UserServiceCommand command = new UserServiceCommand("findByIdDiy", restTemplate, id);
        User user = command.execute();
        return user;
    }
}

在这里我们要注意一下,虽然我们在这里new了个UserServiceCommand,但是并没有调用UserServiceCommand的方法,而是用command.execute();方法来手工执行的。

  3.服务测试

  先启动eureka注册中心,和provider-user微服务,接着再把Ribbon微服务启动起来,在浏览器上输入http://localhost:8010/movieHystrixDiy/3

 

 

   停止provider-user微服务,再次在浏览器请求http://localhost:8010/movieHystrixDiy/3

  注意:如果provider-user是集群部署的,有多台服务停供使用,当其中一台服务宕机的时候,导致服务不可访问了,返回我们原先在代码中定义的服务降级后的结果id=0的User对象回来,当后面一段时间内还有请求再也不会轮询宕机的服务节点

 

 四、非阻塞式IO

  restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class)这是阻塞式的,因为这是阻塞式的,如果后面还有代码,必须等到网络请求restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class)返回结果后,你后面的代码才会执行。如果此刻,有一个请求过来,通过Ribbon客户端进来了,Ribbon客户端调用了三个服务,每一服务执行的时间都是2秒钟,那么这三个服务都是用阻塞IO来执行的话,那么耗时是2+2+2=6,一共就花了6秒钟。那么如果我们使用异步来执行的话,花费的时间就是这三个服务中哪一个耗时长就是总耗时时间,比如,此时耗时最多的一个服务是3秒钟,那么总共耗时就花了3秒钟。那么异步IO是什么意思呢?就是请求发出去以后,主线程不会在原地等着,会继续往下执行我的主线程,什么时候返回结果,我就什么时候过去取出来。等着三个服务执行完了我就一次性把结果取出来。

  非阻塞式IO有两个分别是:Future将来式,Callable回调式

 

  1).Future将来式:就是说你用Future将来式去请求一个网络IO之类的任务,它会以多线程的形式去实现,主线程不必卡死在哪里等待,等什么时候需要结果就通过Future的get()方法去取,不用阻塞。

  2).Callable回调式:预定义一个回调任务,Callable发出去的请求,主线程继续往下执行,等你请求返回结果执行完了,会自动调用你哪个回调任务。

  Future将来式入门实例

  1.自定义继承HystrixCommand的Future示例

    UserServiceCommand类几面不用变,只需要改变一下在Controller层的command的调用方式即可,command的调用方式如下:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.qxj.cloud.entity.User;
import com.qxj.cloud.service.UserServiceCommand;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 非阻塞式IO 自定义实现类使用HystrixCommand
     * 
     * @param id
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @RequestMapping(value = "/movieHystrixDiyFuture/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    public User findByIdDiyFuture(@PathVariable Long id) throws InterruptedException, ExecutionException {
        UserServiceCommand command = new UserServiceCommand("findByIdDiyFuture", restTemplate, id);
        Future<User> future = command.queue();
        return future.get();
    }
}

  2.@HystrixCommand注解方式的Future示例

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.command.AsyncResult;
import com.qxj.cloud.entity.User;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    // findById的fallback方法
    public User findByIdFallback(Long id) {
        User user = new User();
        user.setId(0L);
        return user;
    }

    /**
     * 非阻塞式IO 注解方式使用
     * 
     * @param id
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @RequestMapping(value = "/movieHystrixFuture/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @HystrixCommand(fallbackMethod = "findByIdFallback")
    public User findByIdFuture(@PathVariable Long id) throws InterruptedException, ExecutionException {

        Future<User> future = new AsyncResult<User>() {

            @Override
            public User get() {
                return invoke();
            }

            @Override
            public User invoke() {
                System.out.println("-------外部访问");
                return restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
            }
        };

        User user = future.get();
        System.out.println("-------执行");
        // 微服务的虚拟id http://provider-user
        return user;
    }
}

运行结果跟上面的一样。

 

五、HystrixObservableCommand请求多个服务

  那么接下来我们又有另外一个需求就是,我发多个请求出去请求多个服务,我需要把请求结果汇总起来,一起返回给我,上面的例子,什么同步异步都不太好办。很麻烦,要写N个Future。

  这时候Hystrix又给我们提供了另外一种模式HystrixObservableCommand来让我们继承这个类,其实这种模式就运用了Java的RX编程中的观察者模式,如下:

 

    1.自定义继承HystrixObservableCommand的示例 

  接下来我们新建一个名为UserServiceObservableCommand的类,来继承Hystrix给我们提供的HystrixObservableCommand类,同样HelloServiceObserveCommand类也不是交由Spring管理的,需要我们通过Controller层注入RestTemplate,放在构造方法来注入,代码如下所示:

import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixObservableCommand;
import com.qxj.cloud.entity.User;

import rx.Observable;
import rx.Subscriber;

/**
 * 自定义HystrixObservableCommand实现
 * @author computer
 *
 */
public class UserServiceObservableCommand extends HystrixObservableCommand<User>{
    
    private RestTemplate restTemplate;
    private Long id;
    
    public UserServiceObservableCommand(String group,RestTemplate restTemplate,Long id) {
        super(HystrixCommandGroupKey.Factory.asKey(group));
        this.restTemplate=restTemplate;
        this.id = id;
    }

    @Override
    protected Observable<User> construct() {
        Observable<User> observable = Observable.unsafeCreate(new Observable.OnSubscribe<User>() {

            @Override
            public void call(Subscriber<? super User> subscriber) {
                if(!subscriber.isUnsubscribed()) {
                    System.out.println("方法执行....");
                    
                    User user = restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
                    
                    //这个方法是监听方法,是传递结果的,请求多次的结果通过它返回去汇总起来。
                    subscriber.onNext(user);
                    
                    User user2 = restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
                    //这个方法是监听方法,传递结果的
                    subscriber.onNext(user2);
                    
                    subscriber.onCompleted();
                }
            }
        });
        return observable;
    }
    
    @Override
    protected Observable<User> resumeWithFallback() {
        Observable<User> observable = Observable.unsafeCreate(new Observable.OnSubscribe<User>() {
            @Override
            public void call(Subscriber<? super User> subscriber) {
                if (!subscriber.isUnsubscribed()) {
                    User user = new User();
                    user.setId(0L);
                    subscriber.onNext(user);
                    subscriber.onCompleted();
                }
            }
            
        });
        return observable;
    }
}

  Controller层调用如下代码所示

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.qxj.cloud.entity.User;
import com.qxj.cloud.service.UserServiceObservableCommand;

import rx.Observable;
import rx.Observer;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;


    /**
     * 请求多个服务
     * 自定义实现类使用HystrixObservableCommand
     * 
     * @param id
     * @return
     * @throws InterruptedException
     */
    @RequestMapping(value = "/movieHystrixDiyMreq/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    public List<User> findByIdDiyMreq(@PathVariable Long id) throws InterruptedException{
        //结果集
        List<User> list = new ArrayList<User>();
        
        UserServiceObservableCommand command = new UserServiceObservableCommand("findByIdDiyMreq", restTemplate, id);
        //热执行  不等待 事件注册完(onCompleted(),onError,onNext这三个事件注册)执行业务代码construct方法
        //Observable<User> observable = command.observe();
        //冷执行  等待 事件注册完(onCompleted(),onError,onNext这三个事件注册)才执行业务代码call方法
        Observable<User> observable = command.toObservable();
        //订阅
        observable.subscribe(new Observer<User>() {
            
            //请求完成的方法
            @Override
            public void onCompleted() {
                System.out.println("会聚完了所有查询请求");
            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace();
            }
            
            //订阅调用事件,结果会聚的地方,用集合去装返回的结果会聚起来。
            @Override
            public void onNext(User t) {
                System.out.println("结果来了.....");
                list.add(t);
            }
        });
        return list;
    }
}

运行结果如下:

 

 

  前面的例子有异步和同步这两种方式,这里HystrixObservableCommand也有两个中执行方式,分别是,冷执行,和热执行。刚刚HystrixObservableCommand中的command.toObservable()冷执行方式。

  什么是热执行方式呢?

    所谓的热执行就是不管你事件有没有注册完(onCompleted(),onError,onNext这三个事件注册)就去执行我的业务方法即(HystrixObservableCommand实现类中的construct()方法)

 

  什么是冷执行呢?

    所谓的冷执行就是,先进行事件监听方法注册完成后,才执行业务方法

 

  2.注解使用HystrixObservableCommand的示例 

  Controller层调用如下代码所示

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.ObservableExecutionMode;
import com.qxj.cloud.entity.User;

import rx.Observable;
import rx.Subscriber;

@RestController
public class MovieController {

    @Autowired
    private RestTemplate restTemplate;

    // findById的fallback方法
    public User findByIdFallback(Long id) {
        User user = new User();
        user.setId(0L);
        return user;
    }
    
    /**
     * 请求多个服务
     * 注解方式实现类使用HystrixObservableCommand
     * ObservableExecutionMode.EAGER热执行  ObservableExecutionMode.LAZY冷执行
     * @param id
     * @return
     */
    @RequestMapping(value = "/movieHystrixMreq/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @HystrixCommand(fallbackMethod = "findByIdFallback",observableExecutionMode = ObservableExecutionMode.LAZY)
    public Observable<User> findByIdMreq(@PathVariable Long id) throws InterruptedException{
        Observable<User> observable = Observable.unsafeCreate(new Observable.OnSubscribe<User>() {
            @Override
            public void call(Subscriber<? super User> subscriber) {
                if(!subscriber.isUnsubscribed()) {
                    System.out.println("方法执行....");
                    
                    User user = restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
                    
                    //这个方法监听方法,是传递结果的,请求多次的结果通过它返回去汇总起来。
                    subscriber.onNext(user);
                    
                    User user2 = restTemplate.getForObject("http://provider-user:7900/simple/" + id, User.class);
                    //这个方法是监听方法,传递结果的
                    subscriber.onNext(user2);
                    
                    subscriber.onCompleted();
                }
            }
        });
        return observable;
    }
}

运行结果如下:

 参考文献:https://www.cnblogs.com/huangjuncong/p/9026949.html

 

 

posted @ 2020-04-19 19:25  圣痕道心  阅读(670)  评论(0编辑  收藏  举报