springcloud alibaba 4(短信服务、Nacos Config 、Nacos常见概念、 分布式事务、Seata、Dubbo)

写在前面

springcloud alibaba 1:https://blog.csdn.net/a__int__/article/details/109438417
springcloud alibaba 2:https://blog.csdn.net/a__int__/article/details/109537327
springcloud alibaba 3:https://blog.csdn.net/a__int__/article/details/109848085

1、短信服务

这里我们使用阿里云的短信服务为例

阿里云短信API接入文档:

https://help.aliyun.com/document_detail/59210.html?spm=a2c4g.11174283.4.1.1d942c42kiIUj7

阿里云短信服务控制台:

https://dysms.console.aliyun.com/dysms.htm?spm=a2c4g.11186623.2.26.73a1463aFWvbf5#/overview

第一步:创建密钥

在阿里云短信服务控制台右击头像-AccessKey管理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
密钥创建成功
在这里插入图片描述
第二步:创建签名
回到控制台,创建签名
在这里插入图片描述
在这里插入图片描述
验证码类型的签名,一个用户只能申请一个

第三步:添加模板
在这里插入图片描述

在这里插入图片描述

1.1、API介绍

阿里云短信服务文档使用指引:

https://help.aliyun.com/document_detail/55284.html?spm=a2c4g.11186623.6.671.770542ecaTWwl5

在这里插入图片描述
短信发送
请求参数:
在这里插入图片描述
返回数据:
在这里插入图片描述
短信查询
请求参数:
在这里插入图片描述
返回参数:
在这里插入图片描述

1.2、上手测试

为 shop-user 加入依赖

        <!--短信发送-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alicloud-sms</artifactId>
        </dependency>

在shop-user中新建SmsDemo.java

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.dysmsapi.transform.v20170525.SendSmsResponseUnmarshaller;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;

import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

/**
 * Created on 17/6/7.
 * 短信API产品的DEMO程序,工程中包含了一个SmsDemo类,直接通过
 * 执行main函数即可体验短信产品API功能(只需要将AK替换成开通了云通信-短信产品功能的AK即可)
 * 工程依赖了2个jar包(存放在工程的libs目录下)
 * 1:aliyun-java-sdk-core.jar
 * 2:aliyun-java-sdk-dysmsapi.jar
 *
 * 备注:Demo工程编码采用UTF-8
 * 国际短信发送请勿参照此DEMO
 */
public class SmsDemo {

    //产品名称:云通信短信API产品,开发者无需替换
    static final String product = "Dysmsapi";
    //产品域名,开发者无需替换
    static final String domain = "dysmsapi.aliyuncs.com";

    // TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
    static final String accessKeyId = "yourAccessKeyId";
    static final String accessKeySecret = "yourAccessKeySecret";

    public static SendSmsResponse sendSms() throws ClientException {

        //可自助调整超时时间
        System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
        System.setProperty("sun.net.client.defaultReadTimeout", "10000");

        //初始化acsClient,暂不支持region化
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
        DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
        IAcsClient acsClient = new DefaultAcsClient(profile);

        //组装请求对象-具体描述见控制台-文档部分内容
        SendSmsRequest request = new SendSmsRequest();
        //必填:待发送手机号
        request.setPhoneNumbers("15000000000");
        //必填:短信签名-可在短信控制台中找到
        request.setSignName("云通信");
        //必填:短信模板-可在短信控制台中找到
        request.setTemplateCode("SMS_1000000");
        //可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
        request.setTemplateParam("{\"name\":\"Tom\", \"code\":\"123\"}");

        //选填-上行短信扩展码(无特殊需求用户请忽略此字段)
        //request.setSmsUpExtendCode("90997");

        //可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
        request.setOutId("yourOutId");

        //hint 此处可能会抛出异常,注意catch
        SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);

