SpringCloud入门实例

SpringCloud微服务

概述

​ Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

​ ----来自百度

主流技术栈

服务注册中心 Eureka,Zookeeper,Consul

服务调用 openFeign

负载均衡 Ribbon

服务降级/服务熔断 Hystrix

服务网关 GetWay,Zuul

配置中心 Config

消息总线 Bus

消息驱动 Stream

链路跟踪 Sleuth

项目准备

父依赖说明

<!--统一管理jar包版本-->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <lombok.version>1.16.18</lombok.version>
    <log4j.version>1.2.17</log4j.version>
    <mysql.version>8.0.18</mysql.version>
    <druid.version>1.1.16</druid.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
  </properties>

<!--  子模块继承后,提供作用:锁定版本+子modlue不用写groupId和version-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-project-info-reports-plugin</artifactId>
        <version>3.0.0</version>
      </dependency>
      <!--spring boot 2.2.2-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud Hoxton.SR1-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring-cloud-alibaba 2.1.0.RELEASE-->
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.1.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--mysql-->
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
        <scope>runtime</scope>
      </dependency>
      <!-- druid-->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring.boot.version}</version>
      </dependency>
      <!--junit-->
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
      </dependency>
      <!--log4j-->
      <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
      </dependency>
    </dependencies>

  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>

Cloud-api-commons 自定义工具类

POM依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>

Payment实体类和commonResult实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    private  Long id;
    private  String serial;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommenResult<T> {
    private Integer code;
    private String message;
    private T data;

    public  CommenResult(Integer code,String message)
    {
        this(code,message,null);
    }
}

各个搭建思路

1.建module 2.改POM 3.写YML 4.主启动 5.业务类

支付模块Payment8001构建

POM依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--引入自己定义的api通用包,可以使用payment支付entity-->
        <dependency>
            <groupId>CR553</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml配置文件

server:
  port: 8001


spring:
  application:
    name: cloud-payment-service
  zipkin:
    base-url: http://localhost:9411
    sleuth:
      sampler:
        #采样率介于0到1之间,1表示全部采集
        probability: 1

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
    driver-class-name: com.mysql.jdbc.Driver   # mysql驱动包
    url: jdbc:mysql://127.0.0.1:3306/cloud2020?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
    username: root
    password: root

eureka:
  client:
  #表示是否将自己注册进EurekaServer默认为true
    register-with-eureka: true
  #是否从EurekaServer抓取已有的注册信息,默认为true.单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka
  instance:
    instance-id: payment8001
    prefer-ip-address: true #访问路径可以显示ip地址

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.CR553.springcloud.entities # 所有Entity别名类所在包

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class,args);
    }
}

Dao层

@Mapper//使用mybatis建议使用@Mapper注解
public interface PaymentDao {

    int create(Payment payment);

    Payment getPaymentById(@Param("id") Long id);
}

Mapper.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">

<mapper namespace="com.CR553.springcloud.dao.PaymentDao">
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values(#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="com.CR553.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"></id>
        <id column="serial" property="serial" jdbcType="VARCHAR"></id>
    </resultMap>
    <select id="getPaymentById" resultMap="BaseResultMap" parameterType="Long">
        select * from payment where id=#{id};
    </select>

</mapper>

service层

public interface PaymentService {
    int create(Payment payment);

    Payment getPaymentById(@Param("id") Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService {

    @Resource
    private PaymentDao paymentDao;

    @Override
    public int create(Payment payment) {
        return paymentDao.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
        return paymentDao.getPaymentById(id);
    }
}

controller层

@Controller
@Slf4j
public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @Resource
    private DiscoveryClient discoveryClient;

    @RequestMapping(value = "/payment/create")
    @ResponseBody
    public CommenResult create(@RequestBody Payment payment) {
        int result = paymentService.create(payment);
        log.info("******插入结果" + result);
        if (result > 0) {
            return new CommenResult(200, "插入数据库成功,端口为:" + serverPort, result);
        } else {
            return new CommenResult(444, "插入数据库失败", null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    @ResponseBody
    public CommenResult getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("******插入结果" + payment);

        if (payment != null) {
            return new CommenResult(200, "查询成功,端口为:" + serverPort, payment);
        } else {
            return new CommenResult(444, "没有改记录,查询id:" + id, null);
        }
    }

    @GetMapping("/payment/discovery")
    @ResponseBody
    public Object discovery() {
        List<String> services = discoveryClient.getServices();
        for (String service : services) {
            log.info("******* service:" + service);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
        }
        return discoveryClient;
    }

    //测试手写轮询算法
    @GetMapping(value = "/payment/lb")
    @ResponseBody
    public String getPaymentLB()
    {
        return serverPort;
    }

    @GetMapping(value = "/payment/feign/timeout")
    @ResponseBody
    public String PaymentFeignTimeout()
    {
        try{
            TimeUnit.SECONDS.sleep(3);
        }  catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return serverPort;
    }

    @GetMapping("/payment/zipkin")
    @ResponseBody
    public String paymentZipkin()
    {
        return "hi,it`s paymentzipkin server fall back,welcome to CR553";
    }
}

服务注册与发现

Eureka

入门使用

POM依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!--引入自己定义的api通用包,可以使用payment支付entity-->
        <dependency>
            <groupId>CR553</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--boot web actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--一般通用配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml配置类

在这之前,为了本机地址不混淆,我配置了host文件

路径为 C:\Windows\System32\drivers\etc

######SpringCloud2020 ###############
127.0.0.1	eureka7001.com
127.0.0.1	eureka7002.com
127.0.0.1	eureka7003.com
127.0.0.1	config3344.com
server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com #eurake服务端的实例名称
  client:
    #false 表示不向注册中心注册自己
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
    #设置与eureka Server交互的地址查询服务和注册服务都需要依赖这个地方
      #defaultZone: http://eureka7002.com:7002/eureka/ #集群搭建在这里添加其他Eureka server
      defaultZone: http://eureka7001.com:7001/eureka/ #单机

主启动

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class,args);
    }
}

测试

  1. 先启动Eureka7001

  2. 再启动Payment8001

  3. 浏览器访问 http://eureka7001.com:7001/

  4. 测试结果如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4KerV2c-1596214459469)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595831106478.png)]

