文章目录
微服务示例
用例
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
架构图
父工程搭建
创建一个SpringBoot工程,只留下pom.xml文件,并把依赖删除。
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.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>vip.buddha</groupId>
<artifactId>shopping</artifactId>
<version>1.0.0</version>
<name>shopping</name>
<description>shopping project</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
子工程搭建
根据用例创建仓储服务、订单服务、账户服务
storage服务
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">
<parent>
<artifactId>shopping</artifactId>
<groupId>vip.buddha</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shopping-storage7001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
application.yml文件
server:
port: 7001
spring:
application:
name: shopping-storage
StorageMain7001主启动类
package vip.buddha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StorageMain7001 {
public static void main(String[] args) {
SpringApplication.run(StorageMain7001.class, args);
}
}
StorageController服务接口
package vip.buddha.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/storage")
public class StorageController {
@RequestMapping("list")
public String list() {
return "this is storage list api";
}
}
order服务
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">
<parent>
<artifactId>shopping</artifactId>
<groupId>vip.buddha</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shopping-order8001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
application.yml文件
server:
port: 8001
spring:
application:
name: shopping-order
OrderMain8001主启动类
package vip.buddha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderMain8001 {
public static void main(String[] args) {
SpringApplication.run(OrderMain8001.class, args);
}
}
OrderController服务接口
package vip.buddha.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@RequestMapping("list")
public String list() {
return "this is order list api";
}
}
account服务
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">
<parent>
<artifactId>shopping</artifactId>
<groupId>vip.buddha</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shopping-account9001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
application.yml文件
server:
port: 9001
spring:
application:
name: shopping-account
AccountMain9001主启动类
package vip.buddha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AccountMain9001 {
public static void main(String[] args) {
SpringApplication.run(AccountMain9001.class, args);
}
}
AccountController服务接口
package vip.buddha.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/account")
@RestController
public class AccountController {
@RequestMapping("list")
public String list() {
return "this is account list api";
}
}
三个服务已经建好,传统的做法就是订单服务直接调用仓储服务和账户服务。但是直接调用存在的问题,所以得把服务放到注册中心去。
Nacos服务端搭建
Nacos服务端扮演的就是服务注册中心、服务提供者、服务消费者中的服务注册中心。
nacos挂网:https://nacos.io/zh-cn/
nacos代码仓库:https://github.com/alibaba/nacos
微服务技术整合,各技术存在不同版本问题。根据alibaba/spring-cloud-alibaba版本说明,选择了比较新的技术版本
SpringCloudAlibaba | SpringCloud | SpringBoot | Nacos | Seata | Sentinel |
---|---|---|---|---|---|
2021.0.1.0 | 2021.0.1 | 2.7.3 | 1.4.2 | 1.4.2 | 1.8.3 |
下载解压后,如上图所示。cmd进入bin目录,然后执行命令:startup.cmd -m standalone
出现上图,说明nacos服务端已启动,根据其网址进行登录,账号和密码默认都是nacos
服务注册
A调用B,B就是A的服务提供者,得把B服务注册到注册中心;B调用A,A就是B的服务提供者,得把A服务注册到注册中心。
要把服务注册到注册中心,服务就集成了nacos的客户端,同时应该有一个配置连接到Nacos的服务端。
父pom.xml文件加入如下内容:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
子application.yml配置里添加配置内容:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
启动服务后,服务提供者都注册进注册中心
服务调用
用户购买商品的业务逻辑,就是订单服务调用账户服务和仓储服务。
父pom.xml加入如下内容:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
服务消费者,也就是shopping-order8001的pom.xml加入如下内容:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
新增ServiceConfig配置类
package vip.buddha.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ServiceConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
OrderController改成如下:
package vip.buddha.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private RestTemplate restTemplate;
private final String ACCOUNT_URL = "http://shopping-account";
private final String STORAGE_URL = "http://shopping-storage";
@RequestMapping("list")
public String list() {
String s1 = restTemplate.getForObject(ACCOUNT_URL + "/account/list", String.class);
String s2 = restTemplate.getForObject(STORAGE_URL + "/storage/list", String.class);
System.out.println(s1 + "," + s2);
return "this is order list api";
}
}
启动服务后,实现了远程调用
服务提供者负载均衡
A调用B,B服务处理业务逻辑耗时,则完成整个请求的瓶颈在B服务。
在这里新增一个仓储服务,实现仓储服务的负载均衡。
配置中心
在开发运维过程中,存在多个环境,开发环境、测试环境、验收环境及生产环境,会连接不同的mysql、redis等。
父pom.xml添加以下内容:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- bootstrap 集成配置中心需要用bootstrap 配置文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
子工程application.yml改成bootstrap.yml,内容都改成如下:
server:
port: xxxx
spring:
profiles:
active: dev
application:
name: shopping-xxxx
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: e122716d-7502-4505-9472-c73900e4aefd
group: dev
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
refresh-enabled: true
namespace: e122716d-7502-4505-9472-c73900e4aefd
group: dev
重写服务调用
上面有实现用户购买商品的业务逻辑,就是订单服务调用账户服务和仓储服务。但是代码并不优雅,后期如果有修改,可能存在到一个个接口去改。
可以把上面实现服务调用的所有代码去掉,这里重新实现。
父pom.xml加入如下内容:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
服务消费者,也就是shopping-order8001的pom.xml加入如下内容:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
OrderMain8001主启动类添加注解@EnableFeignClients
AccountService接口代码:
package vip.buddha.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
@FeignClient(value = "shopping-account")
public interface AccountService {
@RequestMapping("/account/list")
public String list();
}
StorageService接口代码:
package vip.buddha.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
@FeignClient(value = "shopping-storage")
public interface StorageService {
@RequestMapping("/storage/list")
public String list();
}
OrderController接口代码:
package vip.buddha.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.buddha.feign.AccountService;
import vip.buddha.feign.StorageService;
import javax.annotation.Resource;
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@RequestMapping("list")
public String list() {
String s1 = accountService.list();
String s2 = storageService.list();
System.out.println(s1 + "," + s2);
return "this is order list api";
}
}
依然实现了服务调用,代码更加符合面向接口编程
路由网关
模块化后,订单服务、账户服务、仓储服务三个服务三个域名和三个端口,更多服务就更多个性化的东西。单前端来对接,就比较麻烦。
新建shopping-gateway8080子工程
pom.xml文件
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- SpringCloud loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
动态路由规则uri:lb://服务名不生效,但是uri://https://www.baidu.com能够生效,说明缺少负载均衡依赖spring-cloud-loadbalancer
bootstrap.yml文件
server:
port: 8080
spring:
profiles:
active: dev
application:
name: shopping-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: e122716d-7502-4505-9472-c73900e4aefd
group: dev
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
refresh-enabled: true
namespace: e122716d-7502-4505-9472-c73900e4aefd
group: dev
GatewayMain8080主启动类
package vip.buddha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayMain8080 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain8080.class, args);
}
}
启动后,得在服务列表里找到这个网关服务,否则无法转发其它服务
新增shopping-gateway-dev.yaml配置文件,把路由网关配置写到这
spring:
cloud:
gateway:
routes:
- id: shopping-account
uri: lb://shopping-account
#uri: https://www.baidu.com
predicates:
- Path=/account/**
filters:
- StripPrefix=1
- id: shopping-order
uri: lb://shopping-order
#uri: http://127.0.0.1:8001
predicates:
- Path=/order/**
filters:
- StripPrefix=1
- id: shopping-storage
uri: lb://shopping-storage
#uri: https://www.baidu.com
predicates:
- Path=/storage/**
filters:
- StripPrefix=1
最终实现路由转发
服务熔断与限流
广州体育西路地铁下班高峰期,地铁把H口彻底给封上,G口留个口排队扫码进站。进去后也是层层关卡。这就是熔断和限流使用的表现。
网址:alibaba/Sentinel
java -jar sentinel-dashboard-1.8.3.jar --server.port=9500
账号和密码默认都是:sentinel,进来是下图的样子
以shopping-order服务为例
pom.xml文件加上以下内容:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
shopping-order-dev.yaml加上以下内容:
spring:
cloud:
sentinel:
transport:
dashboard: http://localhost:9500
访问url:http://localhost:8080/order/order/list,出现如下界面,说明sentinel整合成功!
定义全局降级方法和异常处理方法
CustomerBlockHandler文件代码:
package vip.buddha.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class CustomerBlockHandler {
public static String down_1(String type, String name, BlockException be) {
System.out.println(be.getMessage());
return "Sentinel 流控降级热点时调用的方法----111111---"+type+" "+name;
}
public static String down_2(String type, String name, BlockException be) {
System.out.println(be.getMessage());
return "Sentinel 流控降级热点时调用的方法----222222---" + type + " " + name;
}
}
CustomerFallBack文件代码:
package vip.buddha.handler;
import org.springframework.web.bind.annotation.RequestParam;
public class CustomerFallBack {
public static String fallback_1(@RequestParam(name = "type", required = false) String type,
@RequestParam(name = "name", required = false) String name) {
return "1111111111-------java代码运行时产生的异常调用,没有异常时该怎么运行,怎么运行 "+type+" "+name;
}
public static String fallback_2(@RequestParam(name = "type", required = false) String type,
@RequestParam(name = "name", required = false) String name) {
return "2222222222-------java代码运行时产生的异常调用,没有异常时该怎么运行,怎么运行 "+type+" "+name;
}
}
OrderController文件代码:
package vip.buddha.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import vip.buddha.feign.AccountService;
import vip.buddha.feign.StorageService;
import vip.buddha.handler.CustomerBlockHandler;
import vip.buddha.handler.CustomerFallBack;
import javax.annotation.Resource;
@RestController
public class OrderController {
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@SentinelResource(value = "list",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "down_1",
fallbackClass = CustomerFallBack.class,
fallback = "fallback_1")
@RequestMapping("/order/list")
public String list(@RequestParam(name = "type", required = false) String type,
@RequestParam(name = "name", required = false) String name) {
String s1 = accountService.list();
String s2 = storageService.list();
System.out.println(s1 + "," + s2);
// 模拟报错
//System.out.println(1 / 0);
return "this is order list api" + type + " " + name;
}
}
最后sentinel和Jmeter配合使用,达到接口限流和熔断效果
分布式事务
仓储服务、订单服务、账户服务,各自连不同的数据库,无法使用本地事务。seata是一款阿里开源的分布式事务解决方案。官方网址:https://seata.io/zh-cn,代码仓库网址:https://github.com/seata/seata/releases
搭建seata1.4.2服务端
解压后进入conf目录,备份file.conf和registry.conf配置
file.conf配置修改
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://localhost:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
## redis mode: single、sentinel
mode = "single"
## single mode property
single {
host = "127.0.0.1"
port = "6379"
}
## sentinel mode property
sentinel {
masterName = ""
## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
sentinelHosts = ""
}
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
registry.conf配置修改
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "dev"
namespace = "e122716d-7502-4505-9472-c73900e4aefd"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
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 = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "e122716d-7502-4505-9472-c73900e4aefd"
group = "dev"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
consul {
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
apollo {
appId = "seata-server"
## apolloConfigService will cover apolloMeta
apolloMeta = "http://192.168.1.204:8801"
apolloConfigService = "http://192.168.1.204:8080"
namespace = "application"
apolloAccesskeySecret = ""
cluster = "seata"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
nodePath = "/seata/seata.properties"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
新增seataServer.properties配置文件
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=true
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
# server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
# store
#model改为db
store.mode=db
store.lock.mode=file
store.session.mode=file
# store.publicKey=""
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
#修改数据驱动,这里是mysql8,使用mysql5的话请修改
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 改为上面创建的seata服务数据库
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT
# 改为自己的数据库用户名
store.db.user=root
# 改为自己的数据库密码
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
# store.redis.sentinel.masterName=""
# store.redis.sentinel.sentinelHosts=""
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
# store.redis.password=""
store.redis.queryLimit=100
# log
log.exceptionRate=100
# metrics
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
# service
# 自己命名一个vgroupMapping fsp_tx_group等下要用到 重中之重
service.vgroupMapping.shopping-tx-group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
# client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
新增数据库配置
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;
-- 分支表
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;
-- 锁定表
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
PRIMARY KEY (`lock_key`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
双击seata-server.bat,至此nacos服务端搭建完成
客户端使用seata1.4.2
准备工作
微服务整合seata客户端之前,完成微服务项目本身账户服务更改余额、仓储服务修改库存、订单服务调用账户服务、调用仓储服务、写入订单的功能。全局事务的引入,只是保证在异常情况出现时,能够保证数据一致。
三个微服务分别都引入下面三个依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
账户微服务
CREATE TABLE `account` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
`money` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '账户余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户表';
INSERT INTO `account` VALUES (1, 1, 1000);
INSERT INTO `account` VALUES (2, 2, 2000);
package vip.buddha.entity;
import lombok.Data;
@Data
public class Account {
private Integer id;
private Integer userId;
private Integer money;
}
package vip.buddha.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.buddha.entity.Account;
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}
package vip.buddha.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import vip.buddha.entity.Account;
import vip.buddha.mapper.AccountMapper;
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
public int updateByUserId(Integer userId, Account account) {
QueryWrapper<Account> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
Account one = accountMapper.selectOne(wrapper);
if (one.getMoney() < account.getMoney()) {
return 0;
}
Account a = new Account();
a.setId(one.getId());
a.setMoney(one.getMoney() - account.getMoney());
return accountMapper.updateById(a);
}
}
package vip.buddha.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vip.buddha.entity.Account;
import vip.buddha.service.AccountService;
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@PutMapping("/account/{userId}")
public Integer update(@PathVariable("userId") Integer userId, @RequestBody Account account) {
Integer o = accountService.updateByUserId(userId, account);
System.out.println(o);
return o;
}
}
shopping-account-dev.yaml配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
logging:
level:
root: info
vip.buddha.mapper: debug
仓储服务
CREATE TABLE `storage` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`goods_id` int(10) unsigned NOT NULL COMMENT '商品ID',
`num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '商品库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品库存';
INSERT INTO `storage` VALUES (1, 1, 1000);
INSERT INTO `storage` VALUES (2, 2, 2000);
package vip.buddha.entity;
import lombok.Data;
@Data
public class Storage {
private Integer id;
private Integer goodsId;
private Integer num;
}
package vip.buddha.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.buddha.entity.Storage;
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
}
package vip.buddha.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import vip.buddha.mapper.StorageMapper;
import vip.buddha.entity.Storage;
@Service
public class StorageService {
@Autowired
private StorageMapper storageMapper;
public Integer updateByGoodsId(Integer goodsId, Storage storage) {
QueryWrapper<Storage> wrapper = new QueryWrapper<>();
wrapper.eq("goods_id", goodsId);
Storage one = storageMapper.selectOne(wrapper);
if (one.getNum() < storage.getNum()) {
return 0;
}
Storage a = new Storage();
a.setId(one.getId());
a.setNum(one.getNum() - storage.getNum());
return storageMapper.updateById(a);
}
}
package vip.buddha.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vip.buddha.entity.Storage;
import vip.buddha.service.StorageService;
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@PutMapping("/storage/{goodsId}")
public Integer update(@PathVariable("goodsId") Integer goodsId, @RequestBody Storage storage) {
Integer i = storageService.updateByGoodsId(goodsId, storage);
return i;
}
}
shopping-storage-dev.yaml配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/storage?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
logging:
level:
root: info
vip.buddha.mapper: debug
订单服务
CREATE TABLE `orders` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`goods_id` int(10) unsigned NOT NULL COMMENT '商品ID',
`user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
`num` int(10) unsigned NOT NULL COMMENT '商品数量',
`money` int(10) unsigned NOT NULL COMMENT '总价',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
package vip.buddha.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
@Data
public class Orders implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer goodsId;
private Integer userId;
private Integer num;
private Integer money;
}
package vip.buddha.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.buddha.entity.Orders;
@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
}
package vip.buddha.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import vip.buddha.entity.Account;
@Component
@FeignClient(value = "shopping-account")
public interface AccountService {
@PutMapping("/account/{userId}")
Integer update(@PathVariable("userId") Integer userId, @RequestBody Account account);
}
package vip.buddha.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import vip.buddha.entity.Storage;
@Component
@FeignClient(value = "shopping-storage")
public interface StorageService {
@PutMapping("/storage/{goodsId}")
Integer update(@PathVariable("goodsId") Integer goodsId, @RequestBody Storage storage);
}
package vip.buddha.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import vip.buddha.entity.Account;
import vip.buddha.entity.Orders;
import vip.buddha.entity.Storage;
import vip.buddha.feign.AccountService;
import vip.buddha.feign.StorageService;
import vip.buddha.mapper.OrdersMapper;
@Service
public class OrdersService {
@Autowired
private OrdersMapper ordersMapper;
@Autowired
private AccountService accountService;
@Autowired
private StorageService storageService;
public int save(Orders orders) throws Exception {
// 1. 扣余额
Account a = new Account();
a.setMoney(orders.getMoney());
Integer i1 = accountService.update(orders.getUserId(), a);
if (i1 == 0) {
throw new Exception("扣余额失败");
}
// 2. 扣库存
Storage s = new Storage();
s.setNum(orders.getNum());
Integer i2 = storageService.update(orders.getGoodsId(), s);
if (i2 == 0) {
throw new Exception("扣库存失败");
}
// 3. 添加订单记录
int i = ordersMapper.insert(orders);
return i;
}
}
package vip.buddha.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vip.buddha.entity.Orders;
import vip.buddha.service.OrdersService;
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrdersService ordersService;
@PostMapping("/order")
public String save(@RequestBody Orders orders) throws Exception {
int i = ordersService.save(orders);
log.info(orders.toString());
log.info("-----------------");
log.info(" " + i);
return "ok";
}
}
Seata客户端的配置
在账户服务、仓储服务、订单服务数据库中创建undo_log表
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
在账户服务、仓储服务、订单服务pom文件引入依赖,依赖版本要和seata服务端版本一致
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 要与seata服务端版本一直,所以把自带的替换掉 -->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
在账户服务、仓储服务、订单服务的nacos配置文件中分别加入seata相关配置
seata:
enabled: true
enable-auto-data-source-proxy: true #是否开启数据源自动代理,默认为true
tx-service-group: shopping-tx-group #要与配置文件中的vgroupMapping一致
registry: #registry根据seata服务端的registry配置
type: nacos #默认为file
nacos:
application: seata-server #配置自己的seata服务
server-addr: localhost:8848 #根据自己的seata服务配置
username: nacos #根据自己的seata服务配置
password: nacos #根据自己的seata服务配置
namespace: e122716d-7502-4505-9472-c73900e4aefd #根据自己的seata服务配置
cluster: default # 配置自己的seata服务cluster, 默认为 default
group: dev #根据自己的seata服务配置
config:
type: nacos #默认file,如果使用file不配置下面的nacos,直接配置seata.service
nacos:
server-addr: localhost:8848 #配置自己的nacos地址
group: dev #配置自己的dev
username: nacos #配置自己的username
password: nacos #配置自己的password
namespace: e122716d-7502-4505-9472-c73900e4aefd #配置自己的namespace
dataId: seataServer.properties #配置自己的dataId,由于搭建服务端时把客户端的配置也写在了seataServer.properties,所以这里用了和服务端一样的配置文件,实际客户端和服务端的配置文件分离出来更好
在账户服务、仓储服务、订单服务的主启动类上加上@EnableAutoDataSourceProxy注解
在需要事务的方法上加上@GlobalTransactional注解,用法如图
至此以SpringCloud Alibaba组件Nacos、Sentinel、Seata的微服务框架搭建完毕,其中也用到SpringCloud的组件,比如网关SpringCloud Gateway、远程调用SpringCloud OpenFeign等。