今天给大家带来一个技术点的使用分享,就是分布式事务之seata使用。分布式事务的解决方案,是大家在面试中不可避免会被问到的,而且分布式事务的解决方案也非常多。

今天威哥就以seata为例,把seata的使用在这里做了一个总结,希望能够帮助到大家。本案例在实施过程中,威哥力求简单明了,希望大家在学习过程中能够掌握其中的每个细节。好了,废话不多说,如果我们要学习seata,首先需要具备如下技术储备:

  • 数据库事务的基本知识;

  • maven工具的使用;

  • 熟悉SpringCloudAlibaba技术栈;

  • 掌握SpringDataJPA简单使用;

一. Seata基本概念

1.seata是什么

Seata是阿里巴巴中间件团队发起了开源项目,其愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题,后来更名为 Seata。

Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。它把一个分布式事务理解成一个包含了若干分支事务的全局事务。

全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。

2.seata基本架构

听到这里,是不是觉得很晦涩?那么威哥通过一幅图来帮助你们进一步理解seata的架构:

通过这幅图,我们看到了seata的三个重要的组件,分别是TC TM RM。那么他们到底是什么东西呢?

  • TC:Transaction Coordinator事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。

  • TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务。

  • RM:Resource Manager资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务。

3.seata执行流程

搞清楚了这几个组件的含义之后,那么seata的整个执行流程我们就可以梳理清楚了:

  • A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID。

  • 服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖。

  • A服务执行分支事务,向数据库做操作。

  • A服务开始远程调用B服务,此时XID会在微服务的调用链上传播。

  • B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖。

  • B服务执行分支事务,向数据库做操作。

  • 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚。

  • TC协调其管辖之下的所有分支事务,决定是否回滚。

二. 案例环境搭建

我们搞清楚Seata的相关概念之后,现在威哥带领大家实现一个需求:通过订单微服务实现下订单的操作,然后通知库存微服务进行库存的扣减。

1. 前期准备

我们需要先准备订单和商品实体类。

//商品@Entity(name="shop_product")@DatapublicclassProduct{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateIntegerpid;//主键privateStringpname;//商品名称privateDoublepprice;//商品价格privateIntegerstock;//库存}//订单@Entity(name="shop_order")@DatapublicclassOrder{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongoid;//订单idprivateIntegeruid;//用户idprivateStringusername;//用户名privateIntegerpid;//商品idprivateStringpname;//商品名称privateDoublepprice;//商品单价privateIntegernumber;//购买数量}

我们还需要准备项目必备的pom依赖:

这是父工程的pom.xml文件:

org.springframework.cloudspring-cloud-dependencies${spring-cloud.version}pomimportcom.alibaba.cloudspring-cloud-alibaba-dependencies${spring-cloud-alibaba.version}pomimport

2. 搭建对应的微服务

现在我们分别搭建商品微服务和订单微服务

2.1 创建公共通用模块

我们创建shop-common模块,专门存放一些公共的实体类和工具类,便于其他模块进行共享。

2.1.1 在公共模块添加相关的依赖

org.springframework.cloudspring-cloud-dependencies${spring-cloud.version}pomimportcom.alibaba.cloudspring-cloud-alibaba-dependencies${spring-cloud-alibaba.version}pomimport

然后把之前准备的实体类都拷贝到这个shop-common中来。

2.2 搭建订单微服务模块

2.2.1 添加必要依赖

取名shop-order,在这个模块里面添加相关的依赖。

com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discoveryorg.springframework.cloudspring-cloud-starter-openfeignorg.springframework.bootspring-boot-starter-webcom.qf.commonspringcloudAlibaba-common1.0-SNAPSHOTcom.qf.feignspringcloudAlibaba-order-product-feign1.0-SNAPSHOTorg.springframework.bootspring-boot-starter-testjunitjunitcom.alibaba.cloudspring-cloud-starter-alibaba-nacos-configcom.alibaba.cloudspring-cloud-starter-alibaba-seata

2.2.2 编写controller

在这里,我们编写一个下单操作的服务接口。

@RestController@RequestMapping("order")@Slf4jpublicclassOrderController{@AutowiredOrderService5orderService5;@RequestMapping("prod/{pid}")publicOrderorder(@PathVariable("pid")Integerpid){returnorderService5.createOrder(pid);}}

2.2.3 编写service