        return sendSmsResponse;
    }


    public static QuerySendDetailsResponse querySendDetails(String bizId) throws ClientException {

        //可自助调整超时时间
        System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
        System.setProperty("sun.net.client.defaultReadTimeout", "10000");

        //初始化acsClient,暂不支持region化
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
        DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
        IAcsClient acsClient = new DefaultAcsClient(profile);

        //组装请求对象
        QuerySendDetailsRequest request = new QuerySendDetailsRequest();
        //必填-号码
        request.setPhoneNumber("15000000000");
        //可选-流水号
        request.setBizId(bizId);
        //必填-发送日期 支持30天内记录查询,格式yyyyMMdd
        SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd");
        request.setSendDate(ft.format(new Date()));
        //必填-页大小
        request.setPageSize(10L);
        //必填-当前页码从1开始计数
        request.setCurrentPage(1L);

        //hint 此处可能会抛出异常,注意catch
        QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request);

        return querySendDetailsResponse;
    }

    public static void main(String[] args) throws ClientException, InterruptedException {

        //发短信
        SendSmsResponse response = sendSms();
        System.out.println("短信接口返回的数据----------------");
        System.out.println("Code=" + response.getCode());
        System.out.println("Message=" + response.getMessage());
        System.out.println("RequestId=" + response.getRequestId());
        System.out.println("BizId=" + response.getBizId());

        Thread.sleep(3000L);

        //查明细
        if(response.getCode() != null && response.getCode().equals("OK")) {
            QuerySendDetailsResponse querySendDetailsResponse = querySendDetails(response.getBizId());
            System.out.println("短信明细查询接口返回数据----------------");
            System.out.println("Code=" + querySendDetailsResponse.getCode());
            System.out.println("Message=" + querySendDetailsResponse.getMessage());
            int i = 0;
            for(QuerySendDetailsResponse.SmsSendDetailDTO smsSendDetailDTO : querySendDetailsResponse.getSmsSendDetailDTOs())
            {
                System.out.println("SmsSendDetailDTO["+i+"]:");
                System.out.println("Content=" + smsSendDetailDTO.getContent());
                System.out.println("ErrCode=" + smsSendDetailDTO.getErrCode());
                System.out.println("OutId=" + smsSendDetailDTO.getOutId());
                System.out.println("PhoneNum=" + smsSendDetailDTO.getPhoneNum());
                System.out.println("ReceiveDate=" + smsSendDetailDTO.getReceiveDate());
                System.out.println("SendDate=" + smsSendDetailDTO.getSendDate());
                System.out.println("SendStatus=" + smsSendDetailDTO.getSendStatus());
                System.out.println("Template=" + smsSendDetailDTO.getTemplateCode());
            }
            System.out.println("TotalCount=" + querySendDetailsResponse.getTotalCount());
            System.out.println("RequestId=" + querySendDetailsResponse.getRequestId());
        }

    }
}

如上代码中的accessKeyId 、accessKeySecret等改成自己的
可以把如上代码封装为一个工具类
下面是把发送代码部分封装为工具类SmsUtil.java

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SmsUtil {
    //替换成自己申请的accessKeyId
    private static String accessKeyId = "LTAIMLlf8NKYXn1M";
    //替换成自己申请的accessKeySecret
    private static String accessKeySecret = "hqyW0zTNzeSIFnZhMEkOaZXVVcr3Gj";

    static final String product = "Dysmsapi";
    static final String domain = "dysmsapi.aliyuncs.com";

    /**
     * 发送短信
     *
     * @param phoneNumbers 要发送短信到哪个手机号
     * @param signName     短信签名[必须使用前面申请的]
     * @param templateCode 短信短信模板ID[必须使用前面申请的]
     * @param param        模板中${code}位置传递的内容
     */
    public static void sendSms(String phoneNumbers, String signName, String templateCode, String param) {
        try {
            System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
            System.setProperty("sun.net.client.defaultReadTimeout", "10000");

            //初始化acsClient,暂不支持region化
            IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
            DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
            IAcsClient acsClient = new DefaultAcsClient(profile);

            SendSmsRequest request = new SendSmsRequest();
            request.setPhoneNumbers(phoneNumbers);
            request.setSignName(signName);
            request.setTemplateCode(templateCode);
            request.setTemplateParam(param);
            request.setOutId("yourOutId");
            SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
            if (!"OK".equals(sendSmsResponse.getCode())) {
                log.info("发送短信失败,{}", sendSmsResponse);
                throw new RuntimeException(sendSmsResponse.getMessage());
            }
        } catch (Exception e) {
            log.info("发送短信失败,{}", e);
            throw new RuntimeException("发送短信失败");
        }
    }
}

然后写一个服务类SmsService.java

import com.alibaba.fastjson.JSON;
import com.itheima.dao.UserDao;
import com.itheima.domain.Order;
import com.itheima.domain.User;
import com.itheima.utils.SmsUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;