负载均衡

Ribbon

基于Netflix Ribbon实现的一套客户端 负载均衡的工具

主要功能是提供客户端的软件负载均衡算法和服务调用。ribbon客户端组件提供一系列万三的配置项如连接超时,重试等。就是在配置文件中列出Load Banlancer后面的所有机器,Ribbon会自动的帮你基于某种规则去连接这些机器.

一句话,ribbon是负载均衡+restTemplate调用

入门使用

新建Config类

@Configuration
public class ApplicationContextConfig {

    @Bean
  	@LoadBalanced  //这里表示开启了Ribbon负载均衡,默认为轮询
    public  RestTemplate  getRestTemplate()
    {
        return new RestTemplate();
    }
}

接着在controller类里面调用即可

@Resource
    private RestTemplate restTemplate;
    
 public static final String PAMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
 
	@GetMapping(value = "/consumer/payment/create")
    @ResponseBody
    public CommenResult<Payment> create(Payment payment)
    {
       return restTemplate.postForObject(PAMENT_URL+"/payment/create"
      									,payment,CommenResult.class);
    }

此时微服务端controller层

@RequestMapping(value = "/payment/create")
    @ResponseBody
    public CommenResult create(@RequestBody Payment payment) {
        int result = paymentService.create(payment);
        log.info("******插入结果" + result);
        if (result > 0) {
            return new CommenResult(200, "插入数据库成功,端口为:" + serverPort, result);
        } else {
            return new CommenResult(444, "插入数据库失败", null);
        }
    }

测试,启动eureka7001,payment8001,order80即可

替换轮询策略

  1. 新建package,包里新建MySelfRule规则类
@Configuration
public class MySelfRule {

    @Bean
    public IRule myRule()
    {
        return new RandomRule();//定义为随机
    }
}
  1. 主启动类添加@RibbonClient
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class,args);
    }

}

注意:

​ 这个自定义类不能放在@CommponentScan所扫描的当钱包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户的所共享,达不到特殊化定制的目的了.

手写轮询算法

改造步骤

1.去掉@LoadBalanced注解

2.LoadBalancer接口

public interface LoadBalancer {

ServiceInstance instances(List<ServiceInstance> serviceInstances);

}

3.新建MyLB类

@Component
public class MyLB implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement()
    {
        int current;
        int next;
        do{
            current=atomicInteger.get();
            next=current >= 2147483647? 0: current+1;
        }while(!this.atomicInteger.compareAndSet(current,next));
        System.out.println("********** next:"+next);
        return next;
    }


    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

4.修改OrderController

//测试手写轮询
    @Resource
    
    
    private LoadBalancer loadBalancer;
    
	@GetMapping(value = "/consumer/payment/lb")
    @ResponseBody
    public String getPaymentLB()
    {
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-			SERVICE");
        if(instances==null ||instances.size()==0)
        {
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();
       // log.info(uri.toString());
        return restTemplate.getForObject(uri.toString()+"/payment/lb",String.class);
    }  
    

5.依次打开eureka7001,payment8001,order80测试 http://localhost/consumer/payment/lb

服务调用

OpenFeign

入门使用

新建项目

POM依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

YML配置文件

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka

#设置feign客户端超时时间
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

logging:
  level:
    com.CR553.springcloud.service.PaymentService: debug

主启动

@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class,args);
    }
}

service接口

@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentService {
    @GetMapping(value = "/payment/get/{id}")
    @ResponseBody
    public CommenResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @GetMapping(value = "/payment/feign/timeout")
    @ResponseBody
    public String PaymentFeignTimeout();
}

config配置类

@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}

controller配置类

@Controller
public class OrderFeignController {

    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    @ResponseBody
    public CommenResult<Payment> getPaymentById(@PathVariable("id") Long id)
    {
        return paymentService.getPaymentById(id);
    }

    @GetMapping(value = "/consumer/payment/feign/timeout")
    @ResponseBody
    public String PaymentFeignTimeout()
    {
        return paymentService.PaymentFeignTimeout();
    }
}

主启动类

@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class,args);
    }
}

使用OpenFeign日志

NONE: 默认,不显示任何日志

BASIC:仅记录请求方法,URL,响应状态码及执行时间

HEADERS:除了BASIC中定义的信息之外,还有请求和响应头信息

FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据

@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}

服务降级

Hystrix

入门使用

服务端

新建工程cloud-provider-hystrix-payment8001

