主项目链接:https://gitee.com/java_wxid/java_wxid
项目架构及博文总结:
- 点击:【使用Spring Boot快速构建应用】
- 点击:【使用Spring Cloud Open Feign基于动态代理动态构造请求实现与其他系统进行交互】
- 点击:【使用Spring Cloud Hystrix实现服务容错、熔断、降级、监控】
- 点击:【使用Spring Cloud Ribbon以库的方式集成到服务的消费方实现客户端负载均衡】
- 点击:【使用Spring Cloud Gateway作为API网关服务进行请求拦截、服务分发、降级、限流】
- 点击:【使用Spring Cloud Security Oauth2作为微服务统一认证中心实现用户认证和授权访问】
- 点击:【使用Spring Cloud Stream作为消息驱动用于动态的切换中间件】
- 点击:【使用Spring Cloud Skywalking基于字节码注入通过探针方式进行链路追踪、分布式追踪、性能指标分析、应用和服务依赖分析】
- 点击:【使用Spring Cloud Alibaba Nacos实现服务注册/发现/续约/剔除/下线、心跳检测、服务配置管理、基于长轮训机制实现配置动态变更】
- 点击:【使用Spring Cloud Alibaba Seata作为对项目代码无入侵的分布式事务解决方案】
- 点击:【使用Spring Cloud Alibaba Sentinel实现高可用流量防护】
- 点击:【使用Apache ShardingSphere作为关系型数据库中间件实现分库分表、读写分离】
- 点击:【使用Apache Mybatis作为持久层框架用于定制化SQL、存储过程以及高级映射】
- 点击:【使用Redis作为高性能分布式缓存数据库】
- 点击:【使用ElasticSearch全文搜索】
- 点击:【使用MongoDB非关系型数据库】
- 点击:【使用xxl-job作为分布式任务调度平台】
- 点击:【使用Elasticsearch + Logstash + Kibana作为日志收集系统】
- 点击:【使用Apifox作为API文档、API调试、API Mock、API自动化测试】
- 点击:【使用Apache Spark作为基于内存计算的大数据分析引擎用于批处理、交互式查询】
- 点击:【使用ETL工具将数据源抽取到HDFS作为高可靠、高吞吐量的分布式文件系统存储,通过Hive清洗、处理和计算原始数据,Hive清洗处理后的结果,将存入Hbase,海量数据随机查询场景从HBase查询数据】
- 点击:【使用领域驱动DDD设计和设计模式进行开发】
- 点击:【使用Netty基于Java NIO封装的高性能的网络通信框架】
- 点击:【使用k8s、docker、docker-compose、宝塔面板进行环境搭建和部署】
- 点击:【使用Vue渐进式JavaScript框架作为适用场景丰富的Web前端框架】
- 点击:【分享人才筛选、工作分配、高效办公、项目推动等团队管理经验】
项目模块:
前期规划,实现部分
java_wxid ├── demo // 演示模块 │ └── 模块名称:apache-mybatis-demo模块 //Apache Mybatis集成(已实现并有博文总结) │ └── 模块名称:apache-shardingsphere-demo模块 //Apache ShardingSphere集成(已实现并有博文总结) │ └── 模块名称:design-demo模块 //设计模式实战落地(已实现并有博文总结) │ └── 模块名称:elasticsearch-demo模块 //ElasticSearch集成(已实现并有博文总结) │ └── 模块名称:mongodb-demo模块 //MongoDB集成(已实现并有博文总结) │ └── 模块名称:redis-demo模块 //Redis集成(已实现并有博文总结) │ └── 模块名称:spring-boot-demo模块 //Spring Boot快速构建应用(已实现并有博文总结) │ └── 模块名称:spring-cloud-alibaba-nacos-demo模块 //Spring Cloud Alibaba Nacos集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-alibaba-seata-demo模块 //Spring Cloud Alibaba Seata集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-alibaba-sentinel-demo模块 //Spring Cloud Alibaba Sentinel集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-gateway-demo模块 //Spring Cloud Gateway集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-hystrix-demo模块 //Spring Cloud Hystrix集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-open-feign-demo模块 //Spring Cloud Open Feign集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-ribbon-demo模块 //Spring Cloud Ribbon集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-security-oauth2-demo模块 //Spring Cloud Security Oauth2集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-security-oauth2-sso-client-demo模块 //Spring Cloud Security Oauth2集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-skywalking-demo模块 //Spring Cloud Skywalking集成(已实现并有博文总结) │ └── 模块名称:spring-cloud-stream-demo模块 //Spring Cloud Stream集成(已实现并有博文总结) │ └── 模块名称:swagger-demo模块 //springfox-swagger2集成(已实现并有博文总结) │ └── 模块名称:xxl-job模块 //xxl-job集成(已实现并有博文总结) │ └── 模块名称:apache-spark-demo模块 //Apache Spark集成 │ └── 模块名称:etl-hdfs-hive-hbase-demo模块 //ETL、HDFS、Hive、Hbase集成 │ └── 模块名称:ddd-mode-demo模块 //DDD领域设计 │ └── 模块名称:netty-demo模块 //Netty集成 │ └── 模块名称:vue-demo模块 //前端vue集成 ├── document // 文档 │ └── JavaKnowledgeDocument //java知识点 │ └── java基础知识点.md │ └── mq知识点.md │ └── mysql知识点.md │ └── redis知识点.md │ └── springcould知识点.md │ └── spring知识点.md │ └── FounderDocument //创始人 │ └── 创始人.md
系列文章:快速集成各种微服务相关的技术,帮助大家可以快速集成到自己的项目中,节约开发时间。
提示:系列文章还未全部完成,后续的文章,会慢慢补充进去的。
文章目录
Seata Server部署:DB存储模式+Nacos(注册&配置中心)部署
步骤一:下载安装包
步骤二:建表(仅db模式)
步骤三:修改store.mode
步骤四:修改数据库连接
步骤五:配置Nacos
步骤六:启动Seata Server
Seata Client接入:(普通下单业务场景)
创建spring-cloud-alibaba-seata-demo项目
修改pom.xml
创建order-service微服务
修改pom.xml
创建bootstrap.yml
创建application.yml
修改OrderServiceApplication
创建FeignConfig
创建OrderController
创建AccountFeignService
创建FallbackAccountFeignServiceFactory
创建StorageFeignService
创建OrderService
创建OrderServiceImpl
创建OrderVo
创建db.sql
创建storage-service微服务
修改pom.xml
创建bootstrap.yml
创建application.yml
修改StorageServiceApplication
创建StorageController
创建StorageService
创建StorageServiceImpl
创建db.sql
创建account-service微服务
修改pom.xml
创建bootstrap.yml
创建application.yml
修改AccountServiceApplication
创建AccountController
创建AccountService
创建AccountServiceImpl
创建db.sql
创建mysql-common模块
创建MybatisConfig
创建Account
创建Order
创建OrderStatus
创建Storage
创建AccountMapper
创建OrderMapper
创建StorageMapper
创建ResultVo
开始验证seata是否正常工作
正常减库存
不正常减库存
官网:https://seata.io/zh-cn/index.html
源码: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples
Seata Server部署:DB存储模式+Nacos(注册&配置中心)部署
步骤一:下载安装包
https://github.com/seata/seata/releases
如下(示例):
步骤二:建表(仅db模式)
全局事务会话信息由3块内容构成,全局事务–>分支事务–>全局锁,对应表global_table、branch_table、lock_table
创建数据库seata,执行sql脚本,文件在script/server/db/mysql.sql(seata源码)中
如下(示例):
mysql.sql脚本如下(示例):
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid_and_branch_id` (`xid` , `branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
步骤三:修改store.mode
启动包: seata–>conf–>application.yml,修改store.mode=“db”。
步骤四:修改数据库连接
启动包: seata–>conf–>application.yml,修改store.db相关属性。
如下(示例):
步骤五:配置Nacos
将Seata Server注册到Nacos,修改conf目录下的application.yml
如下(示例):
application.yml文件如下(示例):
server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: ip:4560 kafka-appender: bootstrap-servers: ip:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: # support: nacos, consul, apollo, zk, etcd3 type: nacos nacos: server-addr: ip:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP username: nacos password: nacos ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: "" #secret-key: "" data-id: seataServer.properties registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: ip:8848 group: SEATA_GROUP namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd cluster: default username: nacos password: nacos ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: "" #secret-key: "" store: # support: file 、 db 、 redis mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://ip:3306/seata?rewriteBatchedStatements=true user: root password: root密码 min-conn: 5 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 100 max-wait: 5000 # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
使用nacos时要注意group要和seata server中的group一致,默认group是"DEFAULT_GROUP",示例中修改成了SEATA_GROUP
seataServer.properties文件的配置在/seata/script/config-center/config.txt直接复制粘贴,然后修改配置信息
文件目录如下(示例):
config.txt如下(示例):
config.txt配置如下(示例):
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html #Transport configuration, for client and server transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableTmClientBatchSendRequest=false transport.enableRmClientBatchSendRequest=true transport.enableTcServerBatchSendResponse=false transport.rpcRmRequestTimeout=30000 transport.rpcTmRequestTimeout=30000 transport.rpcTcRequestTimeout=30000 transport.threadFactory.bossThreadPrefix=NettyBoss transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler transport.threadFactory.shareBossWorker=false transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector transport.threadFactory.clientSelectorThreadSize=1 transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread transport.threadFactory.bossThreadSize=1 transport.threadFactory.workerThreadSize=default transport.shutdown.wait=3 transport.serialization=seata transport.compressor=none #Transaction routing rules configuration, only for the client service.vgroupMapping.default_tx_group=default #If you use a registry, you can ignore it service.default.grouplist=127.0.0.1:8091 service.enableDegrade=false service.disableGlobalTransaction=false #Transaction rule configuration, only for the client client.rm.asyncCommitBufferLimit=10000 client.rm.lock.retryInterval=10 client.rm.lock.retryTimes=30 client.rm.lock.retryPolicyBranchRollbackOnConflict=true client.rm.reportRetryCount=5 client.rm.tableMetaCheckEnable=true client.rm.tableMetaCheckerInterval=60000 client.rm.sqlParserType=druid client.rm.reportSuccessEnable=false client.rm.sagaBranchRegisterEnable=false client.rm.sagaJsonParser=fastjson client.rm.tccActionInterceptorOrder=-2147482648 client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.defaultGlobalTransactionTimeout=60000 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 client.tm.interceptorOrder=-2147482648 client.undo.dataValidation=true client.undo.logSerialization=jackson client.undo.onlyCareUpdateColumns=true server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 client.undo.logTable=undo_log client.undo.compress.enable=true client.undo.compress.type=zip client.undo.compress.threshold=64k #For TCC transaction mode tcc.fence.logTableName=tcc_fence_log tcc.fence.cleanPeriod=1h #Log rule configuration, for client and server log.exceptionRate=100 #Transaction storage configuration, only for the server. The file, DB, and redis configuration values are optional. store.mode=db store.lock.mode=file store.session.mode=file #Used for password encryption store.publicKey= #If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block. store.file.dir=file_store/data store.file.maxBranchSessionSize=16384 store.file.maxGlobalSessionSize=512 store.file.fileWriteBufferCacheSize=16384 store.file.flushDiskMode=async store.file.sessionReloadReadSize=100 #These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block. store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true store.db.user=username store.db.password=password store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.distributedLockTable=distributed_lock store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 #These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block. store.redis.mode=single store.redis.single.host=127.0.0.1 store.redis.single.port=6379 store.redis.sentinel.masterName= store.redis.sentinel.sentinelHosts= store.redis.maxConn=10 store.redis.minConn=1 store.redis.maxTotal=100 store.redis.database=0 store.redis.password= store.redis.queryLimit=100 #Transaction rule configuration, only for the server server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.distributedLockExpireTime=10000 server.xaerNotaRetryTimeout=60000 server.session.branchAsyncQueueSize=5000 server.session.enableBranchAsyncRemove=false server.enableParallelRequestHandle=false #Metrics configuration, only for the server metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898
nacos创建文件,粘贴配置
如下(示例):
如下(示例):
需要注意的事项
配置事务分组, 要与客户端配置的事务分组一致
(客户端properties配置:spring.cloud.alibaba.seata.tx‐service‐group=default_tx_group)
如下(示例):
步骤六:启动Seata Server
一般而言,需要修改默认端口减少黑客通过默认端口入侵服务器的可能,这里我端口修改成了9091
修改完配置之后,进行压缩,上传到服务器上,给予文件权限,进行解压,去到seata/bin目录下,启动seata-server.sh
最后使用./seata-server.sh -h 106.14.132.94 -p 8091 &启动
如下(示例):
由于新版本支持了skywalking而我这里没有配置它,所以有这个提示,这个不影响,我们可以查看日志:cat /opt/seata/logs/start.out
发现已经启动起来了
如下(示例):
检查安全组,防火墙的端口是否正常,然后通过部署的ip:seata配置的端口直接进行服务,application.yml中登录用户默认配置的是seata
如下(示例):
浏览器直接服务登录
如下(示例):
可切换成中文,如下(示例):
Seata Client接入:(普通下单业务场景)
用户下单,整个业务逻辑由三个微服务构成:
- 库存服务:对给定的商品扣除库存数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
如下(示例):
这里开多个微服务
创建spring-cloud-alibaba-seata-demo项目
项目代码:https://gitee.com/java_wxid/java_wxid/tree/master/demo/spring-cloud-alibaba-seata-demo
项目结构如下(示例):
删除src文件夹
修改pom.xml
代码如下(示例):
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>spring-cloud-alibaba-seata-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-cloud-alibaba-seata-demo</name> <description>Demo project for Spring Boot</description> <packaging>pom</packaging> <modules> <module>mysql-common</module> <module>account-service</module> <module>order-service</module> <module>storage-service</module> </modules> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version> <seata.version>1.5.1</seata.version> <mysql-jdbc.version>5.1.48</mysql-jdbc.version> <mybatis.version>2.1.1</mybatis.version> <druid.version>1.2.6</druid.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>${mysql-jdbc.version}</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>${seata.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
创建order-service微服务
修改pom.xml
代码如下(示例):
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>spring-cloud-alibaba-seata-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.example</groupId> <artifactId>order-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>order-service</name> <description>Demo project for Seata</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--nacos 注册中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-common</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--加入sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--加入actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>mysql-common</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建bootstrap.yml
代码如下(示例):
#bootstrap.yml优先级比application.yml优先级高 spring: #prefix−{spring.profile.active}.${file-extension} #nacos会根据当前环境去拼接配置名称查找相应配置文件, #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension} #获取到值:nacos-autoconfig-service-dev.yml profiles: #开发环境dev,测试环境test,生产环境prod active: dev application: #配置应用的名称,用于获取配置 name: order-service cloud: nacos: discovery: # 服务注册地址 server-addr: ip:8848 config: #nacos配置中心地址 server-addr: ip:8848 #配置中心的命名空间id namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d #配置分组,默认没有也可以 group: DEFAULT_GROUP #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties file-extension: yaml #配置自动刷新 refresh-enabled: true #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值 #prefix: nacos-autoconfig-service-${spring.profile.active} # 配置编码 encode: UTF-8 username: nacos password: nacos
创建application.yml
代码如下(示例):
server: port: 8020 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://139.224.137.74:3306/seata_at_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: ca0a997ee4770063 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true seata: application-id: ${spring.application.name} # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应 tx-service-group: default_tx_group registry: # 指定nacos作为注册中心 type: nacos nacos: application: seata-server server-addr: 106.14.132.94:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP config: # 指定nacos作为配置中心 type: nacos nacos: server-addr: 106.14.132.94:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP data-id: seataServer.properties #暴露actuator端点 management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true
修改OrderServiceApplication
代码如下(示例):
package com.example.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication(scanBasePackages = "com.example") @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }
创建FeignConfig
代码如下(示例):
package com.example.order.config; import feign.Logger; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Configuration public class FeignConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
创建OrderController
代码如下(示例):
package com.example.order.controller; import com.example.datasource.entity.Order; import com.example.datasource.vo.ResultVo; import com.example.order.service.OrderService; import com.example.order.vo.OrderVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @RestController @RequestMapping("/order") @Slf4j public class OrderController { @Autowired private OrderService orderService; @PostMapping("/createOrder") public ResultVo createOrder(@RequestBody OrderVo orderVo) throws Exception { log.info("收到下单请求,用户:{}, 商品编号:{}", orderVo.getUserId(), orderVo.getCommodityCode()); Order order = orderService.saveOrder(orderVo); return ResultVo.ok().put("order",order); } }
创建AccountFeignService
代码如下(示例):
package com.example.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Repository; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @FeignClient(name = "account-service",path = "/account") @Repository public interface AccountFeignService { @RequestMapping("/debit") Boolean debit(@RequestParam("userId") String userId,@RequestParam("money") int money); }
创建FallbackAccountFeignServiceFactory
代码如下(示例):
package com.example.order.feign; import io.seata.core.context.RootContext; import io.seata.core.exception.TransactionException; import io.seata.tm.api.GlobalTransactionContext; import org.apache.commons.lang.StringUtils; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Component @Slf4j public class FallbackAccountFeignServiceFactory implements FallbackFactory<AccountFeignService> { @Override public AccountFeignService create(Throwable throwable) { return new AccountFeignService() { @Override public Boolean debit(String userId, int money) { log.info("账户服务异常降级了"); // 解决 feign整合sentinel降级导致Seata失效的处理 此方案不可取 /*if(!StringUtils.isEmpty(RootContext.getXID())){ //通过xid获取GlobalTransaction调用rollback回滚 //可以让库存服务回滚 能解决问题吗? 绝对不能用 try { GlobalTransactionContext.reload(RootContext.getXID()).rollback(); } catch (TransactionException e) { e.printStackTrace(); } }*/ return false; } }; } }
创建StorageFeignService
代码如下(示例):
package com.example.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Repository; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @FeignClient(name="storage-service",path="/storage") @Repository public interface StorageFeignService { @RequestMapping(path = "/deduct") Boolean deduct(@RequestParam("commodityCode") String commodityCode,@RequestParam("count") Integer count); }
创建OrderService
代码如下(示例):
package com.example.order.service; import com.example.datasource.entity.Order; import com.example.order.vo.OrderVo; import io.seata.core.exception.TransactionException; public interface OrderService { /** * 保存订单 */ Order saveOrder(OrderVo orderVo) throws TransactionException; }
创建OrderServiceImpl
代码如下(示例):
package com.example.order.service.impl; import com.example.order.feign.AccountFeignService; import com.example.order.feign.StorageFeignService; import com.example.order.service.OrderService; import com.example.order.vo.OrderVo; import com.example.datasource.entity.Order; import com.example.datasource.entity.OrderStatus; import com.example.datasource.mapper.OrderMapper; import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private AccountFeignService accountFeignService; @Autowired private StorageFeignService storageFeignService; @Override //@Transactional @GlobalTransactional(name="createOrder",rollbackFor=Exception.class) public Order saveOrder(OrderVo orderVo) { log.info("=============用户下单================="); log.info("当前 XID: {}", RootContext.getXID()); // 保存订单 Order order = new Order(); order.setUserId(orderVo.getUserId()); order.setCommodityCode(orderVo.getCommodityCode()); order.setCount(orderVo.getCount()); order.setMoney(orderVo.getMoney()); order.setStatus(OrderStatus.INIT.getValue()); Integer saveOrderRecord = orderMapper.insert(order); log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败"); //扣减库存 storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount()); //扣减余额 Boolean debit= accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney()); if(!debit){ // 解决 feign整合sentinel降级导致Seata失效的处理 throw new RuntimeException("账户服务异常降级了"); } //更新订单 Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue()); log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败"); return order; } }
创建OrderVo
代码如下(示例):
package com.example.order.vo; import lombok.Data; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Data public class OrderVo { private String userId; /**商品编号**/ private String commodityCode; private Integer count; private Integer money; }
创建db.sql
代码如下(示例):
CREATE TABLE `order_tbl` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT '0', `money` int(11) DEFAULT '0', `status` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
创建storage-service微服务
项目结构如下(示例):
修改pom.xml
代码如下(示例):
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>spring-cloud-alibaba-seata-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.example</groupId> <artifactId>storage-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>storage-service</name> <description>Demo project for Seata</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--nacos 注册中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-common</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>mysql-common</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建bootstrap.yml
代码如下(示例):
#bootstrap.yml优先级比application.yml优先级高 spring: #prefix−{spring.profile.active}.${file-extension} #nacos会根据当前环境去拼接配置名称查找相应配置文件, #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension} #获取到值:nacos-autoconfig-service-dev.yml profiles: #开发环境dev,测试环境test,生产环境prod active: dev application: #配置应用的名称,用于获取配置 name: storage-service cloud: nacos: discovery: # 服务注册地址 server-addr: 106.14.132.94:8848 config: #nacos配置中心地址 server-addr: 106.14.132.94:8848 #配置中心的命名空间id namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d #配置分组,默认没有也可以 group: DEFAULT_GROUP #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties file-extension: yaml #配置自动刷新 refresh-enabled: true #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值 #prefix: nacos-autoconfig-service-${spring.profile.active} # 配置编码 encode: UTF-8 username: nacos password: nacos
创建application.yml
代码如下(示例):
server: port: 8040 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://139.224.137.74:3306/seata_at_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: ca0a997ee4770063 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true seata: application-id: ${spring.application.name} # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应 tx-service-group: default_tx_group registry: # 指定nacos作为注册中心 type: nacos nacos: application: seata-server server-addr: 106.14.132.94:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP config: # 指定nacos作为配置中心 type: nacos nacos: server-addr: 106.14.132.94:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP data-id: seataServer.properties
修改StorageServiceApplication
代码如下(示例):
package com.example.storage; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(scanBasePackages = "com.example") public class StorageServiceApplication { public static void main(String[] args) { SpringApplication.run(StorageServiceApplication.class, args); } }
创建StorageController
代码如下(示例):
package com.example.storage.controller; import com.example.storage.service.StorageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @RestController @RequestMapping("/storage") public class StorageController { @Autowired private StorageService storageService; @RequestMapping(path = "/deduct") public Boolean deduct(String commodityCode, Integer count) { // 扣减库存 storageService.deduct(commodityCode, count); return true; } }
创建StorageService
代码如下(示例):
package com.example.storage.service; public interface StorageService { /** * 扣减库存 * @param commodityCode 商品编号 * @param count 扣除数量 */ void deduct(String commodityCode, int count); }
创建StorageServiceImpl
代码如下(示例):
package com.example.storage.service.impl; import com.example.datasource.entity.Storage; import com.example.datasource.mapper.StorageMapper; import com.example.storage.service.StorageService; import io.seata.core.context.RootContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Service @Slf4j public class StorageServiceImpl implements StorageService { @Autowired private StorageMapper storageMapper; @Transactional @Override public void deduct(String commodityCode, int count){ log.info("=============扣减库存================="); log.info("当前 XID: {}", RootContext.getXID()); // 检查库存 checkStock(commodityCode,count); log.info("开始扣减 {} 库存", commodityCode); Integer record = storageMapper.reduceStorage(commodityCode,count); log.info("扣减 {} 库存结果:{}", commodityCode, record > 0 ? "操作成功" : "扣减库存失败"); } private void checkStock(String commodityCode, int count){ log.info("检查 {} 库存", commodityCode); Storage storage = storageMapper.findByCommodityCode(commodityCode); if (storage.getCount() < count) { log.warn("{} 库存不足,当前库存:{}", commodityCode, count); throw new RuntimeException("库存不足"); } } }
创建db.sql
代码如下(示例):
CREATE TABLE `storage_tbl` ( `id` int(11) NOT NULL AUTO_INCREMENT, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; insert into `storage_tbl` (`id`, `commodity_code`, `count`) values('1','1000','1000'); -- 微服务对应数据库中添加undo_log表(仅AT模式) CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
创建account-service微服务
项目结构如下(示例):
修改pom.xml
代码如下(示例):
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>spring-cloud-alibaba-seata-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.example</groupId> <artifactId>account-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>account-service</name> <description>Demo project for Seata</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--nacos 注册中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-common</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>mysql-common</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建bootstrap.yml
代码如下(示例):
#bootstrap.yml优先级比application.yml优先级高 spring: #prefix−{spring.profile.active}.${file-extension} #nacos会根据当前环境去拼接配置名称查找相应配置文件, #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension} #获取到值:nacos-autoconfig-service-dev.yml profiles: #开发环境dev,测试环境test,生产环境prod active: dev application: #配置应用的名称,用于获取配置 name: account-service cloud: nacos: discovery: # 服务注册地址 server-addr: ip:8848 config: #nacos配置中心地址 server-addr: ip:8848 #配置中心的命名空间id namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d #配置分组,默认没有也可以 group: DEFAULT_GROUP #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties file-extension: yaml #配置自动刷新 refresh-enabled: true #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值 #prefix: nacos-autoconfig-service-${spring.profile.active} # 配置编码 encode: UTF-8 username: nacos password: nacos
创建application.yml
代码如下(示例):
server: port: 8050 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://ip:3306/seata_at_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: ca0a997ee4770063 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true seata: application-id: ${spring.application.name} # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应 tx-service-group: default_tx_group registry: # 指定nacos作为注册中心 type: nacos nacos: application: seata-server server-addr: ip:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP config: # 指定nacos作为配置中心 type: nacos nacos: server-addr: ip:8848 namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd group: SEATA_GROUP data-id: seataServer.properties
修改AccountServiceApplication
代码如下(示例):
package com.example.account; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(scanBasePackages = "com.example") public class AccountServiceApplication { public static void main(String[] args) { SpringApplication.run(AccountServiceApplication.class, args); } }
创建AccountController
代码如下(示例):
package com.example.account.controller; import com.example.account.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @RestController @RequestMapping("/account") public class AccountController { @Autowired private AccountService accountService; @RequestMapping("/debit") public Boolean debit(String userId, int money) throws Exception { // 用户账户扣款 accountService.debit(userId, money); return true; } }
创建AccountService
代码如下(示例):
package com.example.account.service; public interface AccountService { /** * 用户账户扣款 * @param userId * @param money 从用户账户中扣除的金额 */ void debit(String userId, int money) ; }
创建AccountServiceImpl
代码如下(示例):
package com.example.account.service.impl; import com.example.account.service.AccountService; import com.example.datasource.entity.Account; import com.example.datasource.mapper.AccountMapper; import io.seata.core.context.RootContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Service @Slf4j public class AccountServiceImpl implements AccountService { private static final String ERROR_USER_ID = "1002"; @Autowired private AccountMapper accountMapper; /** * 扣减用户金额 * @param userId * @param money */ @Transactional @Override public void debit(String userId, int money){ log.info("=============用户账户扣款================="); log.info("当前 XID: {}", RootContext.getXID()); checkBalance(userId, money); log.info("开始扣减用户 {} 余额", userId); Integer record = accountMapper.reduceBalance(userId,money); // if (ERROR_USER_ID.equals(userId)) { // // 模拟异常 // throw new RuntimeException("account branch exception"); // } log.info("扣减用户 {} 余额结果:{}", userId, record > 0 ? "操作成功" : "扣减余额失败"); } private void checkBalance(String userId, int money){ log.info("检查用户 {} 余额", userId); Account account = accountMapper.selectByUserId(userId); if (account.getMoney() < money) { log.warn("用户 {} 余额不足,当前余额:{}", userId, account.getMoney()); throw new RuntimeException("余额不足"); } } }
创建db.sql
代码如下(示例):
CREATE TABLE `account_tbl` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `money` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `user_id` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; insert into `account_tbl` (`id`, `user_id`, `money`) values('1','1','1000'); -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
创建mysql-common模块
项目结构如下(示例):
创建MybatisConfig
代码如下(示例):
package com.example.datasource.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import javax.sql.DataSource; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Configuration @MapperScan("com.example.datasource.mapper") public class MybatisConfig { /** * 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置, * 原生datasource前缀取"spring.datasource" * * @return */ @Bean @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSource dataSource() { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); //设置数据源 factoryBean.setDataSource(dataSource); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources("classpath*:mybatis/**/*-mapper.xml")); org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration(); //使用jdbc的getGeneratedKeys获取数据库自增主键值 configuration.setUseGeneratedKeys(true); //使用列别名替换列名 configuration.setUseColumnLabel(true); //自动使用驼峰命名属性映射字段,如userId ---> user_id configuration.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } }
创建Account
代码如下(示例):
package com.example.datasource.entity; import lombok.Data; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Data public class Account { private Integer id; private String userId; private Integer money; }
创建Order
代码如下(示例):
package com.example.datasource.entity; import lombok.Data; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Data public class Order { private Integer id; private String userId; /** 商品编号 */ private String commodityCode; private Integer count; private Integer money; private Integer status; }
创建OrderStatus
代码如下(示例):
package com.example.datasource.entity; public enum OrderStatus { /** * INIT */ INIT(0), /** * SUCCESS */ SUCCESS(1), /** * FAIL */ FAIL(-1); private final int value; OrderStatus(int value) { this.value = value; } public int getValue() { return value; } }
创建Storage
代码如下(示例):
package com.example.datasource.entity; import lombok.Data; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Data public class Storage { private Integer id; private String commodityCode; private Integer count; }
创建AccountMapper
代码如下(示例):
package com.example.datasource.mapper; import com.example.datasource.entity.Account; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Repository public interface AccountMapper { /** * 查询账户 * @param userId * @return */ @Select("select id, user_id, money from account_tbl WHERE user_id = #{userId}") Account selectByUserId(@Param("userId") String userId); /** * 扣减余额 * @param userId 用户id * @param money 要扣减的金额 * @return */ @Update("update account_tbl set money =money-#{money} where user_id = #{userId}") int reduceBalance(@Param("userId") String userId, @Param("money") Integer money); }
创建OrderMapper
代码如下(示例):
package com.example.datasource.mapper; import com.example.datasource.entity.Order; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Repository public interface OrderMapper { /** * 保存订单 * @param record * @return */ @Insert("INSERT INTO order_tbl (user_id, commodity_code, count, status, money) VALUES (#{userId}, #{commodityCode}, #{count}, #{status}, #{money})") @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id") int insert(Order record); /** * 更新订单状态 * @param id * @param status * @return */ @Update("UPDATE order_tbl SET status = #{status} WHERE id = #{id}") int updateOrderStatus(@Param("id") Integer id, @Param("status") int status); }
创建StorageMapper
代码如下(示例):
package com.example.datasource.mapper; import com.example.datasource.entity.Storage; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Repository public interface StorageMapper { /** * 获取库存 * @param commodityCode 商品编号 * @return */ @Select("SELECT id,commodity_code,count FROM storage_tbl WHERE commodity_code = #{commodityCode}") Storage findByCommodityCode(@Param("commodityCode") String commodityCode); /** * 扣减库存 * @param commodityCode 商品编号 * @param count 要扣减的库存 * @return */ @Update("UPDATE storage_tbl SET count = count - #{count} WHERE commodity_code = #{commodityCode}") int reduceStorage(@Param("commodityCode") String commodityCode,@Param("count") Integer count); }
创建ResultVo
代码如下(示例):
package com.example.datasource.vo; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @Author: liaozhiwei * @Description: TODO * @Date: Created in 19:22 2022/8/30 */ @Data public class ResultVo extends HashMap<String, Object> { public ResultVo() { put("code", 0); put("msg", "success"); } public static ResultVo error(int code, String msg) { ResultVo r = new ResultVo(); r.put("code", code); r.put("msg", msg); return r; } public static ResultVo ok(String msg) { ResultVo r = new ResultVo(); r.put("msg", msg); return r; } public static ResultVo ok(Map<String, Object> map) { ResultVo r = new ResultVo(); r.putAll(map); return r; } public static ResultVo ok() { return new ResultVo(); } @Override public ResultVo put(String key, Object value) { super.put(key, value); return this; } }
开始验证seata是否正常工作
- account_tbl表里面我给了一条记录,userid为1的有1000块钱;
- storage_tbl表里面我给了一条记录,商品编号1000的库存有1000;
- 假设这个编号1000的商品现在的价格是20块钱
正常减库存
我userid为1的用户需要买30个,走正常逻辑是可以正常下订单,订单表里会多一条记录,同时account_tbl表的金额也会减少,storage_tbl表的库存也会减少;
使用apifox或者postman调用接口:http://localhost:8020/order/createOrder
{ "userId": "1", "commodityCode": "1000", "count": 30, "money": 20 }
如下(示例):
account_tbl表数据变化
如下(示例):
order_tbl表数据变化
如下(示例):
storage_tbl表数据变化
如下(示例):
订单服务控制台打印:
如下(示例):
不正常减库存
我userid为1的用户需要买10000个,走正常逻辑是不可以正常下订单,第一个钱不够,第二个库存也不够,所以订单表里不会多一条记录,同时account_tbl表的金额不会改变,storage_tbl表的库存不会改变;
使用apifox或者postman调用接口:http://localhost:8020/order/createOrder
{ "userId": "1", "commodityCode": "1000", "count": 10000, "money": 20 }
如下(示例):
订单服务控制台打印:
如下(示例):
库存服务控制台打印:
如下(示例):
三个表都没有发生变化,业务逻辑正确,seata可以正常工作。