- 声明:原文作者:csdn:yuan_404
文章目录
1 . 分布式事务的问题
- 在分布式之前
是一台电脑上包含所有的东西 —— 所有的数据、程序所有的内容 ……
- 慢慢向分布式演变
从 1 对 1 (一个程序对应一个数据库)
到 1 对 N (分库,一个程序对应多个数据库)
再 N 对 N (分布式微服务,多个微服务对应多个数据库)
- 分布式之后
举例:
- 单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。
- 此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
- 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
2 . Seata 简介
-
官网地址 :http://seata.io/zh-cn/
-
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
-
一个 ID + 三个组件
- Transaction ID XID :全局唯一的事务ID
- Transaction Coordinator(TC) :事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
- Transaction Manager™ :控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
- Resource Manager(RM) :控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;
- 分布式事务处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
3 . Seata-Server 安装
-
将 seata-server 解压到指定目录
-
修改 conf 目录下的 file.conf 文件
先对配置文件进行备份
-
在 Mysql 中新建一个数据库 :seata
-
初始化数据库
运行 conf 目录下的 db_store.sql 文件
如果没有,请看,SQL 脚本文件地址(选择自己的数据库) :https://github.com/seata/seata/tree/develop/script/server/db
复制下来去 MySQL 执行 -
修改 conf 目录下的 registry.conf 文件
-
启动 Nacos
-
启动 seata-server
双击 seata\bin\seata-server.bat
4 . 数据库环境搭建
- 下面就按照该框架图进行设计
这里我们会创建三个服务 —— 一个订单服务,一个库存服务,一个账户服务。
- 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
- 再通过远程调用账户服务来扣减用户账户里面的余额,
- 最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
-
启动 Nacos 、Seata
-
创建业务数据库
seata_order: 存储订单的数据库
seata_storage:存储库存的数据库
seata_account: 存储账户信息的数据库建表SQL :
CREATE DATABASE seata_order; CREATE DATABASE seata_storage; CREATE DATABASE seata_account;
-
seata_order 库下建 t_order 表
CREATE TABLE t_order( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `count` INT(11) DEFAULT NULL COMMENT '数量', `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额', `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中; 1:已完结' ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; SELECT * FROM t_order;
-
seata_storage 库下建 t_storage 表
CREATE TABLE t_storage( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `total` INT(11) DEFAULT NULL COMMENT '总库存', `used` INT(11) DEFAULT NULL COMMENT '已用库存', `residue` INT(11) DEFAULT NULL COMMENT '剩余库存' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`) VALUES('1','1','100','0','100'); SELECT * FROM t_storage;
-
seata_account 库下建 t_account 表
CREATE TABLE t_account( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id', `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度', `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额', `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000') SELECT * FROM t_account;
-
建立回滚日志表
找到 Seat 中回滚日志建表 SQL 脚本 :\seata\conf\db_undo_log.sql
三个数据库都需要执行该脚本
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;
5 . 代码环境搭建
5.1 订单模块
- 项目目录:
-
新建模块 :seata-order-service-2001
-
修改 POM
<dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <artifactId>seata-all</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.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> <version>5.1.37</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</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> </dependencies>
-
编写 YML
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: #自定义事务组名称需要与seata-server中的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 username: root password: 123456 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.springcloud.pojo
-
编写 file.conf 文件
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { vgroup_mapping.fsp_tx_group = "default" default.grouplist = "127.0.0.1:8091" enableDegrade = false disable = false max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" disableGlobalTransaction = false } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "123456" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }
-
编写 registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } 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" } }
-
实体类
-
CommonResult
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }
-
Order
@Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; /*用户id*/ private Long userId; /*产品id*/ private Long productId; /*数量*/ private Integer count; /*金额*/ private BigDecimal money; /*订单状态:0:创建中; 1:已完结*/ private Integer status; }
- Mapper / Dao 层实现
-
TOrderMapper
@Mapper public interface TOrderMapper { /** * 查询所有记录 * * @return 返回集合,没有返回空List */ List<Order> listAll(); /** * 新增,插入所有字段 * * @param order 新增的记录 * @return 返回影响行数 */ int insert(Order order); /** * 修改 */ int update(@Param("id") Long id, @Param("status") Integer status); }
-
TOrderMapper.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.demo.springcloud.mapper.TOrderMapper"> <resultMap id="BaseResultMap" type="Order"> <result column="id" property="id" 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> <!-- 表字段 --> <sql id="baseColumns"> t.id , t.user_id , t.product_id , t.count , t.money , t.status </sql> <!-- 查询全部 --> <select id="listAll" resultMap="BaseResultMap"> SELECT <include refid="baseColumns"/> FROM t_order t </select> <!-- 插入不为NULL的字段 --> <insert id="insert" parameterType="Order" keyProperty="id" keyColumn="id" useGeneratedKeys="true" > INSERT INTO t_order <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="userId != null"> user_id, </if> <if test="productId != null"> product_id, </if> <if test="count != null"> count, </if> <if test="money != null"> money, </if> status </trim> <trim prefix="VALUES (" suffix=")" suffixOverrides=","> <if test="id != null"> #{id}, </if> <if test="userId != null"> #{userId}, </if> <if test="productId != null"> #{productId}, </if> <if test="count != null"> #{count}, </if> <if test="money != null"> #{money}, </if> 0 </trim> </insert> <!-- 更新不为NULL的字段 --> <update id="update" > UPDATE t_order <set> <if test="status != null"> status=#{status}, </if> </set> WHERE id = #{id} </update> </mapper>
- Service 接口及实现类
-
TOrderService
@Service public interface TOrderService { /** * 查询所有记录 * * @return 返回集合,没有返回空List */ public List<Order> listAll(); public int insertTest(Order order); /** * 新增,插入所有字段 * * @param order 新增的记录 * @return 返回影响行数 */ public int insert(Order order); /** * 修改 */ int update(Long id,Integer status); }
-
TStorageService
// 配置需要调用的微服务 @FeignClient(value = "seata-storage-service") public interface TStorageService { @GetMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId")Long productId, @RequestParam("count")Integer count); }
-
TAccountService
// 配置需要调用的微服务 @FeignClient(value = "seata-account-service") public interface TAccountService { @GetMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId")Long userId, @RequestParam("money") BigDecimal money); }
-
TOrderServiceImpl
@Service @Slf4j public class TOrderServiceImpl implements TOrderService { @Autowired private TOrderMapper tOrderMapper; @Autowired private TAccountService accountService; @Autowired private TStorageService storageService; /** * 查询所有记录 * * @return 返回集合,没有返回空List */ @Override public List<Order> listAll() { return tOrderMapper.listAll(); } /** * 用于测试本模块的是否搭建成功 * * @param order * @return */ @Override public int insertTest(Order order) { return tOrderMapper.insert(order); } /** * 新增,插入所有字段 * * @param order 新增的记录 * @return 返回影响行数 */ @Override public int insert(Order order) { System.out.println(order.toString()); log.info("--->>>> 开始新建订单 <<<< "); int ordering = tOrderMapper.insert(order); log.info("--->>>> 订单微服务开始调用 库存,做扣减 <<<< "); CommonResult storageResult = storageService.decrease(order.getProductId(), order.getCount()); Integer storage = (Integer) storageResult.getData(); log.info("---<<<< 库存 扣减完成 >>>> "); log.info("--->>>> 订单微服务开始调用 账户,做扣减 <<<< "); CommonResult accountResult = accountService.decrease(order.getUserId(), order.getMoney()); Integer account = (Integer) accountResult.getData(); log.info("---<<<< 账户 扣减完成 >>>> "); log.info("--->>>> 修改订单状态 <<<< "); int update = tOrderMapper.update(order.getId(), 1); log.info("---<<<< 状态 修改完成 >>>> "); if (ordering > 0 && storage > 0 && account > 0 && update > 0){ log.info("---<<<< 订单生成成功 (*^_^*) >>>> "); return 1; } return 0; } /** * 修改 */ @Override public int update(Long id, Integer status) { return tOrderMapper.update(id, status); } }
-
Controller:TOrderController
@RestController public class TOrderController { @Autowired private TOrderService tOrderService; /** * 查询所有记录 * * @return 返回集合,没有返回空List */ @GetMapping("/list") public CommonResult getAll(){ return new CommonResult(200, tOrderService.listAll().toString()); } /** * 用于测试本模块的是否搭建成功 * * @param order * @return */ @GetMapping("/test") public CommonResult insertTest(Order order) { tOrderService.insertTest(order); return new CommonResult(200, tOrderService.listAll().toString()); } @GetMapping("/order") public CommonResult order(Order order) { int insert = tOrderService.insert(order); if (insert > 0){ return new CommonResult(200, "订单创建完成"); } return new CommonResult(444, "订单创建失败"); } }
-
配置数据源,替换掉 SpringBoot 默认的,换上 Seata 的
-
MyBatisConfig
@Configuration @MapperScan({"com.demo.springcloud.mapper"}) public class MyBatisConfig {}
-
DataSourceProxyConfig
@Configuration public class DataSourceProxyConfig { @Autowired private Environment env; @Value("${mybatis.mapper-locations}") private String mapperLocations; static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } /** * 用于配置 Mybatis 的别名 * @param typeAliasesPackage * @return */ public static String setTypeAliasesPackage(String typeAliasesPackage) { ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); List<String> allResult = new ArrayList<String>(); try { for (String aliasesPackage : typeAliasesPackage.split(",")) { List<String> result = new ArrayList<String>(); aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; Resource[] resources = resolver.getResources(aliasesPackage); if (resources != null && resources.length > 0) { MetadataReader metadataReader = null; for (Resource resource : resources) { if (resource.isReadable()) { metadataReader = metadataReaderFactory.getMetadataReader(resource); try { result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } } if (result.size() > 0) { HashSet<String> hashResult = new HashSet<String>(result); allResult.addAll(hashResult); } } if (allResult.size() > 0) { typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); } else { throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); } } catch (IOException e) { e.printStackTrace(); } return typeAliasesPackage; } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { String typeAliasesPackage = env.getProperty("mybatis.type-aliases-package"); typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
- 主启动类
```java
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动创建的配置
public class SeataOrderMainApp2001{
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
```
- 启动测试
访问 :[http://localhost:2001/test?id=1&userId=1&productId=1&count=22&money=66](http://localhost:2001/test?id=1&userId=1&productId=1&count=22&money=66)
![](https://img-blog.csdnimg.cn/20201224144926209.png)
5.2 库存模块
-
新建模块 :seata-order-service-2002
-
修改 POM
和 2001 的 POM 依赖一样
除了 —— 将 spring-boot-starter-actuator 去掉
-
编写 YML
和 2001 模块一样,除了端口号和服务名
-
配置 file.conf 、registry.conf
和 2001 模块的一样
-
实体类
-
CommonResult
和 2001 模块的一样 -
Storage
@Data public class Storage { private Long id; // 产品id private Long productId; //总库存 private Integer total; //已用库存 private Integer used; //剩余库存 private Integer residue; }
- Mapper / Dao
-
StorageMapper
@Mapper public interface StorageMapper { //扣减库存信息 int decrease(@Param("productId") Long productId, @Param("count") Integer count); }
-
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.demo.springcloud.mapper.StorageMapper"> <resultMap id="BaseResultMap" type="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>
- Service 接口及实现类
-
StorageService
@Service public interface StorageService { // 扣减库存 int decrease(Long productId, Integer count); }
-
StorageServiceImpl
@Service @Slf4j public class StorageServiceImpl implements StorageService { @Resource private StorageMapper storageMapper; // 扣减库存 @Override public int decrease(Long productId, Integer count) { log.info("--->>>> storage-service中扣减库存开始 <<<< "); int decrease = storageMapper.decrease(productId, count); log.info("---<<<< storage-service中扣减库存结束 >>>> "); return decrease; } }
- Controller
-
StorageController
@RestController @Slf4j public class StorageController { @Autowired private StorageService storageService; //扣减库存 @RequestMapping("/storage/decrease") public CommonResult decrease(Long productId, Integer count) { int decrease = storageService.decrease(productId, count); log.info("------ storage-decrease-影响行数 :"+ decrease); return new CommonResult(200,"扣减库存成功!", decrease); } }
-
数据源配置,改用 Seata 的
和 2001 模块一样
-
主启动类
-
SeataStorageServiceApplication2002
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient @EnableFeignClients public class SeataStorageServiceApplication2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageServiceApplication2002.class, args); } }
- 启动项目测试
访问 :[http://localhost:2002/storage/decrease?productId=1&count=1](http://localhost:2002/storage/decrease?productId=1&count=1)
![](https://img-blog.csdnimg.cn/20201224151753614.png)
![](https://img-blog.csdnimg.cn/20201224152031918.png)
![](https://img-blog.csdnimg.cn/20201224152110564.png)
5.3 账户模块
-
新建模块 :seata-order-service-2003
-
修改 POM
和 2002 模块一样
-
编写 YML
和 2002 模块一样,除了端口号和服务名
-
配置 file.conf 、registry.conf
和 2002 模块一样
-
实体类
-
CommonResult,和 2002 模块一样
-
Account
@Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; /** 用户id */ private Long userId; /** 总额度 */ private BigDecimal total; /** 已用额度 */ private BigDecimal used; /** 剩余额度 */ private BigDecimal residue; }
-
Mapper : AccountMapper
@Mapper public interface AccountMapper { /** 扣减账户余额 */ int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }
-
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.demo.springcloud.mapper.AccountMapper"> <resultMap id="BaseResultMap" type="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 residue = residue - #{money},used = used + #{money} WHERE user_id = #{userId}; </update> </mapper>
- Service 接口及实现类
-
AccountService
@Service public interface AccountService { /** 扣减账户余额 */ int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
-
AccountServiceImpl
@Service @Slf4j public class AccountServiceImpl implements AccountService { @Resource private AccountMapper accountMapper; /** 扣减账户余额 */ @Override public int decrease(Long userId, BigDecimal money) { log.info("--->>>> account-service中扣减账户余额开始 <<<< "); int decrease = accountMapper.decrease(userId, money); log.info("---<<<< account-service中扣减账户余额结束 >>>> "); return decrease; } }
-
Controller :AccountController
@RestController @Slf4j public class AccountController { @Resource AccountService accountService; /** 扣减账户余额 */ @RequestMapping("/account/decrease") public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){ int decrease = accountService.decrease(userId, money); log.info("------ account-decrease-影响行数 :"+ decrease); return new CommonResult(200,"扣减账户余额成功!", decrease); } }
-
数据源配置,改用 Seata 的
和 2002 模块一样
-
主启动类 :SeataAccountMainApp2003
```java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
```
- 启动测试
访问 :[http://localhost:2003/account/decrease?userId=1&money=11](http://localhost:2003/account/decrease?userId=1&money=11)
![](https://img-blog.csdnimg.cn/20201224154435637.png)
![](https://img-blog.csdnimg.cn/20201224154451206.png)
![](https://img-blog.csdnimg.cn/20201224154517452.png)
6 . 整合 Seata 测试
- 上面已经把环境都搭建完成了,下面就是整合 Seata 进行测试使用了
- 下面的测试斗志针对 2001 模块的
- 测试之前,最好先把数据恢复,这样等等的效果比较直观
-
在 AccountServiceImpl 中加上如下代码
// 暂停几秒,模拟超时异常 try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
-
重启 2003 项目
-
访问 :http://localhost:2001/order?id=2&userId=1&productId=1&count=22&money=66
-
错误情况:
- 当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1
- 而且由于 feign 的重试机制,账户余额还有可能被多次扣减,(Account 就出现了重复扣减的情况)
-
在 2001 中需要进行事务的方法上添加 @GlobalTransactional
@GlobalTransactional(name = "fps-create-order", rollbackFor = Exception.class)
-
重启 2001 项目再测试
-
访问 :http://localhost:2001/order?id=3&userId=1&productId=1&count=22&money=66
-
发生了和刚才一样的异常,查看数据库