publicinterfaceOrderService{OrdercreateOrder(Integerpid);}
@Service@Slf4jpublicclassOrderServiceImplimplementsOrderService{@AutowiredOrderFeignorderFeign;@AutowiredOrderDaoorderDao;@OverridepublicOrdercreateOrder(Integerpid){//查询指定的商品信息Productproduct=orderFeign.findProductByPid(pid);log.info("查询到的商品信息是:{}",JSON.toJSONString(product));//执行下单的操作Orderorder=newOrder();order.setUid(1003);order.setUsername("测试Seata案例");order.setPid(pid);order.setPname(product.getPname());order.setPprice(product.getPprice());//设置订单中的商品数量order.setNumber(1);orderDao.save(order);log.info("订单创建成功,订单信息是:{}",JSON.toJSONString(order));//执行扣减库存的操作orderFeign.reduceStock(pid,order.getNumber());returnorder;}}

2.2.4 编写feign客户端

@FeignClient(name="service-product")publicinterfaceOrderFeign{@RequestMapping("product/{pid}")publicProductfindProductByPid(@PathVariable("pid")Integerpid);@RequestMapping("product/reduceStock")voidreduceStock(@RequestParam("pid")Integerpid,@RequestParam("number")Integernumber);}

2.2.5 编写dao

publicinterfaceOrderDaoextendsJpaRepository<Order,Integer>{}

2.3 搭建商品微服务模块

2.3.1 添加必要依赖

取名shop-product,在这个模块里面添加相关的依赖。

com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discoveryorg.springframework.bootspring-boot-starter-webcom.qf.commonspringcloudAlibaba-common1.0-SNAPSHOTcom.alibaba.cloudspring-cloud-starter-alibaba-nacos-configcom.alibaba.cloudspring-cloud-starter-alibaba-seata

2.3.2 编写controller

@RestController@Slf4j@RefreshScope//配置信息的即时刷新publicclassProductController{@AutowiredProductServiceproductService;//根据id查询对应的商品信息@RequestMapping("product/{pid}")publicProductfindProductByPid(@PathVariable("pid")Integerpid){Productproduct=productService.findProductByPid(pid);//JSON.toJSONString把指定数据转换成json串log.info("查询到的对应的商品是:"+JSON.toJSONString(product));returnproduct;}//扣减库存@RequestMapping("product/reduceStock")publicvoidreduceStock(@RequestParam("pid")Integerpid,@RequestParam("number")Integernumber){productService.reduceStock(pid,number);}}

2.3.3 编写service

publicinterfaceProductService{ProductfindProductByPid(Integerpid);voidreduceStock(Integerpid,Integernumber);}
@ServicepublicclassProductServiceImplimplementsProductService{@AutowiredProductDaoproductDao;@OverridepublicProductfindProductByPid(Integerpid){Optionaloptional=productDao.findById(pid);returnoptional.get();}@OverridepublicvoidreduceStock(Integerpid,Integernumber){Productproduct=productDao.findById(pid).get();product.setStock(product.getStock()-number);productDao.save(product);}}

2.3.4 编写dao

publicinterfaceProductDaoextendsJpaRepository<Product,Integer>{}

现在我们启动测试,目前代码是没有什么问题的。但是如果我手动模拟异常。具体操作如下:

此时我们再去测试,这个时候就出现了问题了。我们发现订单能够下单成功,但是库存没有扣减。这样就出现了数据不一致的事务问题。那么我们可以使用seata来帮我们解决问题。

三. 配置使用seata

1. 下载seata

同学们可以在如下资源链接上进行下载:

下载地址:https://github.com/seata/seata/releases/v0.9.0/

2. 配置Seata

我们下载下来之后,会是一个压缩包。我们把这个压缩包打开之后进行相关配置。

2.1 修改registry.conf,指定seata使用nacos注册中心

registry{#支持的注册中心有:file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 我们使用自己的注册中心即可所以删除其他注册中心相关的配置type="nacos"nacos{serverAddr="localhost"namespace=""cluster="default"}}config{#file、nacos、apollo、zk、consul、etcd3type="nacos"nacos{serverAddr="localhost"namespace=""}}

2.2 修改nacos-config.txt,指定我们的服务名称

2.3 初始化seata在nacos中的配置

我们需要把seata相关的配置信息在nacos配置中心进行注册。

#初始化seata的nacos配置#注意:这里要保证nacos是已经正常运行的cdconfnacos-config.sh127.0.0.1

执行成功后可以打开Naco的控制台,在配置列表中,可以看到初始化了很多Group为SEATA_GROUP的配置。

2.4 启动seata服务

切换到bin目录执行以下命令:

cdbinseata-server.bat-p9000-mfile

启动后在 Nacos 的服务列表下面可以看到一个名为 serverAddr的服务。如果入下图所示,小伙伴们,seata服务启动成功!!!!!

3. 使用seata进行事务控制

3.1 初始化一张数据表,用来seata进行日志记录

CREATETABLE`undo_log`(`id`BIGINT(20)NOTNULLAUTO_INCREMENT,`branch_id`BIGINT(20)NOTNULL,`xid`VARCHAR(100)NOTNULL,`context`VARCHAR(128)NOTNULL,`rollback_info`LONGBLOBNOTNULL,`log_status`INT(11)NOTNULL,`log_created`DATETIMENOTNULL,`log_modified`DATETIMENOTNULL,`ext`VARCHAR(100)DEFAULTNULL,PRIMARYKEY(`id`),UNIQUEKEY`ux_undo_log`(`xid`,`branch_id`))ENGINE=INNODBAUTO_INCREMENT=1DEFAULTCHARSET=utf8;

3.2 在微服务中添加seata相关的依赖

com.alibaba.cloudspring-cloud-starter-alibaba-seatacom.alibaba.cloudspring-cloud-starter-alibaba-nacos-config

3.3 配置DataSourceProxyConfig代理数据源

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

在shop-product和shop-order里面都添加如下配置类:

@ConfigurationpublicclassDataSourceProxyConfig{@Bean@ConfigurationProperties(prefix="spring.datasource")publicDruidDataSourcedruidDataSource(){returnnewDruidDataSource();}@Primary@BeanpublicDataSourceProxydataSource(DruidDataSourcedruidDataSource){returnnewDataSourceProxy(druidDataSource);}}
  • 在shop-product和shop-order的resources目录添加registry.conf(直接将seata里面的配置复制过来即可)。

  • 在shop-product和shop-order的resources目录添加bootstrap.yml里面,然后添加配置。

3.4 在shop-order微服务中开启全局事务

4. 结果测试

我们发生请求:http://localhost:8091/order5/prod/1

此时查看数据库,我们发现事务问题得到了控制。就是当发生异常的时候,下单的记录被回滚了,而且库存也没有出现扣减。

到现在,我们的分布式事务就得到了控制,小伙伴们,你们有没有学会呢?可以留言或者java学习+资料获取加q群:691533824 扫码学习啦!