@Slf4j
@Service("shopSmsService")
//consumerGroup-消费者组名  topic-要消费的主题
@RocketMQMessageListener(
        consumerGroup = "shop-user", //消费者组名
        topic = "order-topic",//消费主题
        consumeMode = ConsumeMode.CONCURRENTLY,//消费模式,指定是否顺序消费 CONCURRENTLY(同步,默认) ORDERLY(顺序)
        messageModel = MessageModel.CLUSTERING//消息模式 BROADCASTING(广播)  CLUSTERING(集群,默认)
)
public class SmsService implements RocketMQListener<Order> {

    @Autowired
    private UserDao userDao;

    //消费逻辑
    @Override
    public void onMessage(Order message) {
        log.info("接收到了一个订单信息{},接下来就可以发送短信通知了", message);

        //根据uid 获取手机号
        User user = userDao.findById(message.getUid()).get();

        //生成验证码 1-9 6
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            builder.append(new Random().nextInt(9) + 1);
        }
        String smsCode = builder.toString();

        Param param = new Param(smsCode);

        try {
            //发送短信 {"code":"123456"}
            SmsUtil.sendSms(user.getTelephone(), "生鲜商城", "SMS_170836451", JSON.toJSONString(param));
            log.info("短信发送成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class Param {
        private String code;
    }
}

2、Nacos Config 服务配置

先来看看当前的的微服务存在的问题

  • 第一点:配置文件分散,不好统一管理
  • 第二点:配置文件没法区分生产环境、测试环境等多环境。
  • 第三点:配置文件无法实时更新

如上问题,我们使用配置中心类解决。

常见的配置中心有:

  • Disconf:百度开源的配置管理中心,同样具备配置的管理能力,不过目前已经不维护了。

  • Spring Cloud Config:Spring Cloud体系,没有界面、不能实时生效,需重启或刷新。

  • Apollo:携程开源的配置管理中心,具备规范的权限、流程治理等特性。

  • Nacos:阿里开源的配置中心,也可以做DNS和RPC的服务发现。

2.1、Nacos Config 开始使用

使用Nacos作为配置中心,将nacos当作服务端,将各个微服务看成客户端,微服务的配置文件统一放nacos上,然后从nacos拉取。

第一步:启动微服务(之前启动过)

  • nocos-server下载后解压,进入bin目录,服务端启动指令startup.cmd -m standalone
  • 后台访问地址:http://127.0.0.1:8848/nacos/index.html(账号密码都是nacos)

第二步:在shop-product微服务中加入依赖
在这里插入图片描述
在这里插入图片描述

第三步:在nacos config中配置

注意: 不能使用原来的application.yaml作为配置文件,而是新建一个bootstrap.yaml作为配置文件

在这里插入图片描述
在application.yaml同目录下,新建bootstrap.yaml
在这里插入图片描述
进入nacos的管理界面http://127.0.0.1:8848/nacos/index.html
在这里插入图片描述
接下来将application.yaml的内容复制到nacos中,然后把application.yaml中的内容注释掉
在这里插入图片描述
点击发布就能看到这项配置
在这里插入图片描述
配完成重启shop-product,访问试试
在这里插入图片描述

2.2、Nacos Config 配置动态更新

首先需要的nacos的配置中添加 一个config.appName,然后点击发布
在这里插入图片描述
在shop-product中新建NacosConfigController.java (用于动态加载nacos中的配置文件的)
在这里插入图片描述
然后重启shop-product,访问localhost:8081/test-config1
在这里插入图片描述
然后把config.appName的值修改成product1
在这里插入图片描述
然后再访问,发现已经修改了,可以动态配置了
在这里插入图片描述
接下来我们把config.appName用注入的方式返回
在这里插入图片描述
在这里插入图片描述
修改nacos,我们发现这样是不能实现动态刷新的,要实现动态其实还需加一个注解@RefreshScope
在这里插入图片描述
利用如上的方式我们可以配置多环境
在这里插入图片描述

到目前NacosConfigController.java完整内容

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope//动态刷新的注解
public class NacosConfigController {

    @Autowired
    private ConfigurableApplicationContext applicationContext;

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

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


    @RequestMapping("/test-config1")
    public String testConfig1() {
        return applicationContext.getEnvironment().getProperty("config.appName");
    }


    @RequestMapping("/test-config2")
    public String testConfig2() {
        return appName;
    }

