Seata 案例应用
业务说明:
创建三个服务:一个订单服务、一个库存服务、一个账户服务
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
再通过远程调用账户服务来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
一、创建数据库(订单/库存/账号)
1.1 创建 存储订单的数据库(seata_order)
1.1.1 创建订单数据库
create DATABASE seata_order;
1.1.2 创建订单数据表(t_order)
CREATE TABLE `t_order` ( `id` bigint(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id', `product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id', `count` int(11) NULL DEFAULT NULL COMMENT '数量', `money` decimal(11, 0) NULL DEFAULT NULL COMMENT '金额', `status` int(11) NULL DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
1.1.3 创建回滚日志
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;
1.2 创建存储库存的数据库(seata_storage)
1.2.1 创建库存数据库
create DATABASE seata_storage;
1.2.2 创建库存数据表
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',
`total` int(11) NULL DEFAULT NULL COMMENT '总库存',
`used` int(11) NULL DEFAULT NULL COMMENT '已用库存',
`residue` int(11) NULL DEFAULT NULL COMMENT '剩余库存'
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
插入一条库存信息
1.2.3 创建日志回滚表
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;
1.3 创建存储账户信息的数据库(seata_account)
1.3.1 创建数据库
create DATABASE seata_account;
1.3.2 创建数据表
CREATE TABLE `t_account` ( `id` bigint(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id', `total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度', `used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用余额', `residue` decimal(10, 0) NULL DEFAULT NULL COMMENT '剩余可用额度' ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
插入一条信息
1.3.3 创建日志回滚表
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;
二、创建订单微服务
2.1 创建订单微服务Module
创建名为“seata-order-service2001”的微服务
2.2 改POM
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-nacos</artifactId>
<groupId>com.ckfuture.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service2001</artifactId>
<dependencies>
<!--SpringCloud alibaba 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-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
</project>
2.3 建YML
新建“application.yml”文件
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: #自定义事务组名称需要与seata-server中file.conf的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.144.215.114:3306/seata_order username: root password: Hckj201509 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
2.4 创建file.conf文件
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://IP地址:3306/seata" user = "账户" password = "密码" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }
2.5 创建registry.conf文件
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
2.6 创建domain实体
2.6.1 CommonResult类
package com.ckfuture.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@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);
}
}
2.6.2 Order实体类
package com.ckfuture.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@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.7 dao
创建“OrderDao”接口
package com.ckfuture.springcloud.alibaba.dao; import com.ckfuture.springcloud.alibaba.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface OrderDao { //1 新建订单 void create(Order order); //2 修改订单状态,从零改为1 void update(@Param("userId") Long userId, @Param("status") Integer status); }
创建OrderMapper.xml
在resources下创建mapper文件夹并创建“OrderMapper.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.ckfuture.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.ckfuture.springcloud.alibaba.domain.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="create">
insert into t_order (id,user_id,product_id,count,money,status)
values(null,#{userId},#{productId},#{count},#{money},0);
</insert>
<update id="update">
update t_order set status = 1
where user_id=#{userId} and status = #{status};
</update>
</mapper>
2.8 service层
2.8.1 AccountService
package com.ckfuture.springcloud.alibaba.service; import com.ckfuture.springcloud.alibaba.domain.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); }
2.8.2 StorageService
package com.ckfuture.springcloud.alibaba.service; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "seata-storage-service") public interface StorageService { @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count); }
2.8.3 OrderService
package com.ckfuture.springcloud.alibaba.service; import com.ckfuture.springcloud.alibaba.domain.Order; public interface OrderService { //1 新建订单 void create(Order order); }
2.8.4 OrderServiceImpl
package com.ckfuture.springcloud.alibaba.service.impl; import com.ckfuture.springcloud.alibaba.dao.OrderDao; import com.ckfuture.springcloud.alibaba.domain.Order; import com.ckfuture.springcloud.alibaba.service.AccountService; import com.ckfuture.springcloud.alibaba.service.OrderService; import com.ckfuture.springcloud.alibaba.service.StorageService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; @Override public void create(Order order) { log.info("------>开始新建订单"); orderDao.create(order); log.info("------>订单微服务开始调用库存,做扣减Count"); storageService.decrease(order.getProductId(),order.getCount()); log.info("------>订单微服务开始调用账户,做扣减Money"); accountService.decrease(order.getUserId(),order.getMoney()); //4.修改订单状态,从零到1,1代表已经完成 log.info("------->修改订单状态开始"); orderDao.update(order.getUserId(),0); } }
2.9 Controller层
package com.ckfuture.springcloud.alibaba.controller; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import com.ckfuture.springcloud.alibaba.domain.Order; import com.ckfuture.springcloud.alibaba.service.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class OrderController { @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order){ orderService.create(order); return new CommonResult(200,"订单创建成功"); } }
2.10 Config配置
2.10.1 MyBatisConfig
package com.ckfuture.springcloud.alibaba.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.ckfuture.springcloud.alibaba.dao") public class MyBatisConfig { }
2.10.2 DataSourceProxyConfig
package com.ckfuture.springcloud.alibaba.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}") //"classpath:mapper/*.xml"
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource){
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
PathMatchingResourcePatternResolver resolver=new PathMatchingResourcePatternResolver();
Resource[] resources=resolver.getResources(mapperLocations);
sqlSessionFactoryBean.setMapperLocations(resources);
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
2.11 主启动类
package com.ckfuture.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude= DataSourceAutoConfiguration.class)//取消数据源的自动创建 public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class,args); System.out.println("订单微服务启动......"); } }
启动测试
三、创建库存微服务
参照订单微服务来创建库存微服务
3.1建立module
名称为:“seata-storage-service2002”的Module。
3.2 改POM
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud-nacos</artifactId> <groupId>com.ckfuture.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-storage-service2002</artifactId> <dependencies> <!--SpringCloud alibaba 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-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--SpringCloud alibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud alibaba sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> </dependencies> </project>
3.3 建YML
server: port: 2002 spring: application: name: seata-storage-service cloud: alibaba: seata: #自定义事务组名称需要与seata-server中file.conf的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.144.215.114:3306/seata_storage username: root password: Hckj201509 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
3.4 file.conf和registry.conf
同订单微服务中一样。
3.5 config
package com.ckfuture.springcloud.alibaba.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.ckfuture.springcloud.alibaba.dao") public class MyBatisConfig { }
package com.ckfuture.springcloud.alibaba.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.transaction.SpringManagedTransactionFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; /** * 使用Seata对数据源进行代理 */ @Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") //"classpath:mapper/*.xml" private String mapperLocations; @Bean @ConfigurationProperties(prefix="spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource){ return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); PathMatchingResourcePatternResolver resolver=new PathMatchingResourcePatternResolver(); Resource[] resources=resolver.getResources(mapperLocations); sqlSessionFactoryBean.setMapperLocations(resources); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
3.6 domain
package com.ckfuture.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data @AllArgsConstructor @NoArgsConstructor public class Storage { private Long id; private Long productId; private Integer total; private Integer used; private Integer residue; }
3.7 dao
package com.ckfuture.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); }
<?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.ckfuture.springcloud.alibaba.dao.StorageDao"> <resultMap id="BaseResultMap" type="com.ckfuture.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>
3.8 service
package com.ckfuture.springcloud.alibaba.service; import org.apache.ibatis.annotations.Param; public interface StorageService { //扣减库存 void decrease(Long productId,Integer count); }
package com.ckfuture.springcloud.alibaba.service.impl; import com.ckfuture.springcloud.alibaba.dao.StorageDao; import com.ckfuture.springcloud.alibaba.service.StorageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; /** * 扣减 * @param productId * @param count */ @Override public void decrease(Long productId, Integer count) { LOGGER.info("------>扣减库存开始"); storageDao.decrease(productId,count); LOGGER.info("------>扣减库存结束"); } }
3.9 controller
package com.ckfuture.springcloud.alibaba.controller; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import com.ckfuture.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,"扣减库存成功"); } }
3.10 主启动
package com.ckfuture.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude= DataSourceAutoConfiguration.class)//取消数据源的自动创建 public class SeataStorageMainApp2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageMainApp2002.class,args); System.out.println("库存微服务启动......"); } }
启动服务
四、创建账户微服务
4.1创建Module
创建一个名为”seata-account-service2003“的Module。
4.2.改POM
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud-nacos</artifactId> <groupId>com.ckfuture.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-account-service2003</artifactId> <dependencies> <!--SpringCloud alibaba 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-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--SpringCloud alibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud alibaba sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> </dependencies> </project>
4.3 建YML
server: port: 2003 spring: application: name: seata-account-service cloud: alibaba: seata: #自定义事务组名称需要与seata-server中file.conf的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.144.215.114:3306/seata_storage username: root password: Hckj201509 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
4.4 file.conf和registry.conf
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://192.144.215.114:3306/seata" user = "root" password = "Hckj201509" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
4.5 domain
package com.ckfuture.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @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); } }
package com.ckfuture.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; }
4.6 dao
package com.ckfuture.springcloud.alibaba.dao; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; @Mapper public interface AccountDao { //扣减账号金额 CommonResult decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }
<?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.ckfuture.springcloud.alibaba.dao.AccountDao"> <resultMap id="BaseResultMap" type="com.ckfuture.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 used = used+#{money},residue=residue-#{money} where user_id=#{userId}; </update> </mapper>
4.7 service
package com.ckfuture.springcloud.alibaba.service; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; public interface AccountService { //扣减账号金额 CommonResult decrease(Long userId,BigDecimal money); }
package com.ckfuture.springcloud.alibaba.service.impl; import com.ckfuture.springcloud.alibaba.dao.AccountDao; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import com.ckfuture.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; @Service public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); @Resource private AccountDao accountDao; @Override public CommonResult decrease(Long userId, BigDecimal money) { return accountDao.decrease(userId,money); } }
4.7 controller
package com.ckfuture.springcloud.alibaba.controller; import com.ckfuture.springcloud.alibaba.domain.CommonResult; import com.ckfuture.springcloud.alibaba.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController public class AccountController { @Autowired private AccountService accountService; @RequestMapping("/account/decrease") public CommonResult decrease(Long userId, BigDecimal money){ accountService.decrease(userId,money); return new CommonResult(200,"扣减金额成功"); } }
4.9 主启动
package com.ckfuture.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude= DataSourceAutoConfiguration.class)//取消数据源的自动创建 public class SeataAccountMainApp2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMainApp2003.class,args); System.out.println("账户微服务启动......"); } }
启动测试
五、@GlobalTransactionz注解
在2001微服务的实现类中,即:调用其他两个微服务的实现类中添加
@GlobalTransactional(name="fsp-create-order",rollbackFor = Exception.class)
package com.ckfuture.springcloud.alibaba.service.impl; import com.ckfuture.springcloud.alibaba.dao.OrderDao; import com.ckfuture.springcloud.alibaba.domain.Order; import com.ckfuture.springcloud.alibaba.service.AccountService; import com.ckfuture.springcloud.alibaba.service.OrderService; import com.ckfuture.springcloud.alibaba.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; @Override @GlobalTransactional(name="fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { log.info("------>开始新建订单"); orderDao.create(order); log.info("------>订单微服务开始调用库存,做扣减Count"); storageService.decrease(order.getProductId(),order.getCount()); log.info("------>订单微服务开始调用账户,做扣减Money"); accountService.decrease(order.getUserId(),order.getMoney()); //4.修改订单状态,从零到1,1代表已经完成 log.info("------->修改订单状态开始"); orderDao.update(order.getUserId(),0); } }
如果程序报错或异常,事物回滚达到数据一致性。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-03-18 C# 引用MyBatis