本篇紧接上文《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(五) SEATA分布式事务篇(上) 运行原理以及AT模式源码启动版集成》
读写分离/分库分表 配置集成
参考我之前的该篇文章《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(四) (mini-cloud) 集成shardingSphere 读写分离 》
mini-cloud下构建分布式事务案例场景-电商订单-库存分布式事务问题
电商订单-库存分布式事务场景图示
场景描述
订单库存是典型的微服务分布式事务问题,首先看看如上图所示的流程
1.用户下单,订单服务首先调用库存服务校验以及扣除库存
2.如果扣除成功,订单服务则创建一个订单并返回用户提示下单成功
3.如果扣除失败,订单服务则返回用户提示库存不足,下单失败。
场景很简单,正常情况下只要业务数据逻辑正确,本身是没有什么大问题的,
但是注意看红框框住的部分,如果扣除库存成功了,但是订单服务后续业务处理报错了,那么将会是什么结果呢,首先订单服务本身的数据肯定是会回滚的,因为在一个事务里报错了,但是库存服务的数据已经commit了,所以现在就会造成库存多扣了实际订单没有创建的情况,其实实际情况要复杂得多,因为图示是简化版,真实项目里可能会调用了更多的服务,相关联的数据有可能横跨很多数据库,所以分布式事务的控制是很必要的,其实现在有很多分布式事务的解决方案
1. 最简单的是xa分布式事务方案,因为是基于数据库本身的机制进行的,少许代码即可
2.二段式,三段式,这些就和业务挂钩相对紧密了,需要手动去写业务回滚的业务处理部分
3.saga 本身其实也是需要手动写业务归滚部分代码,但是现如今框架会帮助处理大部分逻辑流程
4.最终一致性,一般应用mq消息中间件,业务发起到结束期间允许数据的不一致,最后结束时保证一直,这种情况一般不会发生在强一直行场景下,比如转账,支付,扣除库存等,所以这里不太适合,但是可以配和缓存做伪强一致性处理,后续可以单独写一篇高并发下订单问题篇
4.at 模式,比如seata的at模式,框架本身会帮助你拦截你本次涉及的所有服务的数据库sql,并生成反向sql保存起来,如果成功则删除这些sql,失败就执行这些sql恢复数据,可以参考上一篇,里面对原理有所介绍,下面图是简单画的seata at模式原理图
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(五) SEATA分布式事务篇(上) 运行原理以及AT模式源码启动版集成
mini-cloud 集成订单服务与库存服务(shardingsphere读写分离)
创建订单服务和库存服务
在mini-cloud 框架中有一个simulate模块,专门用来创建各个业务创建的模拟服务,这里我们直接创建两个服务,订单服务和库存服务,过程不细说了,就是正常创建服务并且注册到我们的nacos里面,可以参考我之前的文章,结果如下
创建订单服务和库存服务数据库
由于我们是三个库的读写分离,简化起见只展示库和表设计
商品库存库 与表设计
订单库与表设计
为了简单起见每个库只有一个表,每个表就俩字段,undo_log是后面seata 需要的回滚表,后续回有介绍
创建订单服务和库存服务配置中心内sharesphere 读写分离
由于我们是shareshpere的读写分离库,所以需要配置shareshpere 读写分离并写入nacos配置中心
商品库存库
spring: shardingsphere: props: sql: show: true datasource: names: master,slave0,slave1 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:master}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_goods}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:slave0}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_goods}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root slave1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:slave1}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_goods}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root sharding: master-slave-rules: master: master-data-source-name: master slave-data-source-names: slave0,slave1 application: name: @artifactId@ cloud: nacos: discovery: server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848} config: server-addr: ${spring.cloud.nacos.discovery.server-addr} file-extension: yml shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} profiles: active: @profiles.active@
订单库
spring: shardingsphere: props: sql: show: true datasource: names: master,slave0,slave1 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:master}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_order}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:slave0}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_order}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root slave1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${MYSQL_HOST:slave1}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_order}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root sharding: master-slave-rules: master: master-data-source-name: master slave-data-source-names: slave0,slave1 application: name: @artifactId@ cloud: nacos: discovery: server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848} config: server-addr: ${spring.cloud.nacos.discovery.server-addr} file-extension: yml shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} profiles: active: @profiles.active@ security: enable: false client: ignore-urls: - /order/**
编写订单服务和库存服务相互调用代码
库存服务
fegin 部分
package com.minicloud.goods.fegin; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @FeignClient(contextId = "remoteSimulateGoodsService",value = "mini-cloud-simulate-goods-biz") public interface RemoteSimulateGoodsService { @PostMapping("/goods/subStock/{goodsId}/{num}") boolean subStock(@PathVariable("goodsId")Integer goodsId, @PathVariable("num")Integer num); }
业务部分
GoodsController
package com.minicloud.goods.controller; import com.minicloud.goods.service.GoodsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("goods") public class GoodsController { @Autowired private GoodsService goodsService; /** * @desc: 模拟扣减商品库存 * @param goodsId 商品id * @param num 商品数量 * * */ @PostMapping("/subStock/{goodsId}/{num}") public boolean subStock(@PathVariable("goodsId")Integer goodsId,@PathVariable("num")Integer num){ boolean result = goodsService.subStock(goodsId,num); if(!result){ return false; }else { return true; } } }
GoodsServiceImpl
package com.minicloud.goods.service.impl; import cn.org.atool.fluent.mybatis.base.crud.IUpdate; import com.minicloud.goods.entity.GoodsEntity; import com.minicloud.goods.mapper.GoodsMapper; import com.minicloud.goods.service.GoodsService; import com.minicloud.goods.wrapper.GoodsQuery; import com.minicloud.goods.wrapper.GoodsUpdate; import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.transaction.annotation.ShardingTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.apache.shardingsphere.transaction.core.TransactionTypeHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * @Author alan.wang * @date: 2022-02-17 16:11 */ @Slf4j @Service public class GoodsServiceImpl implements GoodsService { @Autowired private GoodsMapper goodsMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean subStock(Integer goodsId, Integer num) { log.info("开始全局事务,XID = " + RootContext.getXID()); IUpdate iUpdate = new GoodsUpdate().set.goodsStock().applyFunc("goods_stock -"+num).end().where.goodsId().eq(goodsId).end(); goodsMapper.updateBy(iUpdate); GoodsEntity goodsEntity = goodsMapper.findById(goodsId); if(goodsEntity.getGoodsStock()>0){ log.info("库存扣除成功 ,goodsId:{},num:{}",goodsId,num); return true; }else { log.info("库存扣除失败 ,goodsId:{},num:{}",goodsId,num); return false; } } }
订单服务
OrderController
package com.minicloud.simulate.order.controller; import com.minicloud.simulate.order.service.OrderService; import io.seata.spring.annotation.GlobalTransactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; /** * @desc: 模拟生成订单 * **/ @PutMapping("/create") public ResponseEntity createOrder() throws Exception { orderService.newOrder(); return ResponseEntity.ok().build(); } }
OrderServiceImpl
package com.minicloud.simulate.order.service.impl; import com.minicloud.goods.fegin.RemoteSimulateGoodsService; import com.minicloud.simulate.order.entity.OrdersEntity; import com.minicloud.simulate.order.mapper.OrdersMapper; import com.minicloud.simulate.order.service.OrderService; import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.transaction.annotation.ShardingTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.apache.shardingsphere.transaction.core.TransactionTypeHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; @Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired private OrdersMapper ordersMapper; @Resource private RemoteSimulateGoodsService remoteSimulateGoodsService; @Override @Transactional(rollbackFor = Exception.class) @ShardingTransactionType(TransactionType.BASE) public String newOrder() throws Exception { log.info("开始全局事务,XID = " + RootContext.getXID()); //demo 测试使用,实际应该用有意义的分布式id String orderId = System.nanoTime()+""; OrdersEntity ordersEntity = new OrdersEntity(); ordersEntity.setOrderId(orderId); ordersEntity.setOrderCreateTime(LocalDateTime.now()); ordersMapper.insertWithPk(ordersEntity); boolean result = remoteSimulateGoodsService.subStock(1,3); if(result){ log.info("创建订单成功 ,id:{}",orderId); }else { throw new Exception("库存不足,创建订单失败"); } return orderId; } }
启动服务并测试正常情况
首先启动所有服务,注册中心,认证中心,用户中心,网关,订单服务,库存服务,并查看
nacos服务注册情况
我们首先在库存数据库创建一个商品并且赋值初始库存为10
然后用请求创建订单接口,针对goods_id是1的商品扣除3个,正常情况下应该是库存少3变为7
然后订单表里多一条数据,那么我们测一下
运行结果:
库存表确实变为了7
订单表创建了一条数据
篇幅原因,下篇介绍异常情况下数据不一致以及seata at 的强一致性回滚