POM依赖

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

application.yml配置类

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true
    fetch-register: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

service

@Service
public class PaymentService {

    public String paymentInfo_OK(Integer id)
    {
        return "线程池:  "+Thread.currentThread().getName()+"     paymentInfo_OK,   id: 				"+id+"\t";
    }

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
           	 				    @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds"
                                     ,value = "5000")
    })
    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);

        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return "线程池:    "+Thread.currentThread().getName()+"    paymentInfo_TimeOut,  id: "+id+"\t"+"耗时(秒):"+timeNumber;
    }
    public String paymentInfo_TimeOutHandler(Integer id)
    {
        return "线程池:   "+Thread.currentThread().getName()+"   服务超时或者运行错误 ,  id: "+id+"\t";
    }

    //=========服务熔断
    @HystrixCommand(fallbackMethod ="paymentCircuitBreaker_fallback",commandProperties = {
            @HystrixProperty(name="circuitBreaker.enabled",value = "true"),
            @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
            @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000"),
            @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "60"),
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id)
    {
        if(id<0)
        {
            throw new RuntimeException("******id 不能为负数");
        }
        String serialNumber= IdUtil.simpleUUID();
        return Thread.currentThread().getName()+"\t"+"调用成功,流水号: "+serialNumber;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id)
    {
        return "id 不能为负数,请稍后再试   id: "+id;
    }

}

controller

@RestController
@Slf4j
public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    @ResponseBody
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result=paymentService.paymentInfo_OK(id);
        log.info("************result: "+result);
        return result;
    }

    @GetMapping("/payment/hystrix/timeOut/{id}")
    @ResponseBody
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
    {
        String result=paymentService.paymentInfo_TimeOut(id);
        log.info("************result: "+result);
        return result;
    }

    //======服务熔断
    @GetMapping("/payment/circuit/{id}")
    @ResponseBody
    public String paymentCircuitBreaker(@PathVariable("id") Integer id)
    {
        String result=paymentService.paymentCircuitBreaker(id);
        return result;
    }
}

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

    @Bean
    public ServletRegistrationBean getServlet()
    {
        HystrixMetricsStreamServlet streamServlet=new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new 		ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

测试 打开eureka7001,打开cloud-provider-hystrix-payment8001

在浏览器输入url

目前问题

  1. 每个业务方法对应一个兜底的方法,代码膨胀
  2. 统一和自定义的分开

解决方法,在客户端的controller类里面添加一下注释和方法

定义一个通用的服务兜底方法,其他特殊问题特殊处理

@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public String payment_Global_FallbackMethod()
{
    return "Global 异常处理信息,请稍后再试!";
}
客户端

新建cloud-consumer-feign-hystrix-order80

POM依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

application.yml配置文件

server:
  port: 80

eureka:
  client:
    register-with-eureka: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

feign:
  hystrix:
    enabled: true

service

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/ok/{id}")
    @ResponseBody
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeOut/{id}")
    @ResponseBody
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);

}
@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------PaymentFallbackService fall back-paymentInfo_OK!";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "------PaymentFallbackService fall back-paymentInfo_TimeOut!";
    }
}

controller

@Controller
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class PaymentHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    @ResponseBody
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeOut/{id}")
    @ResponseBody
    //当设置特定兜底方法的时候用该注解
    /* @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1500")
    })*/ 
    @HystrixCommand//当用全局兜底方法的时候使用该注解
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;

    }

    public String paymentInfo_TimeOutHandler(@PathVariable("id") Integer id)
    {
        return "我是消费者80,对方支付系统繁忙.请10秒钟后再试或者自己运行出错请检查自己!";
    }

    public String payment_Global_FallbackMethod()
    {
        return "Global 异常处理信息,请稍后再试!";
    }
}

主启动类

@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

测试

打开eureka7001,hystrix-payment8001,hystrix-order80

当客户端突然挂了,解决方法如下,新建一个PaymentHystrixService接口

@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------PaymentFallbackService fall back-paymentInfo_OK!";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "------PaymentFallbackService fall back-paymentInfo_TimeOut!";
    }
}

服务监控图形化

使用

新建cloud-consumer-hystrix-dashboard9001

POM依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml

server:
  port: 9001

主启动

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashBoardMain9001.class,args);
    }
}

测试

输入http://localhost:9001/hystrix

进入一下页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l1YnsdXl-1596214459472)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596012464385.png)]

多次点击http://localhost:8001/payment/hystrix/timeOut/11

查看页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5Qbzovi-1596214459473)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596012416947.png)]

服务网关

GetWay

入门使用

搭建cloud-gateway-gateway9527

POM依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.yml配置文件

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh  #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001 #匹配后提供服务的路由地址
          uri: lb://CLOUD-PAYMENT-SERVICE  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由


        - id: payment_routh2  #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001 #匹配后提供服务的路由地址
          uri: lb://CLOUD-PAYMENT-SERVICE  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**  # 断言,路径相匹配的进行路由
            - Cookie=username,CR553

eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

主启动类

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class,args);
    }
}

filter过滤类

