本案例的操作是在第1篇关于seta博客的基础上,后续进行操作的。
案例逻辑:这里有3个服务:一个订单服务,一个库存服务,一个账户服务
1.当用户下单时,会在订单服务中创建一个订单,
2.然后通过远程调用库存服务来扣减下单商品的库存,
3.再通过远程调用账户服务来扣减用户账户里面的余额,
4.最后在订单服务中修改订单状态为已完成。
1.seata_order 存储订单数据库
CREATE DATABASE seata_order;
USE seata_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 CHARSET=utf8;
2.seata_storage 存储库存的数据库
CREATE DATABASE seata_storage;
USE seata_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=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
3.seata_account:存储账户信息的数据库
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,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=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
4.undo.log表
在3个库中分别建立对应的回滚日志表undo_log:为各个微服务模块的数据库添加事务回滚表 undo_log ,切记用到的微服务都要创建这张表。
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_logdrop table if exists `undo_log`;
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.最后,查看所建立的表
1.pom文件
org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2 mysql mysql-connector-java org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.apache.commons commons-lang3 3.4 com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-joda 2.9.6 com.fasterxml.jackson.module jackson-module-parameter-names com.github.pagehelper pagehelper-spring-boot-starter 1.2.5 com.alibaba druid-spring-boot-starter 1.1.9 org.projectlombok lombok true
2.application配置文件
# nacos配置
server:port: 6001spring:datasource:name: mysql_testtype: com.alibaba.druid.pool.DruidDataSource#druid相关配置druid:#监控统计拦截的filtersfilters: statdriver-class-name: com.mysql.jdbc.Driver#基本属性url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=trueusername: rootpassword: cloudiip#配置初始化大小/最小/最大initial-size: 1min-idle: 1max-active: 20#获取连接等待超时时间max-wait: 60000#间隔多久进行一次检测,检测需要关闭的空闲连接time-between-eviction-runs-millis: 60000#一个连接在池中最小生存的时间min-evictable-idle-time-millis: 300000validation-query: SELECT 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: false#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为falsepool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 20application:name: seata-order-servercloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址#config:# server-addr: localhost:8848 #Nacos作为配置中心地址#file-extension: yaml #指定yaml格式的配置#group: DEV_GROUP_ljf#namespace: 05573840-fcf3-472d-a64a-c66b4fe878f4alibaba:seata: #设置事务分支 默认就是 my_test_tx_grouptx-service-group: default_tx_group
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.ljf.mscloud.modelconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#pagehelper
pagehelper:helperDialect: mysqlreasonable: truesupportMethodsArguments: trueparams: count=countSqlreturnPageInfo: check
seata:enabled: trueenable-auto-data-source-proxy: truetx-service-group: default_tx_group #此处与上面config.txt中的vgroupMapping一致registry:type: nacosnacos:application: seata-server #服务name要和nacos中保持一致 默认就是seata-serverserver-addr: 127.0.0.1:8848 #根据自己情况调整username: nacos #根据自己情况调整password: nacos #根据自己情况调整cluster: defaultgroup: DEV_GROUP_ljfnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整config:type: nacosnacos:server-addr: 127.0.0.1:8848 #根据自己情况调整group: DEV_GROUP_ljfusername: nacos #根据自己情况调整password: nacos #根据自己情况调整dataId: seata-server.propertiesnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整service:vgroup-mapping:default_tx_group: default
feign:hystrix:enabled: false
3.controller
@RestController
public class OrderController {@Resourceprivate OrderService orderService;@GetMapping("/order/create")public CommonResult create(Order order){orderService.create(order);return new CommonResult(200,"订单创建成功");}}
4.service
1.接口
public interface OrderService {void create(Order order);
}
2.实现类
package com.ljf.mscloud.service.impl;import com.ljf.mscloud.dao.OrderDao;
import com.ljf.mscloud.model.Order;
import com.ljf.mscloud.service.AccountService;
import com.ljf.mscloud.service.OrderService;
import com.ljf.mscloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;/*** @ClassName: OrderServiceImpl* @Description: TODO* @Author: admin* @Date: 2023/03/01 19:05:06 * @Version: V1.0**/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderDao orderDao;@Resourceprivate StorageService storageService;@Resourceprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:下订单->扣库存->减余额->改状态*/@Override// @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)public void create(Order order){log.info("----->开始新建订单");//1 新建订单orderDao.create(order);//2 扣减库存log.info("----->订单微服务开始调用库存,做扣减Count");storageService.decreaseStorage(order.getProductId(),order.getCount());log.info("----->订单微服务开始调用库存,做扣减end");//3 扣减账户log.info("----->订单微服务开始调用账户,做扣减Money");accountService.decreaseAccount(order.getUserId(),order.getMoney());log.info("----->订单微服务开始调用账户,做扣减end");//4 修改订单状态,从零到1,1代表已经完成log.info("----->修改订单状态开始");orderDao.update(order.getUserId(),0);log.info("----->修改订单状态结束");log.info("----->下订单结束了,O(∩_∩)O哈哈~");}
}
3.StorageFeginclient:
@FeignClient(value = "seata-storage-server")
public interface StorageService
{@PostMapping(value = "/storage/decrease")CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
4.AccountFeginClient
@FeignClient(value = "seata-account-server")
public interface AccountService
{@PostMapping(value = "/account/decrease")CommonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
5.model
1.order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;private Integer status; //订单状态:0:创建中;1:已完结
}
2.commonresult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult
{private Integer code;private String message;private T data;public CommonResult(Integer code, String message){this(code,message,null);}
}
6.mapper
1.dao
@Mapper
public interface OrderDao {//1 新建订单void create(Order order);//2 修改订单状态,从零改为1void update(@Param("userId") Long userId, @Param("status") Integer status);
}
2.mapper
insert into t_order (id,user_id,product_id,count,money,status)values (null,#{userId},#{productId},#{count},#{money},0); update t_order set status = 1where user_id=#{userId} and status = #{status};
7.启动类
@EnableDiscoveryClient
@SpringBootApplication
@EnableAutoDataSourceProxy
@EnableFeignClients
public class App
{public static void main( String[] args ){SpringApplication.run(App.class, args);System.out.println("服务启动成功!!!!!!!");}
}
1.pom文件
org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2 mysql mysql-connector-java org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.apache.commons commons-lang3 3.4 com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-joda 2.9.6 com.fasterxml.jackson.module jackson-module-parameter-names com.github.pagehelper pagehelper-spring-boot-starter 1.2.5 com.alibaba druid-spring-boot-starter 1.1.9 org.projectlombok lombok true
2.application配置文件
# nacos配置
server:port: 6002spring:datasource:name: mysql_testtype: com.alibaba.druid.pool.DruidDataSource#druid相关配置druid:#监控统计拦截的filtersfilters: statdriver-class-name: com.mysql.jdbc.Driver#基本属性url: jdbc:mysql://127.0.0.1:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=trueusername: rootpassword: cloudiip#配置初始化大小/最小/最大initial-size: 1min-idle: 1max-active: 20#获取连接等待超时时间max-wait: 60000#间隔多久进行一次检测,检测需要关闭的空闲连接time-between-eviction-runs-millis: 60000#一个连接在池中最小生存的时间min-evictable-idle-time-millis: 300000validation-query: SELECT 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: false#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为falsepool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 20application:name: seata-storage-servercloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址#config:# server-addr: localhost:8848 #Nacos作为配置中心地址#file-extension: yaml #指定yaml格式的配置#group: DEV_GROUP_ljf#namespace: 05573840-fcf3-472d-a64a-c66b4fe878f4alibaba:seata: #设置事务分支 默认就是 my_test_tx_grouptx-service-group: default_tx_group
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.ljf.mscloud.modelconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#pagehelper
pagehelper:helperDialect: mysqlreasonable: truesupportMethodsArguments: trueparams: count=countSqlreturnPageInfo: check
seata:enabled: trueenable-auto-data-source-proxy: truetx-service-group: default_tx_group #此处与上面config.txt中的vgroupMapping一致registry:type: nacosnacos:application: seata-server #服务name要和nacos中保持一致 默认就是seata-serverserver-addr: 127.0.0.1:8848 #根据自己情况调整username: nacos #根据自己情况调整password: nacos #根据自己情况调整cluster: defaultgroup: DEV_GROUP_ljfnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整config:type: nacosnacos:server-addr: 127.0.0.1:8848 #根据自己情况调整group: DEV_GROUP_ljfusername: nacos #根据自己情况调整password: nacos #根据自己情况调整dataId: seata-server.propertiesnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整service:vgroup-mapping:default_tx_group: default
feign:hystrix:enabled: false
3.controller
@RestController
public class StorageController {@Autowiredprivate StorageService storageService;/*** 扣减库存*/@RequestMapping("/storage/decrease")public CommonResult decrease(Long productId, Integer count) {storageService.decrease(productId, count);return new CommonResult(200,"扣减库存成功!");}
}
4.service
public interface StorageService {/*** 扣减库存*/void decrease(Long productId, Integer count);
}
5.dao
@Service
public class StorageServiceImpl implements StorageService {private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);@Resourceprivate StorageDao storageDao;/*** 扣减库存*/@Overridepublic void decrease(Long productId, Integer count) {LOGGER.info("------->storage-service中扣减库存开始");storageDao.decrease(productId,count);LOGGER.info("------->storage-service中扣减库存结束");}
}
UPDATEt_storageSETused = used + #{count},residue = residue - #{count}WHEREproduct_id = #{productId}
6.model
1.Storage
@Data
public class Storage {private Long id;/*** 产品id*/private Long productId;/*** 总库存*/private Integer total;/*** 已用库存*/private Integer used;/*** 剩余库存*/private Integer residue;
}
2.commonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult
{private Integer code;private String message;private T data;public CommonResult(Integer code, String message){this(code,message,null);}
}
7.启动类
@EnableDiscoveryClient
@SpringBootApplication
@EnableAutoDataSourceProxy
@EnableFeignClients
public class App
{public static void main( String[] args ){SpringApplication.run(App.class, args);System.out.println("服务启动成功!!!!!!!");}
}
1.pom文件
org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2 mysql mysql-connector-java org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.apache.commons commons-lang3 3.4 com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-joda 2.9.6 com.fasterxml.jackson.module jackson-module-parameter-names com.github.pagehelper pagehelper-spring-boot-starter 1.2.5 com.alibaba druid-spring-boot-starter 1.1.9 org.projectlombok lombok true
2.application配置文件
# nacos配置
server:port: 6003spring:datasource:name: mysql_testtype: com.alibaba.druid.pool.DruidDataSource#druid相关配置druid:#监控统计拦截的filtersfilters: statdriver-class-name: com.mysql.jdbc.Driver#基本属性url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=trueusername: rootpassword: cloudiip#配置初始化大小/最小/最大initial-size: 1min-idle: 1max-active: 20#获取连接等待超时时间max-wait: 60000#间隔多久进行一次检测,检测需要关闭的空闲连接time-between-eviction-runs-millis: 60000#一个连接在池中最小生存的时间min-evictable-idle-time-millis: 300000validation-query: SELECT 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: false#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为falsepool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 20application:name: seata-account-servercloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址#config:# server-addr: localhost:8848 #Nacos作为配置中心地址#file-extension: yaml #指定yaml格式的配置#group: DEV_GROUP_ljf#namespace: 05573840-fcf3-472d-a64a-c66b4fe878f4alibaba:seata: #设置事务分支 默认就是 my_test_tx_grouptx-service-group: default_tx_group
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.ljf.mscloud.modelconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#pagehelper
pagehelper:helperDialect: mysqlreasonable: truesupportMethodsArguments: trueparams: count=countSqlreturnPageInfo: check
seata:enabled: trueenable-auto-data-source-proxy: truetx-service-group: default_tx_group #此处与上面config.txt中的vgroupMapping一致registry:type: nacosnacos:application: seata-server #服务name要和nacos中保持一致 默认就是seata-serverserver-addr: 127.0.0.1:8848 #根据自己情况调整username: nacos #根据自己情况调整password: nacos #根据自己情况调整cluster: defaultgroup: DEV_GROUP_ljfnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整config:type: nacosnacos:server-addr: 127.0.0.1:8848 #根据自己情况调整group: DEV_GROUP_ljfusername: nacos #根据自己情况调整password: nacos #根据自己情况调整dataId: seata-server.propertiesnamespace: 05573840-fcf3-472d-a64a-c66b4fe878f4 #根据自己情况调整service:vgroup-mapping:default_tx_group: default
feign:hystrix:enabled: false
3.controller
@RestController
public class AccountController {@ResourceAccountService accountService;/*** 扣减账户余额*/@RequestMapping("/account/decrease")public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){accountService.decrease(userId,money);return new CommonResult(200,"扣减账户余额成功!");}
}
4.service
public interface AccountService {/*** 扣减账户余额* @param userId 用户id* @param money 金额*/void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
/*** 账户业务实现类* Created by zzyy on 2019/11/11.*/
@Service
public class AccountServiceImpl implements AccountService {private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);@ResourceAccountDao accountDao;/*** 扣减账户余额*/@Overridepublic void decrease(Long userId, BigDecimal money) {LOGGER.info("------->account-service中扣减账户余额开始");//模拟超时异常,全局事务回滚//暂停几秒钟线程// try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }accountDao.decrease(userId,money);LOGGER.info("------->account-service中扣减账户余额结束");}
}
5..dao
@Mapper
public interface AccountDao {/*** 扣减账户余额*/void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
6.model
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private Long id;/*** 用户id*/private Long userId;/*** 总额度*/private BigDecimal total;/*** 已用额度*/private BigDecimal used;/*** 剩余额度*/private BigDecimal residue;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult
{private Integer code;private String message;private T data;public CommonResult(Integer code, String message){this(code,message,null);}
}
7.启动类
@EnableDiscoveryClient
@SpringBootApplication
@EnableAutoDataSourceProxy
@EnableFeignClients
public class App
{public static void main( String[] args ){SpringApplication.run(App.class, args);System.out.println("服务启动成功!!!!!!!");}
}
需求先启动nacos,seata这个两个服务,然后启动工程seata-order,seata-storage,seata-account这3个工程。
1.查看3张表
2请求访问后
http://localhost:6001/order/create?userId=001&productId=001&count=1&money=400&status=0
访问后,再次观察3张表,均按照:下订单->扣库存->减余额->改状态 流程执行正确无误。
1.在账户模块中,模拟超时,延迟20s,再次启动账户模块
2.再次访问,页面报错
3.查看这3张表,发现库存表实现了减库存,账户实现了减余额操作,新订单表中也新增了1条订单数据,但是订单状态还是0,订单状态没有完成改变。
4. 原因在于:调用账户模块超时,导致订单模块发生异常,无法进行最后一步修改订单状态逻辑的执行。
1.在订单模块中,执行方法新增 @GlobalTransaction注解,重启order服务
2.多次刷新后,虽然前端还是报错,只是显示问题不管
http://localhost:6001/order/create?userId=001&productId=001&count=1&money=400&status=0
3.可以查看订单表,库存表,账户表均没有发生改变,说明添加@GlobalTransaction
已经启动作用,分布式事务已经起作用。
下一篇: 疫情送温暖的感谢句子