一,基本介绍
1.什么是分布式事务
首先这是普通事务:
下面是分布式事务:
在微服务系统中,每个微服务应用都可能会有自己的数据库,它们首先需要控制自己的本地事务。
一项业务操作可能会调用执行多个微服务。如何保证多个服务执行的多个数据库的操作整体成功或整体失败?这就是分布式事务要解决的问题。
2.理论部分
CAP 和 BASE 是对大规模互联网系统分布式实践的理论总结。
CAP
CAP原则
在分布式系统中,由于网络原因出现子系统之间无法通信的情况,就会造成分区。一般分布式系统中必须容忍这种情况,那么就需要在A和C之间进行取舍。
在分布式事务中
- 如果保证CP,就意味着要让所有子系统的数据操作要么全部成功,要么全部失败,不允许有不一致的情况发生。但是强一致性会造成性能下降。
- 如果保证AP,就意味着可以牺牲一定的一致性,允许在各个子系统中存在有的数据操作成功,有的数据操作失败的情况,只要通过后续处理,能够达到最终一致即可。
BASE
3.分布式事务方案
分布式事务有以下解决方案:
- XA
- TCC
- Seata 框架 AT 事务
- SAGA
- 可靠消息最终一致性
- 最大努力通知
二,业务场景
订单系统
当用户下订单时,执行以下三步流程:
- 订单系统保存订单
- 订单系统调用库存服务,减少商品库存
- 订单系统调用账户服务,扣减用户金额
这三步要作为一个整体事务进行管理,要么整体成功,要么整体失败。
三,创建父项目
三,数据库初始化工具
1.订单案例涉及四个数据库:
为了后续测试方便我们编写一个工具,用来重置所有数据库表,可以方便地把数据重置到初始状态。
2.创建springboot子项目module
添加数据库相关依赖
3.修改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.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.drhj</groupId>
<artifactId>db-init</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>db-init</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.配置application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
5.添加 sql 脚本文件
下面,在 resources 目录下,先新建一个 sql 文件夹,四个 sql 脚本文件放在 sql 文件夹下:
seata-server.sql
drop database if exists `seata`;
CREATE DATABASE `seata` CHARSET utf8;
use `seata`;
-- -------------------------------- 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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
order.sql
drop database if exists `seata_order`;
CREATE DATABASE `seata_order` charset utf8;
use `seata_order`;
CREATE TABLE `order` (
`id` bigint(11) NOT NULL,
`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 '金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;
-- 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(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 = utf8 COMMENT ='AT transaction mode undo table';
CREATE TABLE IF NOT EXISTS segment
(
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
VERSION BIGINT DEFAULT 0 NOT NULL COMMENT '版本号',
business_type VARCHAR(63) DEFAULT '' NOT NULL COMMENT '业务类型,唯一',
max_id BIGINT DEFAULT 0 NOT NULL COMMENT '当前最大id',
step INT DEFAULT 0 NULL COMMENT '步长',
increment INT DEFAULT 1 NOT NULL COMMENT '每次id增量',
remainder INT DEFAULT 0 NOT NULL COMMENT '余数',
created_at BIGINT UNSIGNED NOT NULL COMMENT '创建时间',
updated_at BIGINT UNSIGNED NOT NULL COMMENT '更新时间',
CONSTRAINT uniq_business_type UNIQUE (business_type)
) CHARSET = utf8mb4
ENGINE INNODB COMMENT '号段表';
INSERT INTO segment
(VERSION, business_type, max_id, step, increment, remainder, created_at, updated_at)
VALUES (1, 'order_business', 1000, 1000, 1, 0, NOW(), NOW());
storage.sql
drop database if exists `seata_storage`;
CREATE DATABASE `seata_storage` charset utf8;
use `seata_storage`;
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`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 '剩余库存',
`frozen` int(11) DEFAULT '0' COMMENT 'TCC事务锁定的库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
-- 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(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 = utf8 COMMENT ='AT transaction mode undo table';
account.sql
drop database if exists `seata_account`;
CREATE DATABASE `seata_account` charset utf8;
use `seata_account`;
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) UNIQUE 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 '剩余可用额度',
`frozen` decimal(10,0) DEFAULT '0' COMMENT 'TCC事务锁定的金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '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(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 = utf8 COMMENT ='AT transaction mode undo table';
6.主程序中添加代码,执行sql脚本
Spring 中提供了一个 jdbc 脚本执行器,使用这个工具可以非常方便的运行一个 sql 脚本文件,下面是这个方法:
ScriptUtils.executeSqlScript()
只需要传入它需要的参数即可。
下面代码运行 sql 目录中的四个脚本程序,每次运行都会删除四个数据库再重新创建,并初始化数据。
package com.drhj.dbinit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
@SpringBootApplication
public class DbInitApplication {
@Autowired
private DataSource dataSource;
public static void main(String[] args) {
SpringApplication.run(DbInitApplication.class, args);
}
@PostConstruct
public void init() throws Exception{
exec("sql/account.sql");
exec("sql/order.sql");
exec("sql/seata-server.sql");
exec("sql/storage.sql");
}
private void exec (String sql) throws Exception {
ClassPathResource cpr = new ClassPathResource(sql, DbInitApplication.class.getClassLoader());
EncodedResource resource = new EncodedResource(cpr, "UTF-8");
//spring jdbc 提供的一个工具,来执行sql脚本文件
ScriptUtils.executeSqlScript(dataSource.getConnection(), resource);
}
}
启动项目,数据库刷新成功
四,eureka注册中心
1.新建springboot Module:eureka-server
添加依赖
2.修改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.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.drhj</groupId>
<artifactId>eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.配置application.yml
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
server:
enable-self-preservation: false
client:
register-with-eureka: false
fetch-registry: false
4.启动类添加@EnableEurekaServer
package com.drhj.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
5.测试
启动项目
五,创建微服务的父项目
1.创建Maven的 module
2.修改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 http://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.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.drhj</groupId>
<artifactId>order-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>order-parent</name>
<modules>
<module>account</module>
<module>storage</module>
<module>order</module>
</modules>
<properties>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
<seata.version>1.3.0</seata.version>
<spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
六,account账户项目
1.在order-parent下创建子项目accout,springboot,Module
注意路径
2.修改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">
<parent>
<artifactId>order-parent</artifactId>
<groupId>com.drhj</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>account</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>account</name>
</project>
3.配置application.yml
spring:
application:
name: account
datasource:
url: jdbc:mysql:///seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
# account 8081 storage 8082 order 8083
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.drhj.account.entity
configuration:
map-underscore-to-camel-case: true #驼峰命名
logging: #打印日志
level:
com.drhj.account.mapper: debug
4.配置bootstrap.yml,指定网段
spring:
cloud:
inetutils:
preferred-networks:
- 10\.1\.6\..+
- 192\.168\.0\..+
5.添加实体类
package com.drhj.account.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private Long id;
private Long userId; //用户id
private BigDecimal total; //总数
private BigDecimal used; //已使用,已消费金额
private BigDecimal residue; //可用金额
private BigDecimal frozen; //冻结金额
}
6.创建AccountMapper接口
package com.drhj.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.drhj.account.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
//扣除账户金额
void decrease(Long userId, BigDecimal money);
}
7.Mapper配置
先在 resources 目录下新建文件夹 mapper,然后创建文件 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.drhj.account.mapper.AccountMapper" >
<resultMap id="BaseResultMap" type="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"/>
<result column="frozen" property="frozen" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE account SET residue = residue - #{money},used = used + #{money} where user_id = #{userId};
</update>
</mapper>
8.主程序添加 Mybatis 扫描注解
添加注解 @MapperScan(“com.drhj.account.mapper”) :
package com.drhj.account;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.drhj.account.mapper")
@SpringBootApplication
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
9.添加 AccountService 接口和它的实现类
decrease() 方法实现扣减账户金额的功能
package com.drhj.account.service;
import java.math.BigDecimal;
public interface AccountService {
//扣减账户
void decrease(Long userId, BigDecimal money);
}
package com.drhj.account.service;
import com.drhj.account.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class AccountServiceImpl implements AccountService{
@Autowired
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
accountMapper.decrease(userId,money);
}
}
10.添加 AccountController 类提供客户端访问接口
package com.drhj.account.controller;
import com.drhj.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/decrease") //?userId=x&money=y
public String decrease(Long userId, BigDecimal money) {
accountService.decrease(userId,money);
return "扣减账户成功";
}
}
11.测试
1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看账户服务在 eureka 中的注册信息:
2)访问账户服务执行账户扣减金额
访问 http://localhost:8081/decrease?userId=1&money=100
3)查看控制台 Mybatis 执行的 sql 日志
4)查看数据库表,确认金额已经被减掉
七,storage库存项目
storage 库存微服务项目,用来实现减少库存的功能。
1.新建 springboot的Module:storage
2.修改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">
<parent>
<artifactId>order-parent</artifactId>
<groupId>com.drhj</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>storage</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>storage</name>
</project>
3.配置application.yml
spring:
application:
name: storage
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
# account 8081 storage 8082 order 8083
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.drhj.storage.entity
configuration:
map-underscore-to-camel-case: true #驼峰命名
logging: #打印日志
level:
com.drhj.storage.mapper: debug
4.配置bootstrap.yml
spring:
cloud:
inetutils:
preferred-networks:
- 10\.1\.6\..+
- 192\.168\.0\..+
5.创建实体类
package com.drhj.storage.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage {
private Long id;
private Long productId; //商品id
private Integer total; //总数
private Integer used; //已使用,已售出
private Integer residue; //可用库存
private Integer frozen; //冻结库存
}
6.创建 StorageMapper 接口
package com.drhj.storage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.drhj.storage.entity.Storage;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
//减少库存
void decrease(Long productId,Integer count);
}
7.Mapper配置
先在 resources 目录下新建文件夹 mapper,然后创建文件 StorageMapper.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.drhj.storage.mapper.StorageMapper" >
<resultMap id="BaseResultMap" type="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 storage SET used = used + #{count},residue = residue - #{count} WHERE product_id = #{productId}
</update>
</mapper>
8.主程序添加 Mybatis 扫描注解
添加注解 @MapperScan(“com.drhj.storage.mapper”) :
package com.drhj.storage;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.drhj.storage.mapper")
@SpringBootApplication
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
9.添加 StorageService 接口和它的实现类
decrease() 方法实现减少商品库存功能。
package com.drhj.storage.service;
public interface StorageService {
//减少库存
void decrease(Long productId,Integer count);
}
package com.drhj.storage.service;
import com.drhj.storage.mapper.StorageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
public void decrease(Long productId, Integer count) {
storageMapper.decrease(productId,count);
}
}
10.添加 StorageController 类提供客户端访问接口
package com.drhj.storage.controller;
import com.drhj.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/decrease") //?productId=1&count=1
public String decrease(Long productId,Integer count) {
storageService.decrease(productId,count);
return "减少库存成功";
}
}
11.测试
1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看库存服务在 eureka 中的注册信息:
2)访问库存服务,执行减少库存操作
访问 http://localhost:8082/decrease?productId=1&count=1
3)查看数据库表,确认金额已经被减掉
八,order订单项目
order 订单项目保存订单,并调用 storage 和 account 减少库存和扣减金额。
1.新建 springboot 的 Module:order
2.修改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">
<parent>
<artifactId>order-parent</artifactId>
<groupId>com.drhj</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order</name>
</project>
3.配置application.yml
spring:
application:
name: order
datasource:
url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
# account 8081 storage 8082 order 8083
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.drhj.order.entity
configuration:
map-underscore-to-camel-case: true #驼峰命名
logging: #打印日志
level:
com.drhj.order.mapper: debug
ribbon:
MaxAutoRetriesNextServer: 0 #关闭重试 禁用Ribbon重试,防止对分布式事务有影响
4.配置bootstrap.yml
spring:
cloud:
inetutils:
preferred-networks:
- 10\.1\.6\..+
- 192\.168\.0\..+
5.创建order实体类
package com.drhj.order.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private Long userId; //用户id
private Long productId; //商品id
private Integer count; //购买的数量
private BigDecimal money; //花多少钱
private Integer status; //状态,0-冻结,1-正常
}
6.创建OrderMapper接口
package com.drhj.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.drhj.order.entity.Order;
public interface OrderMapper extends BaseMapper<Order> {
//创建订单
void create(Order order);
}
7.Mapper配置
先在 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.drhj.order.mapper.OrderMapper" >
<resultMap id="BaseResultMap" type="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 `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money},1);
</insert>
</mapper>
8.主程序添加 Mybatis 扫描注解
添加注解 @MapperScan(“com.drhj.order.mapper”) :
package com.drhj.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.drhj.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
9.添加 OrderService 接口和它的实现类
create() 方法实现保存订单的功能。
package com.drhj.order.service;
import com.drhj.order.entity.Order;
public interface OrderService {
//创建订单
void create(Order order);
}
package com.drhj.order.service;
import com.drhj.order.entity.Order;
import com.drhj.order.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void create(Order order) {
//远程调用发号器,生成订单id
//先临时随机产生id,加了发号器后,这行代码删除
Long id = Math.abs(new Random().nextLong());
order.setId(id);
orderMapper.create(order);
//远程调用库存,减少库存
//远程调用账户,扣减账户
}
}
10.添加 OrderController 类提供客户端访问接口
package com.drhj.order.controller;
import com.drhj.order.entity.Order;
import com.drhj.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/create")
public String create(Order order) {
orderService.create(order);
return "创建订单成功";
}
}
11.测试
1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看订单服务在 eureka 中的注册信:
2)访问订单服务,执行订单保存
访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100
3)查看控制台 Mybatis 执行的 sql 日志
4)查看数据库表,确认订单保存成功
九,全局唯一id发号器
分布式系统中,产生唯一流水号的服务系统俗称发号器。
有很多发号器开源项目,这里使用 EasyIdGenerator,具体项目信息请访问
https://github.com/lookingatstarts/easyIdGenerator
1.下载项目
访问https://github.com/lookingatstarts/easyIdGenerator,下载发号器项目。
解压放到我们父项目目录下,并修改名称
2.导入module
1)直接将pom.xml文件拖入idea中
2)修改pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.easy.id</groupId>
<artifactId>easy-id-generator</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>4.12</junit.version>
<mysql.connector.version>8.0.16</mysql.connector.version>
<com.alibaba.fastjson.version>1.2.62</com.alibaba.fastjson.version>
<lombok.version>1.18.8</lombok.version>
<curator.version>2.6.0</curator.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${com.alibaba.fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>${curator.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
3)右键 --> add as maven project
3.配置application.yml
因为我们自己创建了数据库,所以不使用他的数据库
server:
port: 9090
easy-id-generator:
snowflake: #雪花算法
enable: false #关闭
zk:
connection-string: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
load-worker-id-from-file-when-zk-down: true # 当zk不可访问时,从本地文件中读取之前备份的workerId
segment: #使用数据库产生id
enable: true
db-list: "seata_order" #["db1","db2"]
fetch-segment-retry-times: 3 # 从数据库获取号段失败重试次数
spring:
application:
name: easy-id
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
4.配置seata_order.properties
jdbcUrl=jdbc:mysql:///seata_order?serverTimezone=GMT%2B8&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
driverClassName=com.mysql.cj.jdbc.Driver
dataSource.user=root
dataSource.password=root
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
5.配置bootstrap.yml
spring:
cloud:
inetutils:
preferred-networks:
- 10\.1\.6\..+
- 192\.168\.0\..+
7.查看其使用数据库控制层方法
package com.easy.id.web;
import com.easy.id.config.Module;
import com.easy.id.service.EasyIdService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotEmpty;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author zhangbingbing
* @version 1.0.0
* @createTime 2020年05月29日
*/
@RestController
@RequestMapping("/segment/ids")
@Validated
@Module(value = "segment.enable")
public class SegmentEasyIdController {
//http://localhost:9090/segment/ids/next_id?businessType=order_business
@Autowired
@Qualifier("segmentEasyIdService")
private EasyIdService easyIdService;
@GetMapping("/next_id")
public String getNextId(@NotEmpty String businessType) {
return easyIdService.getNextId(businessType).toString();
}
@GetMapping("/next_id/batches")
public Set<String> getNextId(@RequestParam(value = "batches_size", defaultValue = "100") Integer batchSize,
@NotEmpty String businessType) {
return easyIdService.getNextIdBatch(businessType, batchSize).stream()
.map(Object::toString).collect(Collectors.toSet());
}
}
8.测试
1)启动服务
2)查看 eureka 中的注册信息
3)根据 SegmentEasyIdController 类的设置,访问下面地址获取自增id:
http://localhost:9090/segment/ids/next_id?businessType=order_business
谷歌一般看不到,可以右键 --> 查看
或者使用其他浏览器
十,远程调用配置
1.业务分析
当一个订单模块(order)生成时,首先系统得给他一个全局唯一的id,相应的,个人账户对应的资金也会减少,商品库存也会减少;
所以需要远程调用全局唯一id生成模块(easy-id-generator),账户模块(accout),库存模块(storage);
2.添加依赖
由于父项目中order-parent已经下载好对应的依赖,所以这里不用再进行添加。
3.启动项添加注解
因为是order项目调用其他项目,所以在order的启动项上添加@EnableFeignClients
package com.drhj.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@MapperScan("com.drhj.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
4.添加Feign声明式客户端接口
发号器的客户端接口:
package com.drhj.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "easy-id") //对应eureka注册表中名称,大小写不敏感
public interface EasyIdClient {
@GetMapping("/segment/ids/next_id")
String nextId(@RequestParam("businessType") String businessType);
}
账户服务的客户端接口:
package com.drhj.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(name = "account")
public interface AccountClient {
@GetMapping("/decrease")
String decrease(@RequestParam("userId") Long useId, @RequestParam("money") BigDecimal money);
}
库存服务的客户端接口:
package com.drhj.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "storage")
public interface StorageClient {
@GetMapping("/decrease")
String decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count);
}
5.在order业务代码中通过Feign客户端调用远程服务
package com.drhj.order.service;
import com.drhj.order.entity.Order;
import com.drhj.order.feign.AccountClient;
import com.drhj.order.feign.EasyIdClient;
import com.drhj.order.feign.StorageClient;
import com.drhj.order.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private EasyIdClient easyIdClient;
@Autowired
private AccountClient accountClient;
@Autowired
private StorageClient storageClient;
@Override
public void create(Order order) {
//远程调用发号器,生成订单id
String s = easyIdClient.nextId("order_business");
Long id = Long.valueOf(s);
order.setId(id);
orderMapper.create(order);
//远程调用库存,减少库存
storageClient.decrease(order.getProductId(),order.getCount());
//远程调用账户,扣减账户
accountClient.decrease(order.getUserId(),order.getMoney());
}
}
6.测试
启动所有项目,访问订单项目进行测试:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
1)查看执行结果
2)查看控制台日志
order
account
storage
3)查看三个数据库中的数据变化
order
account
storage
十一,备份
至此,项目搭建完毕。