@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("**********come in MyLogGateWayFilter:  "+new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if(uname == null)
        {
            log.info("**********用户名为null,非法用户");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

测试

启动eureka7001,启动payment8001和8002,再启动9527

在cmd里面

curl http://localhost:9527/payment/lb
curl http://localhost:9527/payment/lb --cookie "username=CR553"
curl http://localhost:9527/payment/lb?uname=aa 
curl http://localhost:9527/payment/lb?uname=aa --cookie "username=CR553"

注意日志文件

配置中心

Config

使用

POM依赖

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.50</version>
        </dependency>

application.yml配置类

server:
  port: 3344

spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
          uri: https://github.com/CR553/springcloud-config.git
          ####搜索目录
          search-path:
            - springcloud-config
      #### 读取分支
      label: master

#rabbit相关配置
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
#rabbitmq相关配置,暴露bus刷新配置的端点
management:
  endpoints:
    web:
      exposure:
        include: 'bus-refresh'

主启动

@EnableConfigServer
@SpringBootApplication
public class MainAppConfigCenter3344 {
    public static void main(String[] args) {
        SpringApplication.run(MainAppConfigCenter3344.class,args);
    }
}

测试

客户端

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.50</version>
</dependency>

bootstrap.yml

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    config:
      label: master
      name: config
      profile: prod
      uri: http://localhost:3344

rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

主启动

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class,args);
    }
}

controller

@Controller
@RefreshScope
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    @ResponseBody
    public String getConfigInfo()
    {
        return configInfo;
    }
}

测试

在github上创建好仓库和对应的文件

打开eureka7001,center3344,client3355,client3366

访问

http://localhost:3355/configInfo
http://localhost:3366/configInfo

修改文件,不要重启系统,手动cmd中:

curl -X POST  "http://localhost:3355/actuator/refresh"

再看变化

问题:这是直接对3355发送修改更新的,3366此时并未修改,这意味着我们得手动发送n个请求,不符合我们的预期

消息总线

什么是总线

在微服务架构中,通常回事会使用轻量级的消息代理来构建一个公用的消息主题,并让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以称他为消息总线.在总线上的各个实例都可以方便地传播一些需要让其他连接在该主题上的实例都知道的消息

基本原理

ConfigClient实例都监听MQ中同一个topic.当一个服务刷新数据的时候,他会把这个信息放入到topic中,这样其他监听同一个topic的服务就能得到通知,然后去更新自身的配置.

BUS

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统连接起来的框架,它融合了java的事件处理机制和消息中间件的功能.Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改,事件推送,也可以当作微服务间的通信通道

使用

准备工作

安装Erlang

安装RabbitMQ

进入RabbitMQ的sbin目录,输入

rabbitmq-plugins enable rabbitmq_management

访问地址查看是否安装成功

http://localhost:15672

输入账号密码并登录: guest guest

改造config-client

在pom文件加上起步依赖spring-cloud-starter-bus-amqp,完整的配置文件如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.50</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--引入自己定义的api通用包,可以使用payment支付entity-->
        <dependency>
            <groupId>CR553</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

在配置文件bootstrap.yml中加上RabbitMq的配置,包括RabbitMq的地址、端口,用户名、密码。并需要加上spring.cloud.bus的三个配置,具体如下:

server:
  port: 3366
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
spring:
  application:
    name: config-client
  cloud:
    config:
      label: master
      name: config
      profile: prod
      uri: http://localhost:3344
    bus:    #这里的配置没配也可以实现更新功能,具体作用以后再研究
      enabled: true
      trace:
        enabled: true

#开启消息总线时添加的
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

controller上面加上@RefreshScope注解

@Controller
@RefreshScope
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    @ResponseBody
    public String getConfigInfo()
    {
        return configInfo;
    }
}

依次启动eureka-server、confg-server,启动两个config-client,端口为:3355、3366。

访问http://localhost:3355/configInfo 或者http://localhost:3366/configInfo浏览器显示:

version=1(举例子)

这时候去github上面将version改为2,之后发送post请求(本次用的是cmd,因为咱的浏览器不支持post)

curl -X POST "http://localhost:3344/actuator/bus-refresh"

会发现config-client会重新读取配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPdgMit2-1596214459477)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596116101778.png)]

这时再访问http://localhost:3355/configInfo 或者http://localhost:3366/configInfo浏览器显示:

version =2

另外,/actuator/bus-refresh接口可以指定服务,即使用"destination"参数,比如 “/actuator/bus-refresh?destination=customers:**” 即刷新服务名为customers的所有服务。

消息驱动

Stream入门使用

服务端

新建cloud-stream-rabbitmq-provider8801模块

POM依赖,新增spring-cloud-starter-stream-rabbit

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--引入自己定义的api通用包,可以使用payment支付entity-->
    <dependency>
        <groupId>CR553</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

application.yml配置文件,配置Stream的binders,mq的环境等等

server:
  port: 8801
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: #在此处配置要绑定的rabbitmq的服务信息
        defaultRabbit: #表示定义的名称,用于binding集合
          type: rabbit #消息组件类型
          environment: #设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: #服务的整合处理
        output: #这个名字是一个通道的名称
           destination: studyExchange #表示要使用的Exchange名称定义
           content-type: application/json #设置消息类型
           binder: defaultRabbit   #设置要绑定的消息服务的具体设置

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔
    lease-expiration-duration-in-seconds: 5
    instance-id: send-8801.com #在信息列表时显示主机名称
    prefer-ip-address: true #访问的路径变为IP地址

service接口

public interface IMessageProvider {
    public String send();
}

service实现类,这里注入MessageChannel

