视频链接:2020最新版SpringCloud框架开发教程-周阳
文章源码:https://github.com/geyiwei-suzhou/cloud2020/
设计三个微服务:订单服务、库存服务、账户服务
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减账户里面的余额,最后在订单微服务中修改订单状态为已完成
下订单 --> 扣库存 --> 减余额 --> 改状态
该操作跨越三个数据库,有两次远程调用,是一个分布式问题
创建三个数据库、六张表
-
seata_order:存储订单的数据库,并创建t_order、undo_log表
-
seata_storage:存储库存的数据库,并创建t_storage、undo_log表
-
seata_account:存储账户信息的数据库,并创建t_account、undo_log表
Drop Database if exists seata_order; create database seata_order; Drop Database if exists seata_storage; create database seata_storage; Drop Database if exists seata_account; create database seata_account; DROP TABLE IF EXISTS seata_order.t_order; CREATE TABLE seata_order.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; DROP TABLE IF EXISTS seata_storage.t_storage; CREATE TABLE seata_storage.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; DROP TABLE IF EXISTS seata_account.t_account; CREATE TABLE seata_account.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_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100'); INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000'); DROP TABLE IF EXISTS seata_order.undo_log; CREATE TABLE seata_order.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; DROP TABLE IF EXISTS seata_storage.undo_log; CREATE TABLE seata_storage.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; DROP TABLE IF EXISTS seata_account.undo_log; CREATE TABLE seata_account.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、seata-storage-service2002、seata-account-service2003
seata-order-service2001模块
1. 建module
New --> Module --> Maven[Module SDK:1.8.0_191] --> name[seata-order-service2001] --> Finish
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>cloud2020</artifactId>
<groupId>com.antherd.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service2001</artifactId>
<dependencies>
<!-- 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>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<artifactId>spring-boot-starter</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
<exclusions>
<exclusion>
<artifactId>fastjson</artifactId>
<groupId>com.alibaba</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- SpringCloud alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入公共api包-->
<dependency>
<groupId>com.antherd.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot 整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- actuator监控信息完善 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3. 写yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组名称需要与seata-server中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useSSL=false
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
然后把seata上一篇修改过的两个文件:conf/file.conf、conf/registry.conf 复制到resource目录下
4. 主启动
新建类:com.antherd.springcloud.alibaba.SeataOrderMainApp2001
package com.antherd.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;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
5 写业务
新建类:com.antherd.springcloud.alibaba.config.DataSourceProxyConfig
package com.antherd.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.support.PathMatchingResourcePatternResolver;
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
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 sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
新建类:com.antherd.springcloud.alibaba.config.MybatisConfig
package com.antherd.springcloud.alibaba.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.antherd.springcloud.alibaba.dao"})
public class MybatisConfig {
}
新建类:com.antherd.springcloud.alibaba.domain.Order
package com.antherd.springcloud.alibaba.domain;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@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:已完结
}
新建类:com.antherd.springcloud.alibaba.dao.OrderDao
package com.antherd.springcloud.alibaba.dao;
import com.antherd.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderDao {
// 1.新建订单
void create(Order order);
// 2.修改订单状态,从0改为1
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
新建类:com.antherd.springcloud.alibaba.service.OrderService
package com.antherd.springcloud.alibaba.service;
import com.antherd.springcloud.alibaba.domain.Order;
public interface OrderService {
void create(Order order);
}
新建类:com.antherd.springcloud.alibaba.service.impl.OrderServiceImpl
package com.antherd.springcloud.alibaba.service.impl;
import com.antherd.springcloud.alibaba.dao.OrderDao;
import com.antherd.springcloud.alibaba.domain.Order;
import com.antherd.springcloud.alibaba.service.AccountService;
import com.antherd.springcloud.alibaba.service.OrderService;
import com.antherd.springcloud.alibaba.service.StorageService;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 下订单->减库存->减余额->改状态
* @param order
*/
@Override
public void create(Order order) {
log.info("***** 开始新建订单");
orderDao.create(order);
log.info("***** 订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("***** 订单微服务开始调用库存,做扣减end");
log.info("***** 订单微服务开始调用账户,做扣减money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("***** 订单微服务开始调用账户,做扣减end");
log.info("***** 修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("***** 修改订单状态结束");
log.info("***** 下订单结束了,O(∩_∩)O哈哈~");
}
}
新建类:com.antherd.springcloud.alibaba.service.AccountService
package com.antherd.springcloud.alibaba.service;
import com.antherd.springcloud.entities.CommonResult;
import java.math.BigDecimal;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping(value = "/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
新建类:com.antherd.springcloud.alibaba.service.StorageService
package com.antherd.springcloud.alibaba.service;
import com.antherd.springcloud.entities.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")
public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
新建类:com.antherd.springcloud.alibaba.controller.OrderController
package com.antherd.springcloud.alibaba.controller;
import com.antherd.springcloud.alibaba.domain.Order;
import com.antherd.springcloud.alibaba.service.OrderService;
import com.antherd.springcloud.entities.CommonResult;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping(value = "/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "订单创建成功");
}
}
在resource目录下新建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.antherd.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.antherd.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>
seata-storage-service2002、seata-account-service2003
因为篇幅有限,源码均上传至 GitHub:https://github.com/geyiwei-suzhou/cloud2020
seata-storage-service2002、seata-account-service2003 源码这里就不重复贴出来了
启动测试
启动:seata-order-service2001、seata-storage-service2002、seata-account-service2003 三个模块
访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 发现下单成功(订单生成、库存扣减、账户扣除、订单状态修改)
接下来,修改 seata-account-service2003
模块中com.antherd.springcloud.alibaba.service.impl.AccountServiceImpl.decrease 方法:
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("***** account-service中扣减账户余额开始");
// 模拟超时异常,全局事务回滚
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
log.info("***** storage-service中扣减账户余额结束");
}
访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 发现下单出错(Read timed out executing POST http://seata-account-service/account/decrease?userId=1&money=100
),这是我们查看数据库,发现 订单生成、库存扣减、账户扣除,但是订单状态还是创建中,这就导致了严重的数据问题(钱被扣了,订单还是未支付状态)
com.antherd.springcloud.alibaba.service.impl.create 方法上添加
@GlobalTransactional(name = "antherd-create-order", rollbackFor = Exception.class)
再试一下