写在前面
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;
}
}