安装Nacos
1.1预备环境准备
Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装 使用:
64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。
1.2下载源码或者安装包
你可以通过源码和发行包两种方式来获取 Nacos。
从 Github 上下载源码方式
git clone https://github.com/alibaba/nacos.git
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
ls -al distribution/target/
// change the $version to your actual path
cd distribution/target/nacos-server-$version/nacos/bin
下载编译后压缩包方式
您可以从 最新稳定版本 下载 nacos-server-$version.zip 包。
unzip nacos-server-$version.zip
或者
tar -xvf nacos-server-$version.tar.gz
cd nacos/bin
下载网站:https://github.com/alibaba/nacos/releases
1.3.启动服务器
这里使用windows版本演示,下载上述zip包,解压到全英文路径下
2.修改相关配置信息
进入到bin目录下,修改startup.cmd命令(直接使用记事本打开),将集群模式改为单机模式 set MODE="standalone"
进入conf文件找到nacos-mysql.sql导入到你的mysql数据库,注意版本是mysql5.6+,默认时间写法不同(2.1.x和2.0.x数据库表有变化)
进入conf文件找到application.properties,放开数据库配置连接的注释
上述修改完毕后,进入bin目录下,双击startup.cmd命令启动
使用nacos作为注册中心
1.创建SpringCloud项目,并导入依赖(同原来cloud项目)
pom.xml:
<!-- 注意这里不要使用一级目录com,因为启动类扫描到cloud下com包的类,造成异常 -->
<groupId>com.cssl</groupId>
<artifactId>nacos-provider7011</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nacos-provider7011</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
<spring-cloud-alibaba.version>2.3.7.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 消费者 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</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>
<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>
yml:
server:
port: 7011
spring:
application:
name: nacos-provider #注册到注册中心的服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#注册nacos和sentinel可以不加
management:
endpoints:
web:
exposure:
include: "*"
开放接口后检查服务:http://127.0.0.1:7011/actuator/nacos-discovery
注解:springboot启动类上加上 @EnableDiscoveryClient
2020版本以后openfeign已经移除了Ribbon也没有集成LoadBalancer
<!-- 默认轮询 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
Spring Cloud 组件之OpenFeign
<!--提供者不需要导OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
建议专门建立一个模块
1.消费者的启动类(写在内部就不需要引用模块的依赖(写出独立的工程就要需要引用))
@EnableFeignClients //使用fegin(basePackages="默认本包及子孙包")
可以在消费者(内部使用的话就只能自己使用)或者公共模块(步骤新建springboot项目Spring Cloud Routing-->openfeign-->消费者导入openfeign模块(所有人都可以使用))中创建,注意该接口应该和提供者的controller里面的请求保持一致
//用了fallback就不能再加@RequestMapping("/userprovider")
//@RequestMapping("/userprovider") //通过path属性可以达到同样效果
@FeignClient(name="PROVIDER", //服务名(就是spring:application:name: user-provider取的名字)
fallback = UsersFeginServiceImpl.class,//降级实现类
contextId = "uprovider", //区分服务上下文id
path="/userprovider") //路径前缀
public interface UsersFeignService {
//这些接口注意:要和提供者controller保持一致
@GetMapping("/login")
public Users login(@RequestParam(name="uname") String uname);
@RequestMapping("/showAll")
public List<Users> showAll();
/*@PostMapping("/addUsers")
public int addUsers(@RequestBody Users users);
*/
@GetMapping("/addUsers")
public int addUsers(@SpringQueryMap Users users);
}
2.注意:
1、传普通值:Feign接口参数前加:@RequestParam(name或value必须定义,并且等于被调控制器参数名),控制器都可以不加,或者Feign和提供者使用@PathVariable(name),否则启动报错。 2、传Map:提供者控制器、消费者控制器、Feign接口参数前都加:@RequestParam,或者提供者参数前加@RequestBody,Feign参数前可加可不加@RequestBody,可以用GET请求(最好POST) 3、传对象:提供者和Feign接口参数前加:@RequestBody,消费者控制器如果是Ajax请求也必须加:@RequestBody,不加启动不报错,但值都为空!
或者直接在Feign接口参数前加:@SpringQueryMap,消费者和提供者都不加注解。
4、注意:传对象使用@RequestBody提供者要使用POST请求,因为原生HttpURLConnection发现只要是对象,就会强制的把GET请求转换成POST请求,如果使用@SpringQueryMap就无所谓Get或Post
提供者用@RequestBody又使用get除非替换HttpURLConnection
然后配置:feign.httpclient.enabled = true
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.2.3</version>
</dependency>
3.上传文件
可以直接在消费者保存,如果想通过rest传递到提供者,spring-cloud-starter-openfeign 在2.0.2.RELEASE版本后,已经集成了feign-form-spring 以及 feign-form,因此此版本以后不需再额外添加依赖包,也不再需要添加 MultipartSupportConfig 配置
//调用方:
@PostMapping(value="/upload")
public String upload(MultipartFile files, Users user){ ... }
//Feign客户端:
@PostMapping(value="/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestPart MultipartFile file,@RequestParam String username);//不能使用@RequestBody传值
//被调用方:
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file,String username){
System.out.println(file.getContentType()+":"+file.getOriginalFilename());
try {
file.transferTo(new File("e:/"+file.getOriginalFilename()));
} catch (IOException e) {
return "error";
}
return "success";
}
//注意:被调用的服务用@RequestParam,不是@RequestPart
//不建议把文件从消费者传到提供者,建议直接保存到消费者传文件路径到提供者保存数据库
1、网页测试返回xml解决:produces = {"application/json;charset=UTF-8"}
2、微服务中如果加了context-path,@FeignClient要加path属性映射,否则404
3、注册中心(Eureka)集群要先配置虚拟域名
4.服务熔断降级Sentinel
其他作者链接: https://blog.csdn.net/Leon_Jinhai_Sun/article/details/126070471
5.网关Gateway
1.概念
Springcloud Gateway旨在提供一种简单而有效的方法来路由到api,并为它们提供跨领域的关注点,例如:安全性、监视/度量和恢复能力
2.特征
Spring云网关功能:
能够匹配任何请求属性上的路由
谓词和筛选器特定于路由
断路器集成
易于编写谓词和过滤器
请求速率限制
路径重写
3.工作原理
客户端向Spring云网关发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关Web处理程序。此处理程序通过特定于请求的筛选器链运行请求。过滤器被虚线分割的原因是,过滤器可以在代理请求发送之前和之后运行逻辑。执行所有“预”过滤器逻辑。然后发出代理请求。在发出代理请求之后,运行“post”过滤器逻辑。
4.项目pom.xml文件
网关不能导web包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 网关响应json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
由于springcloud2020弃用了Ribbon,因此Alibaba在2021版本nacos中删除了Ribbon的jar包,因此无法通过lb路由到指定微服务,出现了503情况。
所以只需要引入springcloud loadbalancer包即可
<!-- 默认轮询 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
5.application.yml文件
server:
port: 9001
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#跨域
globalcors:
cors-configurations:
'[/**]':
allowCredentials: true
allowedOrigins: '*'
allowedHeaders: '*'
allowedMethods: '*'
maxAge: 3600
#网关
gateway:
httpclient:
connect-timeout: 3000 # 全局网关超时
response-timeout: 3s
discovery:
locator:
enabled: true # 开启默认网关规则http://网关ip:port/服务名/请求路径
lower-case-service-id: true # 服务名全小写也可以找到
routes:
- id: nacos-consumer # 唯一标识,通常使用服务id
uri: lb://nacos-consumer # lb获取服务,lb是Load Balance的缩写(2020+引用loadbalancer包)
predicates: # 谓词即路由匹配规则(十几种规则)
- Path=/consumer/** # 匹配转发路径,该服务所有请求加/consumer才能匹配(http://网关ip:port/consumer/路径,也就意味着被访问服务控制器必须有/consumer,也可以通过filters修改不加/consumer),zuul设置路径映射服务控制器是不用加/consumer,注意**个数问题
#-id和uri和Path名字一样的话就不会生效,要生效的话path就不要和它们一样
#断言 没有遵守的话就不转发请求 https://blog.csdn.net/weixin_43839681/article/details/106160813
#- After=2021-05-13T15:41:01.223+08:00[GMT+08:00]
#- Header=token,abc\d+ # 第一个是参数名,第二个是参数值(正则)
#- Method=GET,POST
#- Query=name,sb\w+ # 第一个是参数名,第二个是参数值(正则)
filters: #过滤器:添加请求头请求参数值等等内置路由过滤器
- name: StripPrefix #标准写法,等同下面StripPrefix=1
args:
parts: 1
#- StripPrefix=1 #删除路径中的几节前缀(这里就会删掉/consumer)
- AddRequestHeader=X-Request-color, blue
- AddRequestParameter=color, pink
- id: nacos-provider
uri: lb://nacos-provider #也可以写http:/ip:7011(不能负载均衡)
predicates:
- Path=/provider/** # 匹配转发路径,注意和上面路径区分开(如果两个服务路径相同会死循环)
- Method=GET,POST # 匹配请求方法
filters:
- StripPrefix=1
- name: Hystrix # 内置容灾过滤器
args:
name: fallback # 容灾group key
fallbackUri: forward:/fallback # 服务不可用转发本地控制器
断言:https://blog.csdn.net/weixin_43839681/article/details/106160813
//@RequestMapping("/provider") //不能使用fallback,使用path映射
//path="provider"映射网关路由的Path=/provider/**(`/`可加可不加)
//网关配置了StripPrefix=1只是对应服务控制器不需要加/provider,path还是要有,也就是path和StripPrefix没有关系
@FeignClient(name="gateway",path = "provider")
@FeignClient(name="gateway",path = "/provider",contextId = "provider",fallback = UsersClientImpl.class)
public interface UsersClient {
@GetMapping("/check")
public boolean check(@RequestParam(name="username") String username);
@PostMapping("/login")
public Users login(@RequestBody Users user);
@PostMapping("/map")
public Map<String,Object> map(@RequestParam Map<String,Object> map);
}
6.过滤器
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
GatewayFilter : 路由过滤器,框架内置的路由器都属于此类,需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上。
GlobalFilter : 全局过滤器,不需要在配置文件中配置(只需要spring容器加载),作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,系统初始化时加载,并作用在每个路由上。
6.分布式Session
@EnableRedisHttpSession
不需要配置Hystrix了,网关也不需要设置请求头也不会丢失header。
配置:
<!-- redis存储session -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 分布式session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
yml:
spring:
redis:
timeout: 10s
jedis:
pool:
max-idle: 50
min-idle: 10
max-wait: -1s
max-active: -1
项目添加cookie处理拦截器和原来一样:(该类不能在Gateway项目中使用,和Netty服务相冲突,也就是HttpServletRequest拦截不到请求)
@Configuration
public class FeignHeaderConfiguration {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
//通过RequestContextHolder获取本地请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}
//获取本地线程绑定的请求对象
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
//给请求模板附加本地线程头部信息,主要是cookie信息
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement().toString();
requestTemplate.header(name, request.getHeader(name));
}
if(!request.isRequestedSessionIdValid()){
request.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR,null);
requestTemplate.header("cookie","SESSION="+request.getSession().getId());
System.out.println("apply sessionId:"+request.getSession().getId());
}
};
}
}
以上代码不加网关可以正常传递session
加网关后重定向sessionId不一致,则配置网关:
#解决重定向问题
spring:
gateway:
filter:
add-request-header:
enabled: true # 默认true,不用配置
add-response-header:
enabled: true # 默认true
(老一点的版本)注意: 要使第一次session也相同,必须使用重定向,加网关不能使用 127.0.0.1或localhost 必须使用实际ip(http://192.168.111.1)地址访问(浏览器或postman里面地址),因为重定向后地址会变为实际ip地址访问,所以一开始就使用该ip访问才可以:http://192.168.111.1:8011/login,重定向直接走消费者或网关都可以:redirect:http://192.168.111.1:8011/login || redirect:login || redirect:http://192.168.111.1:9011/consumer/session(新版本使用本地ip)
7.Nacos 配置中心
可以参考:https://zhuanlan.zhihu.com/p/144194572
8.分布式事务
单体应用
单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务来保证。
微服务应用
随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
小结
在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,一次业务操作需要操作多个数据源或需要进行远程调用,就会产生分布式事务问题。
Seata简介(Simple Extensible Autonomous Transaction Architecture)
Seata (简单可扩展自治事务框架,简化版GTS)是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
事务模式:https://www.jianshu.com/p/e5dc4d07e24e
http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
http://seata.io/zh-cn/blog/seata-xa-introduce.html
Seata原理和设计
定义一个分布式事务
我们可以把一个分布式事务理解成一个包含了若干分支事务的全局事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足ACID的本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的。
协议分布式事务处理过程的三个组件
1、Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
2、Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
3、Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个典型的分布式事务过程
1、TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
2、XID 在微服务调用链路的上下文中传播;
3、RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
4、TM 向 TC 发起针对 XID 的全局提交或回滚决议;
5、TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata实现步骤
1.官网下载
seata源码:https://github.com/seata/seata/ | https://gitee.com/seata-io/seata/ (只有源码)
创建seate库:
(1.0以下自带,新版https://github.com/seata/seata/tree/develop/script/server/db下载)
或者去码云下载:https://gitee.com/seata-io/seata/tree/develop/script/server/db
-- -------------------------------- 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(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;
下载seata压缩包
https://gitee.com/seata-io/seata/releases
https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip
2.修改seata配置
seata1.6.1版本的配置:https://blog.csdn.net/qq_42263280/article/details/128743510
seata1.5+开始配置application.yml
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
username: nacos
password: nacos
context-path: /nacos
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
cluster: default
username: nacos
password: nacos
context-path: /nacos
store:
# support: file 、 db 、 redis
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT
user: root
password: 1234
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
1.5seata以下的配置
1.file.conf
service {
#对应 application.yml 和 file.conf 配置
vgroup_mapping.tx_group = "bdqn_tx_group"
default.grouplist = "127.0.0.1:8091"
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
#datasource = "dbcp"
datasource = "druid"
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql:///globalsession"
user = "root"
password = "1234"
min-conn = 1
max-conn = 10
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
2.registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "serverAddr" #这里必须用serverAddr
serverAddr = "127.0.0.1:8848"
namespace = "public"
cluster = "default"
group = "DEFAULT_GROUP"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
group = "DEFAULT_GROUP"
username = "nacos"
password = "nacos"
}
}
3.启动服务
1.先启动nacos后启动seata
4.项目中依赖seata配置
<project>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Seata依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
</dependencies>
</project>
5.项目application.yml配置
# 1.0新添加的enabled激活自动配置,使得我们可以在yaml/properties文件中配置,
# 避免了以前需要客户端引入 registry.conf
seata:
enabled: true # 1.0+新特性,默认true
tx-service-group: bdqn_group #事务组编号,对应seata下file.conf
client:
support:
spring:
datasource-autoproxy: true #{datasource="druid"}
#datasource-autoproxy: false #排除自动注入,自定义数据源
service:
vgroup-mapping:
bdqn_group: default #key和上面tx-service-group对应,配置错误出异常NettyClientChannelManager: no available service ‘null’ found, please make sure registry config correct
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false #bug,配了没用需要配file.conf
server:
port: 8111
spring:
application:
name: seata-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///seata_order?serverTimezone=GMT
username: root
password: ROOT
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
type-aliases-package: com.cssl.pojo
mapper-locations: classpath:mapper/*.xml
configuration:
auto-mapping-behavior: full
use-generated-keys: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#或者
mybatis-plus:
type-aliases-package: com.cssl.pojo
configuration:
auto-mapping-behavior: full
use-generated-keys: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: false
global-config:
db-config:
id-type: auto #主键自增
logic-delete-value: 1
logic-not-delete-value: 0
management:
endpoints:
web:
exposure:
include: "*"
//注意:alibaba2020+必须配置该注解才能让代理数据源生效
@EnableAutoDataSourceProxy
@EnableDiscoveryClient
@MapperScan("com.cssl.dao")
@EnableFeignClients
@SpringBootApplication
public class SeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderApplication.class, args);
}
}
注意:在使用service属性时,存在1个问题,关于disableGlobalTransaction和disable-global-transaction都无法生效的问题。
报错: [WARN ] io.seata.config.FileConfiguration [266] - Could not found property service.disableGlobalTransaction, try to use default value instead
解决:https://github.com/seata/seata/issues/2114
增加一个file.conf,添加以下内容,在启动时则没有该错误(注意缩进4个字符)
#bdqn_group对应seata下file.conf
service {
vgroupMapping.bdqn_group = "default"
disableGlobalTransaction = false
}
Seata 2.2.2+以上报:
为 file.conf 没有或配置不正确报No available service
3、新版本JDK8+启动微服务报
Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @49438269 这是由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了,为了兼容原先的版本,需要在运行项目时添加 --add-opens java.base/java.lang=ALL-UNNAMED 选项来开启这种默认不被允许的行为。
如果是通过 IDEA 来运行项目,那么可以在 “Edit Configurations” 中 ——> “VM options” 输入框中输入该选项来完成,最终结果如下图所示:
seata/conf/file.conf | 项目/application.yml | 项目/file.conf
R:/127.0.0.1:8091]) will closed
io.seata.core.exception.TmTransactionException: RPC timeout
解决:启动微服务后在Seata窗口按下回车!
6.启动类配置和数据源配置类
@MapperScan("com.cssl.dao")
@EnableDiscoveryClient
@EnableFeignClients
//不使用默认数据源而使用自定义数据源,不是必须
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class NacosSeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(NacosSeataOrderApplication.class, args);
}
}
mybatis
//自定义数据源配置类(阿里数据源)
@Configuration
public class DataSourceConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
//在同样的DataSource中,首先使用被标注的DataSource
@Primary
@Bean
public DataSourceProxy dataSourceProxy(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
mybatis-plus
@Configuration
public class DataSourceConfig {
/**
* 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
* 原生datasource前缀取"spring.datasource"
*
* @return
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DruidDataSource druidDataSource(){
return new DruidDataSource();
}
/**
* 构造datasource代理对象,替换原来的datasource
*
* @param druidDataSource
* @return
*/
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
Seata使用代理数据源:http://seata.io/zh-cn/docs/overview/faq.html#1
如果 file.conf配置 db {datasource = "druid"}可以不使用自定义了
7.需要使用全局事务的类
在需要使用全局事务的方法上加上注解@GlobalTransactional
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountClient client;
@GlobalTransactional
@Override
public boolean saveOrder(Order order) {
log.info("====开始新增订单====");
order.setOid(UUID.randomUUID().toString());
int c1 = orderDao.insertOrder(order);
log.info("====结束新增订单====");
log.info("====开始付款修改账户====");
int c2 = client.update(order.getMoney(),order.getUser_id());
log.info("====结束付款修改账户====");
//int i=1/0;
log.info("====开始修改订单====");
int c3 = orderDao.updateOrder(order.getOid());
log.info("====结束修改订单====");
if(c1>=1 && c3>=1 &&c2>=1){
return true;
}
return false;
}
}
本案例中seata服务器相当于TC,标记了@GlobalTransactional的方法即为TM,TM向TC发起全局事务的请求,OrderDao和AccountClient对数据库进行操作,相当于是RM。
分布式事务中的DML中带有子查询会报:SQLInSubQueryExpr 可以改为先查询再将查询结果传递给DML语句
8.项目测试的数据sql
客户端回滚日志表:https://github.com/seata/seata/tree/develop/script/client/at/db
CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata_order` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `seata_order`;
/*Table structure for table `tbl_order` */
DROP TABLE IF EXISTS `tbl_order`;
CREATE TABLE `tbl_order` (
`oid` varchar(50) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`money` int(11) DEFAULT NULL,
`status` int(11) DEFAULT NULL,
PRIMARY KEY (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `tbl_order` */
insert into `tbl_order`(`oid`,`user_id`,`money`,`status`) values ('1a5fa36a-ad82-4344-b6a9-54ebae221dd1',1,100,1)
/*Table structure for table `undo_log` */
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`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 NOT NULL COMMENT 'create datetime',
`log_modified` datetime NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata_account` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `seata_account`;
/*Table structure for table `tbl_account` */
DROP TABLE IF EXISTS `tbl_account`;
CREATE TABLE `tbl_account` (
`aid` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL DEFAULT '0',
`total` int(11) NOT NULL DEFAULT '0',
`used` int(11) NOT NULL DEFAULT '0',
`residue` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`aid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*Data for the table `tbl_account` */
insert into `tbl_account`(`aid`,`user_id`,`total`,`used`,`residue`) values (1,1,1000,0,1000);
/*Table structure for table `undo_log` */
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`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 NOT NULL COMMENT 'create datetime',
`log_modified` datetime NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
9.使用 AT 模式需要的注意事项:
必须使用代理数据源,有 3 种形式可以代理数据源:
依赖 seata-spring-boot-starter 时,自动代理数据源,无需额外处理。
依赖 seata-all 时,使用 @EnableAutoDataSourceProxy (since 1.1.0) 注解,注解参数可选择 jdk 代理或者 cglib 代理。
依赖 seata-all 时,也可以手动使用 DatasourceProxy 来包装 DataSource。
配置 GlobalTransactionScanner,使用 seata-all 时需要手动配置,使用 seata-spring-boot-starter 时无需额外处理。
业务表中必须包含单列主键,若存在复合主键,暂时只支持mysql,其他类型数据库建议先建一列自增id主键,原复合主键改为唯一键来规避下。
每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。
跨微服务链路的事务需要对相应 RPC 框架支持,目前 seata-all 中已经支持:Apache Dubbo、Alibaba Dubbo、sofa-RPC、Motan、gRpc、httpClient,对于 Spring Cloud 的支持,请大家引用 spring-cloud-alibaba-seata。其他自研框架、异步模型、消息消费事务模型请结合 API 自行支持。
目前AT模式支持的数据库有:MySQL、Oracle、PostgreSQL和 TiDB。
使用注解开启分布式事务时,若服务 provider 端加入 consumer 端的事务,provider 可不标注注解。但是,provider 同样需要相应的依赖和配置,仅可省略注解。
使用注解开启分布式事务时,若要求事务回滚,必须将异常抛出到事务的发起方,被事务发起方的 @GlobalTransactional 注解感知到。provide 直接抛出异常 或 定义错误码由 consumer 判断再抛出异常。
9.分布式锁
基于redis
加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁
/***
* 加锁的方法
* @return
*/
public boolean lock(String key,Long expire){
RedisConnection redisConnection=redisTemplate.getConnectionFactory().getConnection();
//设置序列化方法
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
if(redisConnection.setNX(key.getBytes(),new byte[]{1})){
redisTemplate.expire(key,expire,TimeUnit.SECONDS);
redisConnection.close();
return true;
}else{
redisConnection.close();
return false;
}
}
解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁
/***
* 解锁的方法
* @param key
*/
public void unLock(String key){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.delete(key);
}
3.redis加锁整体代码
package com.cssl.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
@Resource(name = "stringRedisTemplate")
ValueOperations<String, String> valOpsStr;
@Resource(name = "redisTemplate")
ValueOperations<Object, Object> valOpsObj;
/**
* 递增
* @param key
* @return
*/
public long incr(String key){
return valOpsStr.increment(key); //increment(key,long)
}
/**
* 递减
* @param key
* @return
*/
public long decr(String key){
return valOpsStr.decrement(key); //decrement(key,long);
}
/**
* 根据指定key获取String
* @param key
* @return
*/
public String getStr(String key){
return valOpsStr.get(key);
}
/**
* 设置Str缓存
* @param key
* @param val
*/
public void setStr(String key, String val){
valOpsStr.set(key,val);
}
/***
* 设置Str缓存
* @param key
* @param val
* @param expire 超时时间
*/
public void setStr(String key, String val,Long expire){
valOpsStr.set(key,val,expire, TimeUnit.MINUTES);
}
/**
* 删除指定key
* @param key
*/
public void del(String key){
stringRedisTemplate.delete(key);
}
/**
* 根据指定o获取Object
* @param o
* @return
*/
public Object getObj(Object o){
return valOpsObj.get(o);
}
/**
* 设置obj缓存
* @param o1
* @param o2
*/
public void setObj(Object o1, Object o2){
valOpsObj.set(o1, o2);
}
/**
* 删除Obj缓存
* @param o
*/
public void delObj(Object o){
redisTemplate.delete(o);
}
/***
* 加锁的方法
* @return
*/
public boolean lock(String key,Long expire){
RedisConnection redisConnection=redisTemplate.getConnectionFactory().getConnection();
//设置序列化方法
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
if(redisConnection.setNX(key.getBytes(),new byte[]{1})){
redisTemplate.expire(key,expire,TimeUnit.SECONDS);
redisConnection.close();
return true;
}else{
redisConnection.close();
return false;
}
}
/***
* 解锁的方法
* @param key
*/
public void unLock(String key){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.delete(key);
}
}