    @RequestMapping("/test-config3")
    public String testConfig3() {
        return env;
    }
}

2.3、Nacos Config 配置共享

同一个微服务的不同环境之间共享配置

第一步,我们在nacos新增一个yaml配置文件service-product.yaml(将service-product-dev.yaml拷贝过来)
在这里插入图片描述
将config.appName去掉
再到service-product-dev.yaml清空,改成如下
在这里插入图片描述
之后再新建一个service-product-test.yaml
在这里插入图片描述
到目前我们就有三个配置了
在这里插入图片描述
返回微服务中看看bootstrap.yaml
在这里插入图片描述
目前使用的dev
重启shop-product试一下
在这里插入图片描述
不同微服务中共享配置

第一步,先在nacos中定义一个Data ID为all-service.yaml的配置
在这里插入图片描述
第二步,在nacos中修改service-product.yaml,内容如下
在这里插入图片描述
第三步,修改bootstrap.yaml,如下
在这里插入图片描述
重启微服务就发现已经生效了

2.4、Nacos常见概念

  • 命名空间:用于不同环境的配置隔离,一般一个命名环境划分到一个命名空间
  • 配置分组:配置分组用于将不同的服务归类到同一分组,一般一个项目的配置分到一个组
  • 配置集:一个配置文件就是一个配置集,一般一个微服务就是一个配置集

在这里插入图片描述

新建命名空间
在这里插入图片描述
微服务切换命名空间
在这里插入图片描述

3、分布式事务

  • 本地事务:数据库事务机制,具有四大特性:原子性、一致性、隔离性、持久性
  • 分布式事务:保证分布式系统上不同节点的一致性。

分布式事务的场景:

  • 单体系统访问多个数据库
    -
  • 多个微服务访问同一个数据库
    在这里插入图片描述
  • 多个微服务访问多个数据库
    在这里插入图片描述

3.1、分布式事务解决方案

3.1.1、第一种:全局事务

基于DTP模型实现,它规定要实现分布式事务需要三种角色:

  • AP:application,应用系统
  • TM:事务管理器
  • RM:资源管理器
    在这里插入图片描述

整个思路分成两个阶段:

  • 阶段一:表决阶段,所有参与者将本地事务执行预提交,并将能否成功的信息反馈发给协调者。
  • 阶段二:执行阶段,协调者根据所有参与者的反馈,通知所有参与者步调一致的执行提交或回滚。

在这里插入图片描述
在这里插入图片描述

3.1.2、第二种:可靠消息服务

RocketMQ就是一种可靠消息服务
可靠消息服务:通过消息中间件保证上下游应用数据操作的一致性。
在这里插入图片描述
第一步:消息由系统A投递到中间件
1、在系统A处理任务前,首先向消息中间件发送一条消息
2、消息中间件收到后将该消息持久化,但并不投递,持久化成功后,向A回复一个确认应答
3、系统A收到确认应答后,则开始处理任务A
4、任务A处理完成后,向消息中间件发送Commit或者Rollback请求,该请求发送完成后,对系统A而言,该事务的处理过程就结束了。
5、如果消息中间件收到Commit,则向B系统投递消息;如果收到Rollback,则直接丢弃消息。但如果消息中间件收不到Commit和Rollback指令,那么就要依靠“超时询问机制”。

第二步:消息由中间件投递到系统B

  • 如果消息中间件收到确认应答后便认为该事务处理完毕
  • 如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂的问题。
3.1.3、第三种:最大努力通知

最大努力通知也称定期校对,其实是可靠消息服务解决方案的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,进一步保证消息会被下一级系统消费
在这里插入图片描述
由于耦合较高,没有一个成型的方案解决,所以该方案在业界用的少。
在这里插入图片描述
在这里插入图片描述

3.1.4、第四种:TCC事务

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、Seata

Seata是阿里巴巴开源的分布式事务解决方案
在这里插入图片描述
Seata的组成:
TC:事务协调器,管理全局的分支事务状态,用于全局性事务的提交和回滚。
TM:事务管理器,用于开启全局、提交或回滚全局事务。
RM:资源管理器,用于分支事务上,向TC注册分支事务,上报分支事务状态,介接收TC的命令来提交或回滚分支事务。

在这里插入图片描述
Seata的执行流程
在这里插入图片描述
Seata实现2pc与传统2pc的差别
在这里插入图片描述

4.1、Seata实现分布式事务控制

4.1.1、下单模拟

案例:下订单、扣库存的过程
在这里插入图片描述
第一步:找到shop-order微服务,新建OrderController5.java

在这里插入图片描述
内容如下:

import com.alibaba.fastjson.JSON;
import com.itheima.domain.Order;
import com.itheima.domain.Product;
import com.itheima.service.OrderService;
import com.itheima.service.ProductService;
import com.itheima.service.impl.OrderServiceImpl5;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class OrderController5 {

    @Autowired
    private OrderServiceImpl5 orderService;

    @RequestMapping("/order/prod/{pid}")
    public Order order(@PathVariable("pid") Integer pid) {
        return orderService.createOrder(pid);
    }

}

为避免路由重复,记得关闭其他 OrderController的路由

第二步:新建OrderController5Impl.java
在这里插入图片描述

内容如下:

import com.alibaba.fastjson.JSON;
import com.itheima.dao.OrderDao;
import com.itheima.domain.Order;
import com.itheima.domain.Product;
import com.itheima.service.OrderService;
import com.itheima.service.ProductService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderServiceImpl5{

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private ProductService productService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @GlobalTransactional//全局事务控制
    public Order createOrder(Integer pid) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);

