一、环境准备
已经具有的环境:
mysql8.0.25
nacos
资源
资源名称 | 地址 | 说明 |
---|---|---|
nacos | https://github.com/alibaba/nacos/tags | 服务发现、注册中心、配置中心 |
seata1.4.2 | https://seata.io/zh-cn/blog/download.html | 分布式事务 |
二、seata AT模式整合
1. nacos配置
1.1、将下载好的nacos解压到目标目录
1.2、持久化nacos到MySQL,本地新建nacos数据库,
找到nacos/conf下面的nacos-mysql.sql脚本执行
导入成功就会出现这些表
1.3、修改conf目录下的application.properties
配置文件,如下图:
1.4、进入bin目录下新建start.bat
文件,用记事本打开,输入
startup.cmd -m standalone
双击start.bat
启动,访问http://localhost:8848/nacos/#/login 登录即可
进入到nacos控制台默认用户名密码都是nacos
点击命名空间,在点击新建命名空间
新建一个seata命名空间
2、Seata安装配置
2.1、seata下载地址:https://seata.io/zh-cn/blog/download.html
下载之后解压
2.2、修改conf下的配置文件file.conf
2.3、步骤2初始化化数据库需要的SQL地址: https://github.com/seata/seata/tree/develop/script/server/db
创建数据库seata
,导入sql文件
导入成功之后
2.4、修改registry.conf的注册中心配置
2.5、把seata配置导入到nacosnacos-config.sh
脚本准备:
下载地址https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh
,并将文件置于seata的conf目录下
2.6、config.txt准备,在conf的同级目录下新建config.txt文件
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
#修改my_test_tx_group为自定义服务seata-group
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
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=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
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
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
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
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.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
完成后的目录样子
2.7、在conf目录下,打开git bash,输入 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t c9829d3b-5e7c-4d41-b0f1-00c4240c9c21
-u nacos -w nacos,其中-t是nacos命名空间,可删除(当然建议给seata单独建命名空间,这样以后配置多了,可以很直观的找到),这个命名空间就是我们刚刚新建的seata
的id,不能乱输入,不然nacos配置文件没有
若没有git则去下载安装即可,执行成功的样子
其中config.txt是配置文件,耳nacos-config.sh的作用是将配置文件的属性注入到nacos中,这样就不需要将file.conf和registry.conf放到我们的项目里,若没有找到config.txt则要把它放到seata的根目录
配置就直接从nacos中读取。如图:
然后去nacos控制台
可以看到成功加载
3、项目整合seata
- 数据库准备:
三个数据:seata_account
、seata_order
、seata_storage
sql语句:
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
建表语句注意区分数据库
: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;
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;
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;
最后分别在每一个数据库下面创建undo_log表
:undo_logo
:
-- 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_log
DROP TABLE `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;
建成功之后:
- 新建订单模块
seata-order-service2001
- 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>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</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>8.0.25</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>
- 查看依赖
- 修改
application.yml
配置文件
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
# seata:
# #自定义事务组名称需要与seata-server中的对应
# tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
group: SEATA_GROUP
# namespace: c9829d3b-5e7c-4d41-b0f1-00c4240c9c21 不指定命名空间。默认public
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=NO&serverTimezone=Asia/Shanghai
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
tx-service-group: my_test_tx_group #这里要特别注意和nacos中配置的要保持一直
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
#namespace: c9829d3b-5e7c-4d41-b0f1-00c4240c9c21 默认public
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: c9829d3b-5e7c-4d41-b0f1-00c4240c9c21 #2.2中配置所在命名空间ID,入未配置 默认public空间
service:
vgroup-mapping:
my_test_tx_group: default # 这里要特别注意和nacos中配置的要保持一直
所以是public
,不然就会出现
No available service
- 实体类
Order
、CommonResult
order
package com.zhubayi.springcloud.alibaba.doamin;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author zhubayi
*/
@Data
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
CommonResult
package com.zhubayi.springcloud.alibaba.doamin;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zhubayi
*/
@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);
}
}
-
dao层
创建OrderDao
接口
package com.zhubayi.springcloud.alibaba.dao;
import com.zhubayi.springcloud.alibaba.doamin.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @author zhubayi
*/
@Mapper
public interface OrderDao {
/**
* 创建订单
* @param order
*/
void createOrder(Order order);
/**
* 更新订单
* @param userId
* @param status
*/
void updateStatus(@Param("userId") Long userId, @Param("status") Integer status);
}
-
service
层OrderService
、StorageService
、AccountService
OrderService
package com.zhubayi.springcloud.alibaba.service;
import com.zhubayi.springcloud.alibaba.doamin.Order;
/**
* @author zhubayi
*/
public interface OrderService {
/**
* 创建订单
* @param order
*/
void createOrder(Order order);
}
StorageService
package com.zhubayi.springcloud.alibaba.service;
import com.zhubayi.springcloud.alibaba.doamin.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* @author zhubayi
*/
@FeignClient(value = "seata-storage-service")
public interface StorageService
{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
AccountService
package com.zhubayi.springcloud.alibaba.service;
import com.zhubayi.springcloud.alibaba.doamin.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "seata-account-service")
public interface AccountService
{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
创建orderService
的实现类OrderServiceImpl
package com.zhubayi.springcloud.alibaba.service.impl;
import com.zhubayi.springcloud.alibaba.dao.OrderDao;
import com.zhubayi.springcloud.alibaba.doamin.Order;
import com.zhubayi.springcloud.alibaba.service.AccountService;
import com.zhubayi.springcloud.alibaba.service.OrderService;
import com.zhubayi.springcloud.alibaba.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;
/**
* @author zhubayi
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单
*
* @param order
*/
@Override
public void createOrder(Order order) {
log.info("=====>{}","开始新建订单");
orderDao.createOrder(order);
log.info("=====>{}","新建订单成功");
log.info("=====>{}","库存减少");
storageService.decrease(order.getProductId(),order.getCount());
log.info("=====>{}","库存减少成功");
log.info("=====>{}","开始减少余额");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("=====>{}","减少余额成功");
log.info("=====>{}","开始修改订单状态");
orderDao.updateStatus(order.getUserId(),0);
log.info("=====>{}","修改订单状态成功");
log.info("=====>{}","end");
}
}
- 创建
OrderController
package com.zhubayi.springcloud.alibaba.controller;
import com.zhubayi.springcloud.alibaba.doamin.CommonResult;
import com.zhubayi.springcloud.alibaba.doamin.Order;
import com.zhubayi.springcloud.alibaba.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhubayi
*/
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("create")
public CommonResult create(Order order){
orderService.createOrder(order);
return new CommonResult(200,"创建成功");
}
}
在resources目录下创建mapper
目录,再mapper
下创建OrderDao.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.zhubayi.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseMap" type="com.zhubayi.springcloud.alibaba.doamin.Order">
<id 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>
<insert id="createOrder">
insert into t_order(id , user_id , product_id , count, money , status )
values (null,#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="updateStatus">
update t_order set status=1 where user_id=#{userId} and status=#{status}
</update>
</mapper>
- 主启动类
SeataOrderMainApp2001
package com.zhubayi.springcloud.alibaba;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author zhubayi
*/
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(basePackages = {"com.zhubayi.springcloud.alibaba.dao"})
@SpringBootApplication
public class SeataOrderMainApp2001
{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
完成之后的目录样子:
-
新建库存模块
seata-storage-service2002
- 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>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 修改配置文件
application.yml
,这个与前面的订单模块seata-order-service2001
几乎一样,端口和名字不同,数据库换为seata_storage
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
# seata:
# tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
group: SEATA_GROUP
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=NO&serverTimezone=Asia/Shanghai
username: root
password: 123456
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
tx-service-group: my_test_tx_group #这里要特别注意和nacos中配置的要保持一直
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: c9829d3b-5e7c-4d41-b0f1-00c4240c9c21
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
#namespace: ${spring.cloud.nacos.discovery.namespace} #2.2中配置所在命名空间ID,入未配置 默认public空间
service:
vgroup-mapping:
my_test_tx_group: default # 这里要特别注意和nacos中配置的要保持一直
- 实体类
Storage
Storage
:
package com.zhubayi.springcloud.alibaba.domain;
import lombok.Data;
@Data
public class Storage {
private Long id;
/**
* 产品id
*/
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer used;
/**
* 剩余库存
*/
private Integer residue;
}
CommonResult
与订单模块一样
-
service
层StorageService
package com.zhubayi.springcloud.alibaba.service;
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
StorageService
的实现类StorageServiceImpl
package com.zhubayi.springcloud.alibaba.service.impl;
import com.zhubayi.springcloud.alibaba.dao.StorageDao;
import com.zhubayi.springcloud.alibaba.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class StorageServiceImpl implements StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
@Resource
private StorageDao storageDao;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId,count);
LOGGER.info("------->storage-service中扣减库存结束");
}
}
-
StorageDao
接口:
package com.zhubayi.springcloud.alibaba.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface StorageDao {
/**
*扣减库存
**/
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
StorageDao.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.zhubayi.springcloud.alibaba.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.zhubayi.springcloud.alibaba.domain.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>
-
StorageController
类
package com.zhubayi.springcloud.alibaba.controller;
import com.zhubayi.springcloud.alibaba.domain.CommonResult ;
import com.zhubayi.springcloud.alibaba.service.StorageService ;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200,"扣减库存成功!");
}
}
- 主启动类
SeataStorageServiceApplication2002
package com.zhubayi.springcloud.alibaba;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinWorkerThread;
/**
* @auther zzyy
* @create 2019-12-12 17:31
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(basePackages = {"com.zhubayi.springcloud.alibaba.dao"})
public class SeataStorageServiceApplication2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageServiceApplication2002.class, args);
}
}
完成之后的样子:
-
创建
seata-account-service2003
用户余额模块
- 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>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 新建配置文件
application.yml
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
#seata:
#tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
group: SEATA_GROUP
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf-8&useSSL=NO&serverTimezone=Asia/Shanghai
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
seata:
tx-service-group: my_test_tx_group #这里要特别注意和nacos中配置的要保持一直
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: c9829d3b-5e7c-4d41-b0f1-00c4240c9c21
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
#namespace: ${spring.cloud.nacos.discovery.namespace} #2.2中配置所在命名空间ID,入未配置 默认public空间
service:
vgroup-mapping:
my_test_tx_group: default # 这里要特别注意和nacos中配置的要保持一直
注意数据库名称
- 实体类
Account
package com.zhubayi.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 总额度
*/
private BigDecimal total;
/**
* 已用额度
*/
private BigDecimal used;
/**
* 剩余额度
*/
private BigDecimal residue;
}
CommonResult
与前面的相同
-
AccountDao
接口
package com.zhubayi.springcloud.alibaba.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
/**
* @author zhubayi
*/
@Mapper
public interface AccountDao {
/**
* 扣减账户余额
* @param userId
* @param money
*/
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
-
AccountService
接口
package com.zhubayi.springcloud.alibaba.service;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.math.BigDecimal;
public interface AccountService {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 金额
*/
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
-
AccountService
接口的实现类AccountServiceImpl
package com.zhubayi.springcloud.alibaba.service.impl;
import com.zhubayi.springcloud.alibaba.dao.AccountDao;
import com.zhubayi.springcloud.alibaba.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
/**
* 账户业务实现类
*/
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
AccountDao accountDao;
/**
* 扣减账户余额
*/
@Override
public 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中扣减账户余额结束");
}
}
AccountController
类
package com.zhubayi.springcloud.alibaba.controller;
import com.zhubayi.springcloud.alibaba.domain.CommonResult;
import com.zhubayi.springcloud.alibaba.service.AccountService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣减账户余额成功!");
}
}
- 启动类
SeataAccountMainApp2003
package com.zhubayi.springcloud.alibaba;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author zhubayi
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan({"com.zhubayi.springcloud.alibaba.dao"})
public class SeataAccountMainApp2003
{
public static void main(String[] args)
{
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
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.zhubayi.springcloud.alibaba.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.zhubayi.springcloud.alibaba.domain.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>
启动三个模块还有nacos
和seata
021-07-05 11:48:22.383 INFO 12568 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-07-05 11:48:22.469 INFO 12568 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'Nacos-Watch-Task-Scheduler'
2021-07-05 11:48:22.820 INFO 12568 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
2021-07-05 11:48:22.895 INFO 12568 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 2001 (http) with context path ''
2021-07-05 11:48:22.899 INFO 12568 --- [ main] c.a.c.n.registry.NacosServiceRegistry : nacos registry, SEATA_GROUP seata-order-service 192.168.0.123:2001 register finished
2021-07-05 11:48:23.006 INFO 12568 --- [ main] c.z.s.alibaba.SeataOrderMainApp2001 : Started SeataOrderMainApp2001 in 4.239 seconds (JVM running for 4.756)
2021-07-05 11:48:23.395 INFO 12568 --- [)-192.168.0.123] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-07-05 11:48:23.395 INFO 12568 --- [)-192.168.0.123] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-07-05 11:48:23.400 INFO 12568 --- [)-192.168.0.123] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
`2021-07-05 11:49:20.607 INFO 12568 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.0.123:8091
2021-07-05 11:49:20.608 INFO 12568 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:TMROLE,address:192.168.0.123:8091,msg:< RegisterTMRequest{applicationId='seata-order-service', transactionServiceGroup='my_test_tx_group'} >
2021-07-05 11:49:20.613 INFO 12568 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient : register TM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xd7cc02ac, L:/192.168.0.123:54703 - R:/192.168.0.123:8091]
2021-07-05 11:49:20.613 INFO 12568 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 3 ms, version:1.4.2,role:TMROLE,channel:[id: 0xd7cc02ac, L:/192.168.0.123:54703 - R:/192.168.0.123:8091]`
注意出现黄色部分才是配置正确了,注册进了nacos
打开浏览器输入:http://127.0.0.1:2001/order/create?userId=1&productId=1&count=10&money=100
可以看到订单创建成功,再去看看数据库:
这时我们手动制造一个超时异常,默认openfeign
远程调用1秒,没有返回数据就报错
再去访问:http://127.0.0.1:2001/order/create?userId=1&productId=1&count=10&money=100
再看看控制台:
订单模块报了超时异常,数据库里面多了一个订单,但是订单的状态没有改变,这里就要用分布式事务来处理。
为了解决这个问题,我们要在发生分布式事务的方法上加上@GlobalTransactiona注解
然后再去浏览去访问:http://127.0.0.1:2001/order/create?userId=1&productId=1&count=10&money=100
再去看看控制台:
也是超时异常
去查看数据库order
表并没有增加,accout
、storage
表也没有发送改变。说明解决了分布式事务的问题.
为了看的更加详细我们打个断点看看
再去调用http://127.0.0.1:2001/order/create?userId=1&productId=1&count=10&money=100
- 多了一条数据
库存扣减成功
查看全局锁、分支锁、表数据锁
查看seata库表锁状态信息
- 查看order库、storage库 undo_log回滚日志