Seata 分布式事务 二
接上文: https://mp.csdn.net/mp_blog/creation/success/124003885
SEATA:
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
4. 订单/库存/账户业务微服务准备
4.1 业务需求:
下订单->减库存->扣余额->改(订单)状态
以下数据库连接可以考虑用 MybatisPlus 接入
4.2 新建订单Order-Module
模块名称: seata-order-service2001
4.2.1 pom
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos config-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
<!-- </dependency>-->
<!-- seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.5.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 上面 有可能会报错:
Caused by: io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext
,换成下面的依赖即可,但是需要看版本对应-->
<!-- seata 依赖 -->
<!-- <dependency>-->
<!-- <groupId>io.seata</groupId>-->
<!-- <artifactId>seata-spring-boot-starter</artifactId>-->
<!-- <version>1.4.0</version>-->
<!-- </dependency>-->
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.mybatis.spring.boot</groupId>-->
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!-- <version>2.0.0</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.pyh.springcloud</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
4.2.2 yaml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
# alibaba:
# seata:
# tx-service-group: seata-order-group
nacos:
discovery:
#server-addr: 192.168.226.128:8848 # #Nacos服务注册中心地址
# 换成 nginx 的 1111 端口 nginx - keepalived - nacos
#serverAddr: 192.168.226.129:1111
server-addr: 192.168.226.129:1111
namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
#这里的名字就是registry.conf中 nacos的group名字
group: SEATA_GROUP
userName: "nacos"
password: "nacos"
# config: # 目前暂时用不到 nacos.config
# server-addr: 192.168.226.129:1111 # Nacos 作为配置中心地址
# file-extension: yaml # 指定 yaml 格式的配置
# group: DEFAULT_GROUP # 默认 DEFAULT_GROUP # DEV_GROUP
# namespace: # 不填写则默认default, 这个在nacos的配置中找 fc3baf0a-17c6-4bc4-809b-6fc947adb309
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://192.168.226.129:3306/seata_order?serverTimezone=UTC
url: jdbc:mysql://192.168.226.129:3306/seata_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
management:
endpoints:
web:
exposure:
include: '*'
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml # 不能与mybatis.configuration同用
mapper-locations: classpath*:mybatis/mapper/*.xml
configuration: #指定mybatis全局配置文件中的相关配置, mybatis-config.xml无效
map-underscore-to-camel-case: true
mybatis-plus: #myBatis plus的配置文件位置
mapper-locations: classpath*:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
seata:
enabled: true
application-id: ${spring.application.name} #你的当前服务的application name
#这里的名字与file.conf中vgroup_mapping.seata-order-group = "default"相同
tx-service-group: seata-order-group
enable-auto-data-source-proxy: true
service:
#这里的名字与file.conf中vgroup_mapping.seata-order-group = "default"相同
vgroup-mapping:
seata-order-group: default
vgroupMapping:
seata-order-group: default
#这里的名字与file.conf中default.grouplist = "127.0.0.1:8091"相同
default:
grouplist: 192.168.226.128:8091
config:
type: nacos
nacos:
namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
server-addr: 192.168.226.129:1111
#这里的名字就是registry.conf中 nacos的group名字
group: SEATA_GROUP
userName: "nacos"
password: "nacos"
registry:
type: nacos
nacos:
application: seata-server
#这里的地址就是你的nacos的地址,可以更换为线上
server-addr: 192.168.226.129:1111
#这里的名字就是registry.conf中 nacos的group名字
group: SEATA_GROUP
namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
userName: "nacos"
password: "nacos"
4.2.3 file.conf(deprecated)
由于使用了那cos作为配置文件存放地址,本节点可忽略
注意:
1). vgroup_mapping.sentinel_tx_group = “default”
sentinel_tx_group 是在sentinel中配置的事务组名称
2). database store 填好sql的连接参数
transport {
# tcp, unix-domain-socket
type = "TCP"
#NIO, NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = false
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThreadPrefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#vgroup->rgroup
#vgroup_mapping.seata-order-group = "default"
vgroupMapping.seata-order-group = "default"
#only support single node
#default.grouplist = "127.0.0.1:8091"
default.grouplist = "192.168.226.128:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
## transaction log store, only used in server side
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
#url = "jdbc:mysql://127.0.0.1:3306/seata"
url = "jdbc:mysql://192.168.226.129:3306/seata?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
## server configuration, only used in server side
server {
recovery {
#schedule committing retry period in milliseconds
committingRetryPeriod = 1000
#schedule asyn committing retry period in milliseconds
asynCommittingRetryPeriod = 1000
#schedule rollbacking retry period in milliseconds
rollbackingRetryPeriod = 1000
#schedule timeout retry period in milliseconds
timeoutRetryPeriod = 1000
}
undo {
logSaveDays = 7
#schedule delete expired undo_log in milliseconds
logDeletePeriod = 86400000
}
#check auth
enableCheckAuth = true
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
maxCommitRetryTimeout = "-1"
maxRollbackRetryTimeout = "-1"
rollbackRetryTimeoutUnlockEnable = false
}
## metrics configuration, only used in server side
metrics {
enabled = false
registryType = "compact"
# multi exporters use comma divided
exporterList = "prometheus"
exporterPrometheusPort = 9898
}
4.2.4 registry.conf(deprecated)
由于使用了那cos作为配置文件存放地址,本节点可忽略
注意:
1). type=“nacos”
2). 填写好nacos的相关参数
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
# serverAddr = "localhost:8848"
# serverAddr: 192.168.226.128:8848 # 配置 nacos 地址
# 换成 nginx 的 1111 端口 nginx - keepalived - nacos
serverAddr = "192.168.226.129:1111"
namespace = ""
cluster = "default"
#username = "nacos"
#password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
4.2.5 bean 或者 entity 或者 domain
Order:
package com.pyh.springcloud.bean;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_order")
public class Order {
private Long id;
private Long orderId;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
CommonResult:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
// 404 not found
private Integer code;
private String message;
private T data;
// 上面配置了空参和全参, 这里自定义一个只有编码和消息的构造方法
public CommonResult(Integer code, String message){
this(code,message,null);
}
}
4.2.6 Dao(或者Mapper) 及其实现
1). OrderDao:
package com.pyh.springcloud.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pyh.springcloud.bean.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/* mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法 */
@Mapper
public interface OrderDao extends BaseMapper<Order> {
// 1. 新建订单
void createOrder (Order order);
// 2. 修改订单状态, 从 0 改为 1
void update(@Param("orderId") Long orderId, @Param("status") Integer status);
}
2). OrderMapper.xml: (resources文件夹下新建mapper文件夹后添加)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pyh.springcloud.dao.OrderDao">
<!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
map-underscore-to-camel-case: true -->
<resultMap id="BaseResultMap" type="com.pyh.springcloud.bean.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="order_id" property="orderId" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="createOrder" useGeneratedKeys="true" keyProperty="id">
insert into t_order (id, order_id, user_id, product_id, count, money, status )
values (null, #{orderId}, #{userId}, #{productId}, #{count}, #{money}, 0 )
</insert>
<update id="update" >
update t_order set status = 1 where order_id = #{orderId} and status = #{status}
</update>
</mapper>
4.2.7 Service 接口及其实现
service 要创建3个service
OrderService:
package com.pyh.springcloud.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pyh.springcloud.bean.Order;
public interface OrderService extends IService<Order> {
void create(Order order);
}
AccountService:
package com.pyh.springcloud.service;
import com.pyh.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "seata-account-service")
public interface AccountService {
// 该用户账户减少指定额度
@PostMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
StorageService:
package com.pyh.springcloud.service;
import com.pyh.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")
public interface StorageService{
// 某个商品减少指定库存量
@PostMapping("/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
OrderServiceImpl:
package com.pyh.springcloud.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pyh.springcloud.bean.Order;
import com.pyh.springcloud.dao.OrderDao;
import com.pyh.springcloud.service.AccountService;
import com.pyh.springcloud.service.OrderService;
import com.pyh.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {
@Resource
OrderDao orderDao;
@Resource
AccountService accountService;
@Resource
StorageService storageService;
/**
* 下订单->减库存->扣余额->改(订单)状态
*/
@Override
public void create(Order order) {
log.info("--->step1 开始新建订单");
// 1. 新建订单
orderDao.createOrder(order);
log.info("--->step2 订单微服务开始调用库存微服务, 扣减数量 start");
// 2. 扣减库存
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->step2 订单微服务开始调用库存微服务, 扣减数量 end");
log.info("----->step3 订单微服务开始调用账户微服务, 扣减money start");
// 3. 扣账户钱
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->step3 订单微服务开始调用账户微服务, 作扣减money end");
log.info("----->step4 修改订单状态从0 到 1 ,代表完成 start");
// 4. 修改订单状态为完成
// 这里传0,是把订单状态从0 改到 1
orderDao.update(order.getOrderId(),0 );
log.info("----->step4 修改订单状态从0 到 1 ,代表完成 end");
log.info("----->新建订单 完成!!!");
}
}
4.2.8 controller 类
package com.pyh.springcloud.controller;
import com.pyh.springcloud.bean.Order;
import com.pyh.springcloud.entities.CommonResult;
import com.pyh.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
4.2.9 config 配置
MybatisPlusConfig:
package com.pyh.springcloud.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.pyh.springcloud.dao")
public class MyBatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
// @Bean
// public MybatisPlusInterceptor mybatisPlusInterceptor() {
// MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//
// //这是分页拦截器
// PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// paginationInnerInterceptor.setMaxLimit(500L); // 每次最大查询500条数据
// paginationInnerInterceptor.setOverflow(true); //超出页数后回滚
//
// interceptor.addInnerInterceptor(paginationInnerInterceptor);
// return interceptor;
// }
// @Bean
// public ConfigurationCustomizer configurationCustomizer() {
// return configuration -> configuration.setUseDeprecatedExecutor(false);
// }
}
MyDataSourceConfig:
package com.pyh.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* 用 seata 数据源进行代理
*/
//@Deprecated
@Configuration //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {
@Value("${mybatis-plus.mapper-locations}")
private String mapperLocations;
// @ConfigurationProperties("spring.datasource")
@ConfigurationProperties(prefix = "spring.datasource") // 与上面相同
@Bean
public DataSource dataSource() throws SQLException {
## 只需新建DruidDataSource,其他通过配置实现
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 官网信息:
* Register DataSourceProxy bean by yourself is discouraged since 1.3.
* If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
* just register and use Datasource bean in your old way.
* @param dataSource
* @return
* @throws Exception
*/
// 目前使用seata 1.4.0版本, 官网建议1.3版本开始,不用自定义注册DataSourceProxy了
// seata datasource proxy
// @Bean
// public DataSourceProxy dataSourceProxy(DataSource dataSource) {
// return new DataSourceProxy(dataSource);
// }
//代理mybaits-plus
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean;
}
//代理mybatis / mybatis-config
/* @Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}*/
}
4.2.10 主启动类
@EnableDiscoveryClient
@EnableFeignClients
//取消数据源自动创建的配置, 使用我们自己的数据源管理配置 MyDataSourceConfig
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
启动 nacos, 启动seata, 并开始测试
cd /usr/local/seata_1.4.0/seata/bin/
sh seata-server.sh -h 192.168.226.128 -p 8091
4.3 踩坑说明
4.3.1 踩坑1 版本不匹配
错误如下:
Caused by: io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext
此类报错多是版本不匹配,解决方案是修改seata的版本问题:
参考资料: https://blog.csdn.net/qq_21959403/article/details/116135702
修改内容如下:
<!-- seata 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.5.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
4.3.2 踩坑2 nacos config可看情况配置
maven 写了多余的nacos config starter依赖
Caused by: com.alibaba.nacos.api.exception.NacosException: endpoint is blank
参考资料:https://blog.csdn.net/gxy03/article/details/111463882
解决方案:如果不用config,删除即可
4.3.3 踩坑3
The following method did not exist: org.apache.ibatis.session.Configuration.getLanguageDriver(Ljava/lang/Class;)Lorg/apache/ibatis/scripting/LanguageDriver;
原因: Mybatis-plus与Mybatis依赖冲突问题
参考资料:https://blog.csdn.net/Octopus21/article/details/114879758
处理方案:看上方参考资料
升级mybatis包,由于引入的mybatis-plus-boot-starter已经集成了MyBatis包,所以这里需要首先将其exclude,然后引入单独的较高版本的MyBatis包依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
4.3.4 踩坑4 切记要开防火墙
这个问题卡了两个晚上!!!
firewall-cmd --permanent --add-port=8091/tcp
firewall-cmd --reload
4.4 新建库存Storage-Module
4.4.1 pom
同 4.2.1
4.4.2 yaml
同 4.2.2, 以下部分修改,其余相同
server:
port: 2002
spring:
application:
name: seata-storage-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC
url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
4.4.3 file.conf(deprecated)
4.4.4 registry.config(deprecated)
4.4.5 bean 或者 entity 或者 domain
package com.phy.springcloud.bean;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_storage")
public class Storage {
private Long id;
// 产品id
private Long productId;
//总库存
private Integer total;
//已用库存
private Integer used;
//剩余库存
private Integer residue;
}
CommonResult.java 放在公共模块里面
4.4.6 Dao(或者Mapper) 及其实现
1). StorageDao
package com.phy.springcloud.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.phy.springcloud.bean.Storage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法
*/
@Mapper
public interface StorageDao extends BaseMapper<Storage> {
// 扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
2). StorageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.phy.springcloud.dao.StorageDao">
<!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
map-underscore-to-camel-case: true
-->
<resultMap id="BaseResultMap" type="com.phy.springcloud.bean.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease" >
update t_storage set used = used + ${count},residue = residue - ${count} where product_id = #{productId}
</update>
</mapper>
4.4.7 Service 接口及其实现
1). StorageService:
package com.phy.springcloud.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.phy.springcloud.bean.Storage;
public interface StorageService extends IService<Storage> {
/**
* 扣减库存
* @param productId
* @param count
*/
void decrease(Long productId, Integer count);
}
2).StorageServiceImpl
package com.phy.springcloud.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.phy.springcloud.bean.Storage;
import com.phy.springcloud.dao.StorageDao;
import com.phy.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageDao, Storage> implements StorageService {
@Resource
StorageDao storageDao;
@Override
public void decrease(Long productId, Integer count) {
log.info("-----> StorageService库存微服务, 扣减库存 start");
storageDao.decrease(productId, count);
log.info("-----> StorageService库存微服务, 扣减库存 end");
}
}
4.4.8 controller 类
StorageController:
package com.phy.springcloud.controller;
import com.phy.springcloud.service.StorageService;
import com.pyh.springcloud.entities.CommonResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class StorageController {
@Resource
StorageService storageService;
// 某个商品减少指定库存量
/**
* 具体项目中还需考虑单号等数据
* @param productId
* @param count
* @return
*/
@PostMapping("/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){
storageService.decrease(productId,count);
return new CommonResult(200,"扣减库存成功");
}
}
4.4.9 config 配置
package com.phy.springcloud.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.phy.springcloud.dao")
public class MyBatisPlusConfig {
}
MyDataSourceConfig:
package com.phy.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* 用 seata 数据源进行代理
*/
//@Deprecated
@Configuration //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {
@Value("${mybatis-plus.mapper-locations}")
private String mapperLocations;
// @ConfigurationProperties("spring.datasource")
@ConfigurationProperties(prefix = "spring.datasource") // 与上面相同
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 官网信息:
* Register DataSourceProxy bean by yourself is discouraged since 1.3.
* If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
* just register and use Datasource bean in your old way.
* @param dataSource
* @return
* @throws Exception
*/
// seata datasource proxy
// @Bean
// public DataSourceProxy dataSourceProxy(DataSource dataSource) {
// return new DataSourceProxy(dataSource);
// }
//代理mybaits-plus
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean;
}
//代理mybatis / mybatis-config
/* @Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}*/
}
4.4.10 主启动类
SeataStorageMainApp2002.java
package com.phy.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
//取消数据源自动创建的配置, 使用我们自己的数据源管理配置 MyDataSourceConfig
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataStorageMainApp2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMainApp2002.class,args);
}
}
4.5 新建账户Account-Module
4.5.1 pom
同 4.2.1
4.5.2 yaml
除以下部分修改,其余同4.2.2
server:
port: 2003
spring:
application:
name: seata-account-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC
url: jdbc:mysql://192.168.226.129:3306/seata_account?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
4.5.3 file.conf(deprecated)
4.5.4 registry.conf(deprecated)
4.5.5 bean 或者 entity 或者 domain
package com.pyh.springcloud.bean;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_account")
public class Account {
private Long id;
// 用户id
private Long userId;
// 用户总余额
private Double total;
// 用户已用余额
private Double used;
// 用户剩余余额;
private Double residue;
}
4.5.6 Dao(或者Mapper) 及其实现
1)AccountDao.java
package com.pyh.springcloud.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pyh.springcloud.bean.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
/**
* mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法
*/
@Mapper
public interface AccountDao extends BaseMapper<Account> {
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
2). AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pyh.springcloud.dao.AccountDao">
<!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
map-underscore-to-camel-case: true
-->
<resultMap id="BaseResultMap" type="com.pyh.springcloud.bean.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease" >
update t_account set used = used + ${money},residue = residue - ${money} where user_id = #{userId}
</update>
</mapper>
4.5.7 Service 接口及其实现
AccountService:
package com.pyh.springcloud.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pyh.springcloud.bean.Account;
import java.math.BigDecimal;
public interface AccountService extends IService<Account> {
void decrease(Long userId, BigDecimal money);
}
AccountServiceImpl:
package com.pyh.springcloud.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pyh.springcloud.bean.Account;
import com.pyh.springcloud.dao.AccountDao;
import com.pyh.springcloud.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
@Service
@Slf4j
public class AccountServiceImpl extends ServiceImpl<AccountDao, Account> implements AccountService {
@Resource
AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("-----> StorageService库存微服务, 扣减库存 start");
accountDao.decrease(userId,money);
log.info("-----> StorageService库存微服务, 扣减库存 end");
}
}
4.5.8 controller 类
package com.pyh.springcloud.controller;
import com.pyh.springcloud.entities.CommonResult;
import com.pyh.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
@RestController
public class AccountController {
@Resource
AccountService accountService;
// 该用户账户减少指定额度
@PostMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId, money);
return new CommonResult(200, "用户账户扣减成功");
}
}
4.5.9 config 配置
MyBatisPlusConfig:
package com.pyh.springcloud.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.pyh.springcloud.dao")
public class MyBatisPlusConfig {
}
MyDataSourceConfig:
package com.pyh.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* 用 seata 数据源进行代理
*/
//@Deprecated
@Configuration //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {
@Value("${mybatis-plus.mapper-locations}")
private String mapperLocations;
// @ConfigurationProperties("spring.datasource")
@ConfigurationProperties(prefix = "spring.datasource") // 与上面相同
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 官网信息:
* Register DataSourceProxy bean by yourself is discouraged since 1.3.
* If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
* just register and use Datasource bean in your old way.
* @param dataSource
* @return
* @throws Exception
*/
// seata datasource proxy
// @Bean
// public DataSourceProxy dataSourceProxy(DataSource dataSource) {
// return new DataSourceProxy(dataSource);
// }
//代理mybaits-plus
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean;
}
//代理mybatis / mybatis-config
/* @Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}*/
}
4.5.10 主启动类
package com.pyh.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMain2003.class,args);
}
}
4.6 测试
4.6.1 正常情况使用postman:
http://localhost:2001/order/create?orderId=22222222&userId=5555&productId=9999&money=100&count=2
返回:
{
"code": 200,
"message": "订单创建成功",
"data": null
}
4.6.2 异常情况:
为openfeign设置超时弹出异常,用其来测试
三个工程 yaml 添加如下配置
feign:
client:
config:
default: # feign 全局日志
# cloud-payment-service: #也可以指定某个 feign 服务名称
connectTimeout: 5000
readTimeout: 5000
loggerLevel: FULL # 日志级别 FULL / headers / basic / none 默认
# fiegnName: # fiegnName服务请求的配置,优先defalut配置。
# loggerLevel: FULL # 日志级别 FULL / headers / basic / none 默认
# connectTimeout: 5000 # 链接超时时间
# readTimeout: 5000 # 请求
# errorDecoder: com.example.SimpleErrorDecoder #异常处理
# retryer: com.example.SimpleRetryer # 重试策略
# defaultQueryParameters: # 默认参数条件
# query: queryValue
# defaultRequestHeaders: # 默认默认header
# header: headerValue
# requestInterceptors: # 默认拦截器
# - com.example.FooRequestInterceptor
# - com.example.BarRequestInterceptor
# decode404: false #404响应 true-直接返回,false-抛出异常
# encoder: com.example.SimpleEncoder #传输编码
# decoder: com.example.SimpleDecoder #传输解码
# contract: com.example.SimpleContract #传输协议
4.6.3 超时异常,没加@GlobalTransactional
//AccountServiceImpl添加超时
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("-----> StorageService库存微服务, 扣减库存 start");
// 模拟超时异常,全局事务回滚
accountDao.decrease(userId,money);
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("-----> StorageService库存微服务, 扣减库存 end");
}
//当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1
//而且由于feign的重试机制,账户余额还有可能被多次扣减
4.6.4 超时异常,添加@GlobalTransactional
// OrderServiceImpl.java 添加超时
// 发生异常则回滚
@GlobalTransactional(name = "svs-create-order", rollbackFor = Exception.class)
public void create(Order order) {
xxxx
}
-----------下单失败后数据库数据并没有任何改变
5. 总结
再看TC/TM/RM三大组件
5.1 分布式事务的执行流程:
1. TM开启分布式事务(TM向TC注册全局事务记录)
2. 换业务场景,编排数据库,服务等事务内资源(RM向TC汇报资源准备状态)
3. TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
4. TC汇总事务信息,决定分布式事务是提交还是回滚
5. TC通知所有RM提交/回滚资源,事务二阶段结束。
5.2 AT模式如何做到对业务的无侵入
5.2.1 seata 是什么
5.2.2 一阶段加载
5.2.3 二阶段提交
5.2.4 二阶段回滚
5.3 补充