@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {

    @Resource
    private MessageChannel output;

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("********** serial: "+serial);
        return null;
    }
}

controller

@Controller
public class SendMessageController {

    @Resource
    private IMessageProvider iMessageProvider;

    @GetMapping("/sendMessage")
    @ResponseBody
    public  String sendMessage()
    {
       return iMessageProvider.send();
    }
}

主启动类

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

测试,依次启动7001,rabbitMQ服务器,启动8801,访问http://localhost:8801/sendMessage 测试8801的send方法调用正常

进入RabbitMQ的sbin目录,输入
rabbitmq-plugins enable rabbitmq_management
访问地址查看是否安装成功
http://localhost:15672
输入账号密码并登录: 
guest guest
客户端

新建模块cloud-stream-rabbitmq-consumer8802/8803

POM依赖,和服务端一样增加spring-cloud-starter-stream-rabbit

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

8002的application.yml配置文件

server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: #在此处配置要绑定的rabbitmq的服务信息
        defaultRabbit: #表示定义的名称,用于binding集合
          type: rabbit #消息组件类型
          environment: #设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: #服务的整合处理
        input: #这个名字是一个通道的名称
           destination: studyExchange #表示要使用的Exchange名称定义
           content-type: application/json #设置消息类型
           binder: defaultRabbit   #设置要绑定的消息服务的具体设置
           group: CR553A

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔
    lease-expiration-duration-in-seconds: 5
    instance-id: receive-8802.com #在信息列表时显示主机名称
    prefer-ip-address: true #访问的路径变为IP地址

controller,@Component注解不要忘记,新增注解@EnableBinding(Sink.class), @StreamListener(Sink.INPUT)

@Component
@EnableBinding(Sink.class)
public class ReveiceMessageListenerController {

    @Value("${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)
    public void input(Message<String>message)
    {
        System.out.println("消费者1号,--->接收到的消息: "+message.getPayload()+"\t port:" +serverPort);
    }
}

测试,启动eureka7001,rabbitmq服务器,依次启动streamServer,streamclient,访问http://localhost:8801/sendMessage访问一次,8802打印内容,访问两次8803才会打印内容.这是因为我们给两个客户端分到了一个组里面,处于竞争关系,一次只有一个能获取信息.分组后还有一个功能,先关闭两个客户端8802,8803,更改8802的配置文件,注释掉

#group: CR553A

多次访问http://localhost:8801/sendMessage,先启动8802客户端,观察日志,没有打印,再启动8803客户端,观察日志,有信息出现.

链路追踪

Sleuth

入门使用

服务端

改造cloud-provider-payment8001

POM依赖,新增spring-cloud-starter-zipkin

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

application.yml配置文件,在spring下面配置zipkin的url,采样率

spring:
  application:
    name: cloud-payment-service
  zipkin:
    base-url: http://localhost:9411
    sleuth:
      sampler:
        #采样率介于0到1之间,1表示全部采集
        probability: 1

controller,常规添加方法

@GetMapping("/payment/zipkin")
@ResponseBody
public String paymentZipkin()
{
    return "hi,it`s paymentzipkin server fall back,welcome to CR553";
}
客户端

改造cloud-consumer-order80

POM,application同服务端,controller如下

//+++++++++++zipkin + sleuth
@GetMapping("/consumer/payment/zipkin")
@ResponseBody
public String paymentZipkin()
{
   return restTemplate.getForObject("http://localhost:8001/payment/zipkin/",String.class);
}
测试

先启动zipkin,安装目录cmd直接java -jar 执行即可,依次启动eureka7001,payment8001,order80,访问localhost:9411可以看到下面页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrbjuFcZ-1596214459479)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596169170726.png)]

SpringCloud Alibaba

Nacos

下载安装

Nacos下载

Nacos依赖于Java环境,所以必须安装Java环境。然后从官网下载Nacos的解压包,安装稳定版的,下载地址:https://github.com/alibaba/nacos/releases

本次案例下载的版本为0.09 ,下载完成后,解压,在解压后的文件的/bin目录下,windows系统点击startup.cmd就可以启动nacos。linux或mac执行以下命令启动nacos。

sh startup.sh -m standalone

启动时会在控制台,打印相关的日志。nacos的启动端口为8848,在启动时要保证端口不被占用。

启动成功,在浏览器上访问:http://localhost:8848/nacos,会跳转到登陆界面,默认的登陆用户名为nacos,密码也为nacos。

入门使用

服务端

新建cloudalibaba-provider-payment9001

POM依赖,添加spring-cloud-starter-alibaba-nacos-discovery

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

application.yml配置,配置nacos地址

server:
  port: 9001

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置nacaos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'

controller

@RestController
public class PaymentController {

    @Value("${server.port}")
    private String serverport;

    @GetMapping("/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id)
    {
        return "nacos registry,serverPort: "+serverport+"\t id: "+id;
    }
}

主启动

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9001.class,args);
    }
}
客户端

新建cloudalibaba-consumer-nacos-order83

POM依赖,同上

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

application.yml,配置nacos-user-service

server:
  port: 83
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  application:
    name: nacos-order-consumer

#消费者将要去访问的微服务名称
service-url:
  nacos-user-service: http://nacos-payment-provider

主启动

@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain83.class,args);
    }
}

controller

@RestController
public class OrderNacosController {

    @Resource
    private RestTemplate restTemplate;