        //1 调用商品微服务,查询商品信息
        Product product = productService.findByPid(pid);
        log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));

        //2 下单(创建订单)
        Order order = new Order();
        order.setUid(1);
        order.setUsername("测试用户");
        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));

        //3 扣库存m
        productService.reduceInventory(pid, order.getNumber());

        //4 向mq中投递一个下单成功的消息
        rocketMQTemplate.convertAndSend("order-topic", order);


        return order;
    }

}

目前暂时不需要 sentinel的服务容错机制,先把与其相关的内容都注释了

在这里插入图片描述
在这里插入图片描述
第三步,修改productService.java如下
在这里插入图片描述

import com.itheima.domain.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

//value用于指定调用nacos下哪个微服务
//fallback 指定当调用出现问题之后,要进入到哪个类中的同名方法之下执行备用逻辑
@FeignClient(
        value = "service-product"//,
        //fallback = ProductServiceFallback.class,
        //fallbackFactory = ProductServiceFallbackFactory.class
)
public interface ProductService {
    //@FeignClient的value +  @RequestMapping的value值  其实就是完成的请求地址  "http://service-product/product/" + pid
    //指定请求的URI部分
    @RequestMapping("/product/{pid}")
    Product findByPid(@PathVariable Integer pid);

    //扣减库存
    //参数一: 商品标识
    //参数二:扣减数量
    @RequestMapping("/product/reduceInventory")
    void reduceInventory(@RequestParam("pid") Integer pid,
                         @RequestParam("number") Integer number);
}

第四步,在shop-product,先注释掉nacos的相关注解,现在暂时不需要它
在这里插入图片描述
然后在productController里面添加一个“减库存”的操作
在这里插入图片描述
然后在productService里面生成一个扣库存的方法
在这里插入图片描述
在productServiceImpl里实现这个方法
在这里插入图片描述
接下来修改一下配置文件,因为我们之间把配置文件都写到naocos里面,现在为了简单演示Seata,我们把nacos添加的配置文件删除
在这里插入图片描述
然后把application.yaml里注释掉的都放开,把bootstap.yaml里面的引用远程的都删掉,剩下的内容如下
在这里插入图片描述
然后重启order、product
在这里插入图片描述
下单前查看数据库

在这里插入图片描述
接下来下单一次
在这里插入图片描述
下单后库存已经扣除了
在这里插入图片描述

4.1.2、异常模拟

在shop-product的ProductServiceImpl.java中模拟一个异常
在这里插入图片描述
重启product
我们发现有异常是库存不会减少

4.2、利用Seata服务端管理分布式事务

4.2.1、启动seata

Seata下载:https://github.com/seata/seata/releases
在这里插入图片描述

linux下载tar.gz格式、windows下载zip格式

下载好后,解压
在这里插入图片描述

然后修改regisry.conf(利用Notepad++修改配置文件)
在这里插入图片描述
修改nacos-config.txt
在这里插入图片描述
接下初始化seata在nacos里面的配置

在seata的conf文件夹下:nacos-config.sh 127.0.0.1

在这里插入图片描述
如果初始化成功,nacos中会多出很多seata的相关配置
在这里插入图片描述
在seata的bin目录下启动seata
在这里插入图片描述

启动成功会在nacos中看到seata微服务:serverAddr
在这里插入图片描述

4.2.2、使用seata进行事务控制

在数据库中加入undo_log表,用来记录seata事务日志

