从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(五) SEATA分布式事务篇(中)shardingshere 多库读写分离/分库分表下分布式事务完整代码及案例

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(五) SEATA分布式事务篇(中)shardingshere 多库读写分离/分库分表下分布式事务完整代码及案例

image.png 本篇紧接上文《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(五) SEATA分布式事务篇(上) 运行原理以及AT模式源码启动版集成》


读写分离/分库分表 配置集成


参考我之前的该篇文章《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(四) (mini-cloud) 集成shardingSphere 读写分离 》


mini-cloud下构建分布式事务案例场景-电商订单-库存分布式事务问题


电商订单-库存分布式事务场景图示


1.png

场景描述

订单库存是典型的微服务分布式事务问题,首先看看如上图所示的流程


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模式源码启动版集成

image.png

mini-cloud 集成订单服务与库存服务(shardingsphere读写分离)


创建订单服务和库存服务


在mini-cloud 框架中有一个simulate模块,专门用来创建各个业务创建的模拟服务,这里我们直接创建两个服务,订单服务和库存服务,过程不细说了,就是正常创建服务并且注册到我们的nacos里面,可以参考我之前的文章,结果如下

1.png

创建订单服务和库存服务数据库

由于我们是三个库的读写分离,简化起见只展示库和表设计

商品库存库 与表设计

1.png

1.png

订单库与表设计

1.png

1.png

为了简单起见每个库只有一个表,每个表就俩字段,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 部分

1.png

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;
    }
}

启动服务并测试正常情况


首先启动所有服务,注册中心,认证中心,用户中心,网关,订单服务,库存服务,并查看

1.png

nacos服务注册情况

image.png

我们首先在库存数据库创建一个商品并且赋值初始库存为10

1.png

1.png

然后用请求创建订单接口,针对goods_id是1的商品扣除3个,正常情况下应该是库存少3变为7

然后订单表里多一条数据,那么我们测一下

1.png

运行结果:

1.png

库存表确实变为了7

1.png

订单表创建了一条数据

1.png

篇幅原因,下篇介绍异常情况下数据不一致以及seata at 的强一致性回滚

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
16天前
|
Java UED Sentinel
微服务守护神:Spring Cloud Sentinel,让你的系统在流量洪峰中稳如磐石!
【8月更文挑战第29天】Spring Cloud Sentinel结合了阿里巴巴Sentinel的流控、降级、熔断和热点规则等特性,为微服务架构下的应用提供了一套完整的流量控制解决方案。它能够有效应对突发流量,保护服务稳定性,避免雪崩效应,确保系统在高并发下健康运行。通过简单的配置和注解即可实现高效流量控制,适用于高并发场景、依赖服务不稳定及资源保护等多种情况,显著提升系统健壮性和用户体验。
44 1
|
23天前
|
资源调度 Java 调度
Spring Cloud Alibaba 集成分布式定时任务调度功能
定时任务在企业应用中至关重要,常用于异步数据处理、自动化运维等场景。在单体应用中,利用Java的`java.util.Timer`或Spring的`@Scheduled`即可轻松实现。然而,进入微服务架构后,任务可能因多节点并发执行而重复。Spring Cloud Alibaba为此发布了Scheduling模块,提供轻量级、高可用的分布式定时任务解决方案,支持防重复执行、分片运行等功能,并可通过`spring-cloud-starter-alibaba-schedulerx`快速集成。用户可选择基于阿里云SchedulerX托管服务或采用本地开源方案(如ShedLock)
|
16天前
|
Cloud Native Java Nacos
微服务时代的新宠儿!Spring Cloud Nacos实战指南,带你玩转服务发现与配置管理,拥抱云原生潮流!
【8月更文挑战第29天】Spring Cloud Nacos作为微服务架构中的新兴之星,凭借其轻量、高效的特点,迅速成为服务发现、配置管理和治理的首选方案。Nacos(命名和配置服务)由阿里巴巴开源,为云原生应用提供了动态服务发现及配置管理等功能,简化了服务间的调用与依赖管理。本文将指导你通过五个步骤在Spring Boot项目中集成Nacos,实现服务注册、发现及配置动态管理,从而轻松搭建出高效的微服务环境。
84 0
|
17天前
|
人工智能 前端开发 Java
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
本文介绍了如何使用 **Spring Cloud Alibaba AI** 构建基于 Spring Boot 和 uni-app 的聊天机器人应用。主要内容包括:Spring Cloud Alibaba AI 的概念与功能,使用前的准备工作(如 JDK 17+、Spring Boot 3.0+ 及通义 API-KEY),详细实操步骤(涵盖前后端开发工具、组件选择、功能分析及关键代码示例)。最终展示了如何成功实现具备基本聊天功能的 AI 应用,帮助读者快速搭建智能聊天系统并探索更多高级功能。
148 2
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
|
8天前
|
缓存 Java 应用服务中间件
随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架
【9月更文挑战第6天】随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架。Nginx作为高性能的HTTP反向代理服务器,常用于前端负载均衡,提升应用的可用性和响应速度。本文详细介绍如何通过合理配置实现Spring Boot与Nginx的高效协同工作,包括负载均衡策略、静态资源缓存、数据压缩传输及Spring Boot内部优化(如线程池配置、缓存策略等)。通过这些方法,开发者可以显著提升系统的整体性能,打造高性能、高可用的Web应用。
32 2
|
1月前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
1月前
|
缓存 NoSQL Java
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
Spring Cache 是 Spring 提供的简易缓存方案,支持本地与 Redis 缓存。通过添加 `spring-boot-starter-data-redis` 和 `spring-boot-starter-cache` 依赖,并使用 `@EnableCaching` 开启缓存功能。JetCache 由阿里开源,功能更丰富,支持多级缓存和异步 API,通过引入 `jetcache-starter-redis` 依赖并配置 YAML 文件启用。Layering Cache 则提供分层缓存机制,需引入 `layering-cache-starter` 依赖并使用特定注解实现缓存逻辑。
192 1
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
|
26天前
|
Java 微服务 Spring
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
文章介绍了如何利用Spring Cloud Alibaba快速构建大型电商系统的分布式微服务,包括服务限流降级等主要功能的实现,并通过注解和配置简化了Spring Cloud应用的接入和搭建过程。
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
|
29天前
|
运维 Java Nacos
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心
|
2月前
|
资源调度 Java 调度
Spring Cloud Alibaba 集成分布式定时任务调度功能
Spring Cloud Alibaba 发布了 Scheduling 任务调度模块 [#3732]提供了一套开源、轻量级、高可用的定时任务解决方案,帮助您快速开发微服务体系下的分布式定时任务。
14587 23