    @Value("${service-url.nacos-user-service}")
    private String serverURL;

    @GetMapping(value = "/consumer/payment/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Long id)
    {
        return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
    }
}

测试,启动nacos服务器,payment9001,再启动order83.访问http://localhost:83/consumer/payment/nacos/11

返回 nacos registry,serverPort: 9001 id: 11

持久端配置

新建cloud-alibaba-config-nacos-clent3377

POM依赖,新增spring-cloud-starter-alibaba-nacos-config

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

application.yml配置

spring:
  profiles:
    active: dev #表示开发环境
    #active: test #表示测试环境
    #active: info #表示测试环境

bootstrap.yml配置

server:
  port: 3377

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yaml #指定yaml格式的配置
        group: DEV_GROUP
        namespace: 6608bb77-fa9b-42e3-8bac-e7b9063622f5

# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml

启动类

@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClienMain3377 {
    public static void main(String[] args) {
        SpringApplication.run(NacosConfigClienMain3377.class,args);
    }
}

controller,注意@RefreshScope

@RestController
@RefreshScope
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo()
    {
        return configInfo;
    }
}

nacos 配置文件的起名规范

# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml

测试

启动 nacos server启动pament9001,order83,config3377,输入http://localhost:3377/config/info将配置文件中info的version更改,再次访问http://localhost:3377/config/info查看结果改变了.

nacos config的分类配置

问题

实际开发中,通常一个系统会准备dev开发环境,test测试环境,prod生产环境,如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境,测试环境,预发环境,正式环境,那么怎么对这些为服务配置进行管理呢?

默认情况

Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT

Nacos默认的命名空间是public,Namespace主要用来实现隔离,比方说三个环境,开发,测试,生产.

Group默认是DEFAULT_GROUP,GROUP可以把不同的微服务分到同一个分组里面去

Service就是微服务;一个Service可以包含多个Cluster(集群),Cluster是对指定微服务的一个虚拟划分,比方说为了容灾,将service微服务分别部署在了两个机房.

Sentinel

入门使用

新建

POM依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

application.yml配置文件配置sentinel

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未被占用的端口
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

management:
  endpoints:
    web:
      exposure:
        including: '*'

controller层FlowLimitController,一般方法,主要测试正常返回,有延时返回,有报错返回,这里只用testA和testB就行

@RestController
public class FlowLimitController {

    @GetMapping("/testA")
    @ResponseBody
    public String testA()
    {
      /*  try{
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }*/
        return "_____testA";
    }

    @GetMapping("/testB")
    @ResponseBody
    public String testB()
    {
        System.out.println("testBBBBBBBBBBBBBBBBBB");
        return "________testB";
    }

    @GetMapping("/testD")
    @ResponseBody
    public String testD()
    {
       /* try{
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }*/
        int a=10/0;
        return "_____testD";
    }

    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotkey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2)
    {
        return "***************testHotKey";
    }
    public String deal_testHotkey(String p1, String p2, BlockException exception)
    {
        return "***************兜底";
    }
    
}

RateLimitController

@RestController
public class RateLimitController {

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource",blockHandler = "handleException")
    public CommenResult byResource()
    {
        return new CommenResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
    }
    public CommenResult handleException(BlockException exception)
    {
        return new CommenResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
    }

    @GetMapping("/rateLimit/byUrl")
    @SentinelResource(value = "byUrl")
    public CommenResult byUrl()
    {
        return new CommenResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));
    }

    @GetMapping("/rateLimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
     blockHandlerClass = CustomerBlockHandler.class,
     blockHandler = "handlerException2")
    public CommenResult customerBlockHandler()
    {
        return new CommenResult(200,"按客户自定义",new Payment(2020L,"serial003"));
    }

}

主启动

@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class,args);
    }
}

测试,启动nacos8848,启动sentinel8080,启动微服务8401.进入localhost:8080,登录,用户名和密码为sentinel

多次访问
localhost:8401/testA
localhost:8401/testB

刷新:8080,就可以查看到调用记录

流控规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMaceVoY-1596214459481)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595393355616.png)]

  • 资源名:唯一名称,默认请求路径

  • 针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认default

  • 阈值类型/单机阈值:

    • QPS(每秒钟的请求数量): 当调用该api的QPS达到阈值的时候进行限流

    • 线程数:当调用该api的线程数达到阈值的时候,进行限流

  • 流控模式:

    • 直接:api达到限流条件时,直接限流

    • 关联:当关联的资源达到阈值时,就限流自己

    • 链路:只记录指定链路上的流量,

  • 流控效果:

    • 快速失败:直接失败,抛异常
    • warm up:根据codeFactor的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
    • 排队等待:匀速排队,让请求匀速的速度通过,阈值类型必须设置为QPS否则无效

流控模式

直接,QPS偏向外部,而线程数偏向内部

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eb9VfbUi-1596214459482)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595393806705.png)]

当访问次数超过阈值立刻进行流量控制,问题,如何自定义fallback兜底?

关联,当关联的资源达到阈值时,就限流自己,当与A关联的资源B达到阈值后,就限流A自己.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfrDbe5G-1596214459483)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406033587.png)]

流控效果

直接,预热

Warm Up方式,即预热冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮.通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热时间,避免冷系统被压垮.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-khUKIszg-1596214459484)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406622542.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVyjxgHG-1596214459485)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406756922.png)]