CREATE TABLE `undo_log` ( 
	`id` BIGINT(20) NOT NULL AUTO_INCREMENT, 
	`branch_id` BIGINT(20) NOT NULL, 
	`xid` VARCHAR(100) NOT NULL,
	`context` VARCHAR(128) NOT NULL, 
	`rollback_info` LONGBLOB NOT NULL, 
	`log_status` INT(11) NOT NULL, 
	`log_created` DATETIME NOT NULL, 
	`log_modified` DATETIME NOT NULL, 
	`ext` VARCHAR(100) DEFAULT NULL, 
	PRIMARY KEY (`id`), 
	UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) 
) ENGINE = INNODB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

在shop-order、shop-pruduct添加依赖

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

因为要从nacos里面读取配置,所以要保证shop-order、shop-pruduct也有如下依赖

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

在shop-order、shop-pruduct分别新建类DataSourceProxyConfig.java

Seata 是通过代理数据源实现事务分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy 的
Bean,且是 @Primary默认的数据源,否则事务不会回滚,无法实现分布式事务

@Configuration 
public class DataSourceProxyConfig { 
	@Bean 
	@ConfigurationProperties(prefix = "spring.datasource") 
	public DruidDataSource druidDataSource() { 
		return new DruidDataSource(); 
	}
	
	@Primary 
	@Bean 
	public DataSourceProxy dataSource(DruidDataSource druidDataSource) { 
		return new DataSourceProxy(druidDataSource); 
	} 
}

spring.datasources就是指使用的内部配置文件
在这里插入图片描述
接下来将seata的conf目录下的regisry.conf,复制到shop-order、shop-pruduct的resouce目录下

在这里插入图片描述
接下来修改shop-pruduct的bootstrap.yaml

spring:
 application:
  name: service-product 
 cloud: 
  nacos:
   config:
    server-addr: localhost:8848 # nacos的服务端地址 
    namespace: public 
    group: SEATA_GROUP 
 alibaba: 
  seata: 
   tx-service-group: ${spring.application.name}

在这里插入图片描述
如果两边不一样,你也可以直接把tx-service-group改成product-service
shop-order的bootstrap.yaml也是同样这么改

接下来在order微服务开启全局事务
在这里插入图片描述

开始测试

在这里插入图片描述
现在有异常的订单就不会扣库存了

下面是seata的运行流程图分析
在这里插入图片描述

5、Dubbo

Spring-cloud-alibaba-dubbo 是基于SpringCloudAlibaba技术栈对dubbo技术的一种封装,目的在
于实现基于RPC的服务调用。

在这里插入图片描述

将我们之前的项目只保留nacos相关的部分,其他的都删了

现在我们来利用nacos+Dubbo实现远程调用

提供统一业务api

public interface ProductService { 
		Product findByPid(Integer pid);
}

在这里插入图片描述
然后所有实体类都要实现Serializable
在这里插入图片描述

为服务提供者shop-product,添加依赖

<!--dubbo--> 
<dependency>
 <groupId>com.alibaba.cloud</groupId>
 <artifactId>spring-cloud-starter-dubbo</artifactId> 
</dependency>

接下来修改productServiceImpl,暴露服务,把Service注解改成dubbo提供的
在这里插入图片描述

向shop-product的application添加dubbo配置

dubbo:
 scan:
  base-packages: com.itheima.service.impl # 开启包扫描 
 protocols: 
  dubbo:
   name: dubbo # 服务协议 
   port: -1 # 服务端口 
 registry:
  address: spring-cloud://localhost # 注册中心

在这里插入图片描述

服务消费者shop-order,加入依赖

<!--dubbo--> 
<dependency>
 <groupId>com.alibaba.cloud</groupId> 
 <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>

添加dubbo配置

dubbo:
 registry:
  address: spring-cloud://localhost # 注册中心 
 cloud: 
  subscribed-services: service-product # 订阅的提供者名称

引用服务

import com.alibaba.fastjson.JSON;
import com.itheima.domain.Order;
import com.itheima.domain.Product;
import com.itheima.service.OrderService;
import com.itheima.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
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.RestController;

@RestController
@Slf4j
public class OrderController {

    @Autowired
    private OrderService orderService;

    //服务引用
    @Reference
    private ProductService productService;

    @RequestMapping("/order/prod/{pid}")
    public Order order(@PathVariable Integer pid) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);

        //调用商品微服务,查询商品信息
        Product product = productService.findByPid(pid);
        log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));

        //下单(创建订单)
        Order order = new Order();
        order.setUid(1);
        order.setUsername("测试用户");
        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderService.createOrder(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));

        return order;
    }

}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值