排队等待

  • 匀速排队让请求以匀速的速度通过,阈值类型必须设置成QPS,否则无效.

  • 设置含义:/testA每秒钟1次请求,超过的话就排队等待,等待的超时时间为20000毫秒

匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以匀速的速度通过,对应的是漏桶算法.这种方式主要用于处理间隔性突发的流量,例如消息队列,想想一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ml7gSq8q-1596214459486)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595407176015.png)]

降级规则

  • RT(平均响应时间,秒级)

平均响应时间超出阈值且在时间窗口内通过的请求>=5,两个条件同时满足后触发降级

窗口期过后关闭断路器,RT最大4900

  • 异常比例(秒级)

QPS>=5且异常比例超过阈值时,触发降级;时间窗口结束后,关闭降级

  • 异常数(分钟级)

  • 异常数超过阈值,厨房降级,时间窗口结束后,关闭降级

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或者异常比例升高),对这个资源的调用进行限制会让请求快速失败,避免影响到其它的资源而导致级联错误.当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都会自动熔断,Sentinel的断路器是没有半开状态的

降级策略实战

RT

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VimC93D7-1596214459487)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595408261991.png)]

@GetMapping("/testD")
    @ResponseBody
    public String testD()
    {
        try{
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }
        return "_____testD";
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apN8W1K8-1596214459488)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595408285468.png)]

效果: 压测时手动访问访问不了,停止压测后立马恢复

异常比例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Xla2GXG-1596214459490)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595409197147.png)]

@GetMapping("/testD")
@ResponseBody
public String testD()
{
   /* try{
        TimeUnit.MILLISECONDS.sleep(1000);
    }
    catch (InterruptedException e){
        e.printStackTrace();
    }*/
    int a=10/0;
    return "_____testD";
}

效果,单独访问一次,必然调用一次报错一次;开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了,断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务降级了.

异常数

当资源近1分钟的异常数且超过阈值之后会进行熔断,注意由于统计时间窗口是分钟级别的,若timewindow小于60s,则结束熔断状态后仍可能在进入熔断状态.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Q868SAm-1596214459491)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595409508065.png)]

热点key限流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOMiOGxR-1596214459492)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595427508698.png)]

@GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotkey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2)
    {
        return "***************testHotKey";
    }
    public String deal_testHotkey(String p1, String p2, BlockException exception)
    {
        return "***************兜底";
    }
访问
http://localhost:8401/testHotKey?p2=2
http://localhost:8401/testHotKey?p1=2&&p2=4
http://localhost:8401/testHotKey?p1=2

参数例外项

设置例外的限流目标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vytd6Lq-1596214459493)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595428208510.png)]

@SentinelResource处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理.而RuntimeException .int age=10/0,这个是java运行时报出的运行时异常RunTimeException.

总结@SentinelResource主管配置出错,运行出错走异常

系统规则

各项参数配置说明

系统保护规则是从应用级别的入口流量进行控制,从单台机器的load,cpu使用率,平均RT,入口QPS,和并发线程数等几个维度监控应用指标,让系统尽可能泡在最大吞吐量的同时保证系统整体的稳定性.

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效,入口流量指的是进入应用的流量.

系统规则支持一下的模式:

  • Load自适应(仅对Linux/Unix-like机器生效):系统的load1作为启发指标,进行自适应系统保护.当系统load超过设定的启发值,且系统当前的并发线程数超过估值的系统容量时才会触发系统保护(BBR阶段).系统容量由系统的maxQPS*minRt估算得出,设定参考值一般是cpu cores * 2.5.

  • Cpu usage:当系统cpu使用率超过阈值即触发系统保护(0-1),比较灵敏.

  • 平均RT: 当单台机器上所有入口流量的平均RT达到阈值即触发系统保护.

  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护

  • 入口QPS:当单台机器所有入口流量QPS达到阈值即触发系统保护

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJ4W7Sd7-1596214459494)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595470103412.png)]

若超过阈值则所有微服务失效

@SentinelResource

按资源名称,URL地址依次测试出现的问题:

  1. 系统默认的,没有体现自己的业务要求
  2. 依照现有条件,自定义处理方法和业务代码糅合在一块,不直观
  3. 每个业务方法都添加一个兜底,代码膨胀加剧
  4. 全局统一的处理方法没有体现

客户自定义限流处理逻辑

创建CustomerBlockHandler类用于自定义限流处理逻辑,再定义限流处理类

@GetMapping("/rateLimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
     blockHandlerClass = CustomerBlockHandler.class,
     blockHandler = "handlerException2")
    public CommenResult customerBlockHandler()
    {
        return new CommenResult(200,"按客户自定义",new Payment(2020L,"serial003"));
    }
public class CustomerBlockHandler {
    public static CommenResult handlerException(BlockException exception)
    {
        return new CommenResult(4444,"按客户自定义,global handlerException-----1");
    }
    public static CommenResult handlerException2(BlockException exception)
    {
        return new CommenResult(4444,"按客户自定义,global handlerException-----2");
    }
}

Seata

Seata下载与安装

下载地址:

https://github.com/seata/seata/releases
  1. 修改conf目录下的file.conf配置文件,修改的主要内容为自定义事务组名称+事务日志存储模式为db+数据库连接信息

  2. 数据库建库seata

  3. 在seata库里建表

  4. 测试,先启动Nacos端口8848,再启动seata-server

service {
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "fsp_tx_group"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}
## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC"
    user = "root"
    password = "root"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }

建库SQL

-- the table to store GlobalSession data
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
  `xid` VARCHAR(128)  NOT NULL,
  `transaction_id` BIGINT,
  `status` TINYINT NOT NULL,
  `application_id` VARCHAR(32),
  `transaction_service_group` VARCHAR(32),
  `transaction_name` VARCHAR(128),
  `timeout` INT,
  `begin_time` BIGINT,
  `application_data` VARCHAR(2000),
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
  KEY `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
  `branch_id` BIGINT NOT NULL,
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT ,
  `resource_group_id` VARCHAR(32),
  `resource_id` VARCHAR(256) ,
  `lock_key` VARCHAR(128) ,
  `branch_type` VARCHAR(8) ,
  `status` TINYINT,
  `client_id` VARCHAR(64),
  `application_data` VARCHAR(2000),
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
);

-- the table to store lock data
DROP TABLE IF EXISTS `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

入门使用

Seata事务实例–模拟网上支付

业务逻辑: 当用户下单时,会在订单服务汇总创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过

新建

数据库搭建,建表语句

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
###
CREATE TABLE t_order(
	`id` BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`user_id` BIGINT(11) DEFAULT NULL COMMENT'用户id',
	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
	 `count`INT(11) DEFAULT NULL COMMENT'数量',
	 `money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额',
	 `status` INT(1) DEFAULT NULL COMMENT'订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
###
CREATE TABLE t_storage(
`id` BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT'产品id',
`total`INT(11) DEFAULT NULL COMMENT'总库存',
`used`INT(11) DEFAULT NULL COMMENT'已用库存',
`residue` INT(11) DEFAULT NULL COMMENT'剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
###
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT'用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT'总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT'已用余额',
`residue` DECIMAL(10,0) DEFAULT'0' COMMENT'剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

回滚日志表

新建项目

seata-order-service2001
seata-storage-service2002
seata-account-service2003

POM依赖

<dependencies>
<!--       seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
<!--        nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--引入自己定义的api通用包,可以使用payment支付entity-->
        <dependency>
            <groupId>CR553</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

YML

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3366/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
    username: root
    password: root


feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapper-locations: classpath:mapper/*.xml

domain

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code,String message)
    {
        this(code,message,null);
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal moeny;
    private Integer status; //订单状态
}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long userId;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;//总库存
    private Integer used; //已用库存
    private Integer residue;//剩余库存
}

config

@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource()
    {
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource)
    {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception
    {
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}
@Configuration
@MapperScan({"com.CR553.springcloud.dao"})
public class MyBatisConfig {
}

controller

@RestController
public class StorageController {

    @Resource
    private StorageService storageService;

    @RequestMapping("/storage/decrease")
    @ResponseBody
    public CommenResult decrease(Long productId,Integer count)
    {
        storageService.decrease(productId,count);
        return new CommenResult(200,"扣减库存成功");
    }
}
@RestController
public class AccountController {

    @Resource
    private AccountService accountService;

    @RequestMapping("/account/decrease")
    @ResponseBody
    public CommenResult decrease(@RequestParam("userId") Long userId,
                                 @RequestParam("money") BigDecimal money)
    {
        accountService.decrease(userId,money);
        return new CommenResult(200,"扣减账户余额成功!");
    }
}
@RestController
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    @ResponseBody
    public CommenResult create(Order order)
    {
        orderService.create(order);
        return new CommenResult(200,"订单创建成功");
    }
}

测试

先设置数据库中库存和账户的值

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

账户支付延时与否为变量

esult(Integer code,String message)
{
this(code,message,null);
}
}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal moeny;
private Integer status; //订单状态
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
private Long productId;
private Integer total;//总库存
private Integer used; //已用库存
private Integer residue;//剩余库存
}


config

@Configuration
public class DataSourceProxyConfig {

@Value("${mybatis.mapperLocations}")
private String mapperLocations;

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource()
{
    return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource)
{
    return new DataSourceProxy(dataSource);
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception
{
    SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSourceProxy);
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
    sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
    return sqlSessionFactoryBean.getObject();
}

}


@Configuration
@MapperScan({“com.CR553.springcloud.dao”})
public class MyBatisConfig {
}


controller

@RestController
public class StorageController {

@Resource
private StorageService storageService;

@RequestMapping("/storage/decrease")
@ResponseBody
public CommenResult decrease(Long productId,Integer count)
{
    storageService.decrease(productId,count);
    return new CommenResult(200,"扣减库存成功");
}

}


@RestController
public class AccountController {

@Resource
private AccountService accountService;

@RequestMapping("/account/decrease")
@ResponseBody
public CommenResult decrease(@RequestParam("userId") Long userId,
                             @RequestParam("money") BigDecimal money)
{
    accountService.decrease(userId,money);
    return new CommenResult(200,"扣减账户余额成功!");
}

}


@RestController
public class OrderController {
@Resource
private OrderService orderService;

@GetMapping("/order/create")
@ResponseBody
public CommenResult create(Order order)
{
    orderService.create(order);
    return new CommenResult(200,"订单创建成功");
}

}




测试

先设置数据库中库存和账户的值

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100


账户支付延时与否为变量

全局事务添加
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值