近况
8,9月份学习了ES6,webpack,Vue的内容,10,11月份在学微服务的内容,不能说学的有多好,
但大致都了解了一下,很多名词也不再陌生,当然,很多技术仅仅学习了使用,具体原理什么的
我暂时没有深入,因为现在的时间不允许,我还有很多其他要紧的事,现在的学习是为以后学习做个铺垫。
在未来,我会弥补自身基础不足,这是道心上的坎,操作系统,计算机网络,计算机组成,数据结构和算法
这些我都不会放弃,我从始至终都认为重要,但我没有时间,我必须先找到一份工作,才有机会沉淀。
学习Java上,同样也有很多重要的内容没有完成,多线程,JVM之类的,Linux什么的都没有好好学习。
有人问我想走前端还是后端,坦白说我也不太清楚,不过我觉得国外程序员都是两者都学,没有像国内划分明显
所以,我大概也是两者都学习,既然自己实在太菜,深度不够,只有拿广度来凑了,多学点,总没有坏处。
本篇博客主要是对之前微服务的学习做一个总结,复习Feign,nacos,sentinel,gateway,seata内容,
当然,eureka,rabbitMQ,Ribbon,Hystrix都大致了解使用了下,本案例更多使用alibaba组件。
案例描述:三个微服务,订单服务,账户服务,库存服务,用户下订单,调用订单服务接口,
订单服务创建订单,并调用账户服务进行扣款,库存服务扣减库存,最后如果一切顺利,订单服务更改订单状态
表示完成该订单,如果另外两个微服务失败,则数据回滚,这是典型的分布式事务场景。
下面是本篇博客索引
案例框架搭建
创建父子模块
父子模块
父模块的POM文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>world.keyi</groupId> <artifactId>SpringCloudDemo</artifactId> <version>1.0-SNAPSHOT</version> <name>SpringCloudDemo</name> <modules> <module>ApiCommon</module> <module>OrderService</module> <module>AccountService</module> <module>StorageService</module> <module>GatewayService</module> </modules> <packaging>pom</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <junit.version>4.12</junit.version> <log4j.version>2.13.3</log4j.version> <lombok>1.18.10</lombok> <mysql.version>8.0.22</mysql.version> <druid.version>1.1.17</druid.version> <mybatis.spring.boot.version>2.1.0</mybatis.spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR9</version> <type>pom</type> <scope>import</scope> </dependency> <!-- alibaba依赖版本是2.2.6,但是nacos最高只有2.2.0,所以会报错spring-cloud-alibaba-nacos-config版本找不到 我们需要自己指定版本号 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-config</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok}</version> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.3.RELEASE</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
OrderService模块POM文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>SpringCloudDemo</artifactId> <groupId>world.keyi</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>OrderService</artifactId> <name>OrderService</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <!--去除jackson-dataformat-xml,否则降级方法会返回xml数据,而不是JSON数据--> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>world.keyi</groupId> <artifactId>ApiCommon</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.3.RELEASE</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
OrderService模块依赖相比AccountService模块,Storage模块只多了OpenFeign
所以另外两个模块依赖省略了ApiCommon模块POM文件
本模块用来存放公共类或工具类<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.1.0</version> </dependency> </dependencies>
Gateway模块POM文件
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies>
搭建三个业务数据库
OrderServie,AccountService,StorageService三个模块分别对应三个数据库,下面是建库建表语句#创建order表 CREATE DATABASE springcloud_order USE springcloud_order CREATE TABLE t_order( id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY , user_id BIGINT(11) DEFAULT NULL COMMENT '用户id', product_id BIGINT(11) DEFAULT NULL COMMENT '产品id', COUNT INT(11) DEFAULT NULL COMMENT '数量', money DECIMAL(11,0) DEFAULT NULL COMMENT '金额', STATUS INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结' )ENGINE=INNODB AUTO_INCREMENT=1 CHARSET=utf8; #创建account表 CREATE DATABASE springcloud_account USE springcloud_account CREATE TABLE t_account( id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY , user_id BIGINT(11) DEFAULT NULL COMMENT '用户id', total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度', used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度', residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度' )ENGINE=INNODB AUTO_INCREMENT=1 CHARSET=utf8; INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000); #创建storage表 CREATE DATABASE springcloud_storage USE springcloud_storage CREATE TABLE t_storage( id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY , product_id BIGINT(11) DEFAULT NULL COMMENT '产品id', total INT(11) DEFAULT NULL COMMENT '总库存', used INT(11) DEFAULT NULL COMMENT '已用库存', residue INT(11) DEFAULT NULL COMMENT '剩余库存' )ENGINE=INNODB AUTO_INCREMENT=1 CHARSET=utf8; INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
seata所需要的的数据库和表语句放在下面
配置三个微服务的配置文件
暂时先贴上三个和业务相关的配置文件,不引入任何微服务组件配置OrderService
server: port: 2346 spring: datasource: username: root password: 123765 url: jdbc:mysql://localhost:3306/springcloud_order?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: mapper/*.xml #在resources资源目录下创建mapper目录 configuration: map-underscore-to-camel-case: true
AccountService
server: port: 2345 spring: datasource: username: root password: 123765 url: jdbc:mysql://localhost:3306/springcloud_account?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: mapper/*.xml #在resources资源目录下创建mapper目录 configuration: map-underscore-to-camel-case: true
StorageService
server: port: 2347 spring: datasource: username: root password: 123765 url: jdbc:mysql://localhost:3306/springcloud_storage?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true
除了数据库URL和端口号不同,其他都一样。
增加业务
上面我们创建了父子模块,增加了依赖,配置了基本的配置文件,下面我们编写基本业务
OrderService微服务
Order
@Data @NoArgsConstructor @AllArgsConstructor public class Order { private Integer id; private String userId; //用户id private String productId; //产品id private String count; //数量 private String money; //金额 private String status; //订单状态:0创建中,1已完结 }
OrderController
//以上略 @RestController @RequestMapping("order") public class OrderController { @Resource private OrderService orderService; //CommonResult是ApiCommon服务公共类 @PostMapping("/") public CommonResult<Integer> create(@RequestBody Order order){ order.setStatus("0"); return new CommonResult<>(200,"success",orderService.create(order)); } }
OrderService及OrderServiceImpl
public interface OrderService { Integer create(Order order); }
@Service public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; /* * 案例主方法:增加订单--->扣减账户余额--->扣减库存--->更改订单状态 * */ @Override public Integer create(Order order) { orderDao.addOrder(order); /* 这里需要使用feign调用账户模块,库存模块 */ Integer result = orderDao.updateOrderStatus(order.getUserId(),"1"); return result; //若修改状态成功,则返回1,否则为0 } }
OrderDao及OrderMapper.xml
@Mapper public interface OrderDao { Integer addOrder(Order order); Integer updateOrderStatus(@Param("userId") String userId,@Param("status") String status); }
<?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="world.keyi.dao.OrderDao"> <insert id="addOrder" parameterType="world.keyi.bean.Order"> insert into t_order(user_id,product_id,count,money,status) values(${userId},${productId},${count},${money},${status}) </insert> <update id="updateOrderStatus"> update t_order set status=${status} where user_id=${userId} </update> </mapper>
AccountService微服务
Account
@Data @NoArgsConstructor @AllArgsConstructor public class Account { private Integer id; private String userId; //用户id private String total; //总额度 private String used; //已用额度 private String residue; //剩余可用额度 }
AccountController
@RestController @RequestMapping("/account") public class AccountController { @Resource private AccountService accountService; @RequestMapping("/list") public CommonResult<List<Account>> getAccountList(){ return new CommonResult<>(200, "success", accountService.getAccountList()); } @PutMapping("/") public CommonResult<Integer> reduceAccount( @RequestParam("userId") String userId, @RequestParam("money") String money){ if (accountService.reduceAccount(userId,money)!=1) { return new CommonResult<>(500,"error"); } return new CommonResult<>(200,"success"); } }
AccountService及AccountServiceImpl
public interface AccountService { List<Account> getAccountList(); Integer reduceAccount(String userId, String money); }
@Service public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Override public List<Account> getAccountList() { return accountDao.getAccountList(); } @Override public Integer reduceAccount(String userId, String money) { return accountDao.reduceAccount(userId,money); } }
AccountDao及AccountMapper.xml
@Mapper public interface AccountDao { public List<Account> getAccountList(); public Integer reduceAccount(@Param("userId") String userId,@Param("money") String money); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="world.keyi.dao.AccountDao"> <select id="getAccountList" resultType="world.keyi.bean.Account"> select * from t_account; </select> <update id="reduceAccount"> update t_account set residue=residue-${money},used=used+${money} where user_id=${userId} </update> </mapper>
StorageService微服务
Storage
@Data @NoArgsConstructor @AllArgsConstructor public class Storage { private Integer id; private String productId; //产品id private String total; //总库存 private String used; //已用库存 private String residue; //剩余库存 }
StorageController
@RestController @RequestMapping("/storage") public class StorageController { @Resource private StorageService storageService; @PutMapping("/") public CommonResult<Integer> reduceStorage( @RequestParam("productId") String productId, @RequestParam("count") String count){ return new CommonResult<>(200,"success",storageService.reduceStorage(productId,count)); } }
StorageService及StorageServiceImpl
public interface StorageService { Integer reduceStorage(String productId, String count); }
@Service public class StorageServiceImpl implements StorageService { @Resource private StorageDao storageDao; @Override public Integer reduceStorage(String productId, String count) { return storageDao.reduceStorage(productId,count); } }
StorageDao及StorageMapper.xml
@Mapper public interface StorageDao { Integer reduceStorage(@Param("productId") String productId,@Param("count") String count); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="world.keyi.dao.StorageDao"> <update id="reduceStorage"> update t_storage set used=used+${count},residue=residue-${count} where product_id=${productId} </update> </mapper>
增加nacos
导入依赖
<!--nacos服务注册--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--nacos热部署--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-config</artifactId> </dependency>
有一点你可能有疑问,为什么在父模块中需要额外nacos依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-config</artifactId> <version>2.2.0.RELEASE</version> </dependency>
我在配置seata的时候,百度到seata依赖:
spring-cloud-starter-alibaba-seata
最好是2.2.1及以上版本
所以我需要将:spring-cloud-alibaba-dependencies
调整到2.2.1版本,但是刷新maven依赖时报错
找不到nacos-config依赖,原因是nacos-config最高只有2.2.0版本,又考虑到alibaba,cloud,boot三者
需要保持一致,所以将alibaba依赖版本号更改成2.2.6并且额外配置nacos-config版本号为2.2.0.在微服务入口类上增加@EnableDiscoveryClient注解
在微服务配置文件中配置nacos
没使用nacos之前微服务配置文件是application.yaml,引入nacos后,为了实现热部署,需要将配置文件
改成bootstrap.yaml,它会先微服务启动时,去nacos控制台上拉取相应的配置信息。
下面是OrderService微服务的bootstrap.xml,另外两个微服务的naocs配置一样server: port: 2346 spring: datasource: username: root password: 123765 url: jdbc:mysql://localhost:3306/springcloud_order?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver application: name: OrderService profiles: active: dev cloud: nacos: discovery: server-addr: 47.98.138.53:8848 cluster-name: HZ namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138 config: server-addr: 47.98.138.53:8848 file-extension: yaml namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138
在nacos控制台配置中心创建好namespace和微服务配置文件
配置文件的dataId规则如下:#dataId=${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
下面是OrderService微服务在nacos上配置文件的内容,OrderService-dev.yamlinfo: 一只穿云箭
测试nacos上的配置文件,在OrderController类上增加@RefreshScope注解
下面是更新后的OrderController@RestController @RequestMapping("order") @RefreshScope public class OrderController { @Resource private OrderService orderService; @Value("${info}") private String info; @RequestMapping("/nacos") public String nacosTest(){ return info; } @PostMapping("/") public CommonResult<Integer> create(@RequestBody Order order){ order.setStatus("0"); return new CommonResult<>(200,"success",orderService.create(order)); } }
以上就是配置nacos的全部内容,其他两个微服务也是如此
增加feign
三个业务微服务中,是OrderService调用另外两个微服务,所以只需要在OrderService中配置feign就好
导入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
在微服务的入口类上增加@EnableFeignClients注解
在微服务配置文件中增加feign的配置
feign: client: config: default: #全局设置为default,可以填其他微服务名称 # ribbon的超时时间,默认等待1秒,得不到爆错,所以重新设置超时时间 connectTimeout: 5000 # 指的是建立连接所用的时间,适用于网路状况正常情况下,两端连接所用的时间 readTimeout: 5000 # 指的是建立连接后从服务器读取到可用资源所用的时间 loggerLevel: full #日志级别
创建另外两个微服务的feign接口
@FeignClient(value = "AccountService") @RequestMapping("/account") public interface AccountServiceFeign { @PutMapping("/") public CommonResult<Integer> reduceAccount( @RequestParam("userId") String userId, @RequestParam("money") String money ); }
@FeignClient(value = "StorageService") @RequestMapping("/storage") public interface StorageServiceFeign { @PutMapping("/") public CommonResult<Integer> reduceStorage( @RequestParam("productId") String productId, @RequestParam("count") String count ); }
在OrderServiceImpl中调用另外两个微服务的接口方法
底层是feign创建这两个接口的实现类,来看看更新后的OrderServiceImpl@Service public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private AccountServiceFeign accountServiceFeign; @Resource private StorageServiceFeign storageServiceFeign; /* * 案例主方法:增加订单--->扣减账户余额--->扣减库存--->更改订单状态 * */ @Override public Integer create(Order order) { orderDao.addOrder(order); /* feign接口不能有异常处理,需要错误需要暴露在这里,不然seata任务没有报错,不回滚数据库 */ accountServiceFeign.reduceAccount(order.getUserId(),order.getMoney()); storageServiceFeign.reduceStorage(order.getProductId(),order.getCount()); Integer result = orderDao.updateOrderStatus(order.getUserId(),"1"); return result; //若修改状态成功,则返回1,否则为0 } }
以上就是配置feign的全部内容
增加sentinel
sentinel分为核心库和控制台dashboard,sentinel控制台独立运行,核心库则被整合进微服务,
我们在sentinel控制台配置的规则数据,会被发送到sentinel核心库中,sentinel核心库运行于内存中,
接收的规则数据立即生效。
值得注意的是:sentinel核心库与微服务整合在一起,但也开启了另一个端口,用来接收sentinel控制台规则数据
sentinel核心库,sentinel控制台,微服务三个最好在同一台机器上,否则可能sentinel监控不了微服务,
只是我推测,因为控制台在阿里云上时,监控不了我的微服务。
导入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <!--去除jackson-dataformat-xml,否则降级方法会返回xml数据,而不是JSON数据--> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </exclusion> </exclusions> </dependency>
在微服务配置文件中配置sentinel
spring: cloud: #sentinel-1 sentinel: transport: dashboard: localhost:8080 port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未被占用的端口 #这里的port和clientIp指的是sentinel核心库,接收sentinel dashboard的数据并将规则数据注册到sentinel进程中 clientIp: localhost #sentinel-2 management: endpoints: web: exposure: include: '*' endpoint: sentinel: enabled: true #开启actuator对sentinel的支持 feign: #sentinel与feign整合需要开启 sentinel: enabled: true
上述就是sentinel的配置,暴露微服务端点,配置sentinel控制台地址,开启feign对sentinel的支持
之前已经展示的配置已省略为OrderController接口配置限流和降级方法
- OrderController更新后
@RestController @RequestMapping("order") @RefreshScope public class OrderController { @Resource private OrderService orderService; @Value("${info}") private String info; @RequestMapping("/nacos") @SentinelResource( value = "nacos", blockHandlerClass = OrderDegradeAndException.class,blockHandler = "info_degrade", fallbackClass = OrderDegradeAndException.class,fallback = "info_exception") public String nacosTest(){ return info; } @PostMapping("/") @SentinelResource( value = "create", blockHandlerClass = OrderDegradeAndException.class,blockHandler = "create_degrade", fallbackClass = OrderDegradeAndException.class,fallback = "create_exception") public CommonResult<Integer> create(@RequestBody Order order){ order.setStatus("0"); return new CommonResult<>(200,"success",orderService.create(order)); } }
OrderDegradeAndException
@Slf4j @Component public class OrderDegradeAndException { //对指定方法配置熔断降级方法和异常处理方法 public static CommonResult<Integer> create_degrade(@RequestBody Order order, BlockException exception){ log.info("OrderController--create方法被熔断限流:"+exception.getMessage()); return new CommonResult<>(500,"error:"+exception.getMessage(),0); } public static CommonResult<Integer> create_exception(@RequestBody Order order, Throwable throwable){ log.info("OrderController--create方法出现异常:"+throwable.getMessage()); return new CommonResult<>(500,"error:"+throwable.getMessage(),0); } public static String info_degrade(BlockException exception){ log.info("OrderController--info方法被熔断限流:"+exception.getStackTrace()); return "info方法被熔断限流:"+exception.getRule(); } public static String info_exception(Throwable throwable){ log.info("OrderController--info方法出现异常:"+throwable.getMessage()); return "info方法出现异常:"+throwable.getMessage(); } //全局异常处理 public static CommonResult<Integer> global_exception(Throwable throwable){ log.info("OrderController--create方法出现异常:"+throwable.getMessage()); return new CommonResult<>(500,"error:"+throwable.getMessage(),0); } }
注意:随着后来的认识,blockHandler指定的方法本以为专门处理限流,降级,
fallback指定的方法本以为是专门处理接口处理异常的情况,其实并不对,
@SentinelResource注解中blockHandler指定的方法处理的是限流,而fallback指定的方法处理异常,
熔断降级的方法,接口熔断后或发生异常后,执行fallback方法,接口被限流后,执行blockHandler方法为feign接口配置降级方法
ps:interceptor中是seata的配置
AccountServiceFeign更新后
@FeignClient(value="AccountService",fallbackFactory=AccountServiceFallback.class)
增加fallbackFactory配置项
AccountServiceFallback
@Component @Slf4j public class AccountServiceFallback implements FallbackFactory<AccountServiceFeign> { @Override public AccountServiceFeign create(final Throwable throwable) { return new AccountServiceFeign() { @Override public CommonResult<Integer> reduceAccount(String userId, String money) { log.info("AccountServiceFeign报错:"+throwable.getMessage()); return new CommonResult<>(500,"error:"+throwable.getMessage(),0); } }; } }
StorageServiceFeign也是同样的配置,注意,使用seata时,不能为feign配置降级方法,
不然seata认为feign接口并没有报错,导致另外两个微服务数据库不回滚!!!
增加gateway
Gateway微服务则是独立的微服务了,一般作为业务网关,做权限验证,请求负载均衡,熔断限流等等
Gateway同样需要注册进nacos,所以也需要nacos依赖,具体依赖上面已经给出。
下面是gateway的配置文件,bootstrap.yaml
server:
port: 2348
spring:
application:
name: GatewayService
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 47.98.138.53:8848
cluster-name: HZ
namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138
config:
server-addr: 47.98.138.53:8848
file-extension: yaml
namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138
gateway:
routes:
- id: OrderService #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://OrderService
predicates:
- Path=/order/** #断言,路径相匹配的进行路由
- id: AccountService
# uri: http://localhost:8001
uri: lb://AccountService #使用服务名进行路由,需要开启discovery.locator.enabled=true
predicates:
- Path=/account/** #断言,路径相匹配的进行路由
- After=2021-10-26T22:48:16.597+08:00[Asia/Shanghai]
filters:
- AddRequestHeader=Truth,zhangsan is freaking awesome!
default-filters: # 这里是默认路由作用每一个路由
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
# 注意:路由实例过滤器(配置在路由里面),defaultFilter,globalFilter,这三个路由器是根据order排序的
# 如果order值相同,则defaultFilter>路由实例过滤器>globalFilter
Gateway微服务的路由规则可以通过配置实现,还可以自定义过滤器,这里偷懒没有配置了
增加seata
seata是解决分布式事务的组件,其中三个重要的角色,TC,TM,RM,TC作为seata-server服务端
TM,RM作为seata客户端被整合进微服务中,seata有几种分布式解决方案模型,这里使用是默认的AT模式
让我们来看看在案例中如何配置seata
seata-server(TC)的配置
下载好seata-server服务端,服务端是独立运行的,点击下载seata-server
创建seata-server需要使用的数据库(数据库名任意,本案例数据库名是springcloud_seata,之后在配置文件中要指定),
下面是建表sql语句,ps:seata-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(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;
上述提到的存储模式,可以在
seata-server-1.4.2/conf/file.conf
文件中修改,现在我们需要修改该文件
修改后文件如下,ps:如果你打算将seata的配置放到nacos配置中心上,那么可以不修改该文件## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "db" ## rsa decryption public key publicKey = "" ## database store property db { 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/springcloud_seata?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8" user = "root" password = "123765" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } }
file.conf文件中内容是seata-server所必须的,我们也可以将该文件中的内容放在配置中心上,
你需要修改的是registry.conf文件,与file.conf文件是同级目录。下面是修改后的文件内容registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { application = "SeataServer" #TC服务在nacos上的服务名称 serverAddr = "47.98.138.53:8848" #nacos地址 group = "DEFAULT_GROUP" #TC服务所在组 namespace = "a3a5fee0-15a2-4023-894c-34fbde0f5138" cluster = "HZ" #TC服务所在集群,client端需要配置一致的映射,重要! username = "nacos" password = "nacos" } file { #改成nacos后,file.conf就不加载了,之前配置的数据库信息也就无效了,我们需要在nacos上配置数据库信息 name = "file.conf" } } config { #TC服务的配置文件在nacos上的位置 # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { serverAddr = "47.98.138.53:8848" namespace = "a3a5fee0-15a2-4023-894c-34fbde0f5138" group = "SEATA_GROUP" #这里是TC的配置文件的组,可随意 username = "nacos" password = "nacos" dataId = "seataServer.properties" #TC的配置文件名称,nacos配置中心需要同样配置 } file { name = "file.conf" } }
其实就是配置seata-server在nacos上的一些信息。
上面配置中,我们将seata-server需要的信息,本来是放在file.conf的信息放在了nacos配置文件上
配置文件名为:seataServer.properties,所以我们现在需要在nacos上创建该配置文件
并添加seata-server需要的内容,该文件内容如下:# 数据存储方式,db代表数据库 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://localhost:3306/springcloud_seata?serverTimezone=GMT&useSSL=false&characterEncoding=utf-8 store.db.user=root store.db.password=123765 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 # 事务、日志等配置 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 # 客户端与服务端传输方式 transport.serialization=seata transport.compressor=none # 关闭metrics功能,提高性能 metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898
之后只需双击
seata-server-1.4.2/bin/seata-server.bat
即可启动seata-server
以上就是seata服务端配置的全部过程
seata客户端(TM,RM)的配置
seata客户端已经整合进了微服务,三个业务微服务都需要进行seata客户端的配置
导入依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>
因为alibaba-seata依赖中seata依赖版本是1.3,我们最好使用最新的seata版本,所以将内部自带的
依赖排除,添加最新的seata依赖在微服务的入口类上添加@EnableAutoDataSourceProxy注解,该注解用于代理druid的数据库连接池
注意,因为seata版本不同,会有不同的配置,使用1.4.2最新版本,或者比较新版本需要这个注解
使用版本比较低的seata,需要使用@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
因为seata默认使用AT模式,需要我们在各个微服务的数据库中增加undo_log表,用于存储seataAt模式下的前后镜像
创建表的sql语句如下: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` LONGraw 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';
接下来在各个微服务配置文件中配置Seata
# seata的配置,用于找到seata-server(TC)的服务实例和配置文件信息 seata: enabled: true #开启seata enable-auto-data-source-proxy: true #seata自动代理数据源,新版本有的 tx-service-group: seata-demo #配置事务组,事务组用于映射TC服务的集群名称 data-source-proxy-mode: AT #配置seata模式 config: #配置的是TC服务的配置文件在nacos上的地址 TC服务的配置中心 type: nacos nacos: server-addr: 47.98.138.53:8848 group: SEATA_GROUP #这里的组与registry.conf中config一致 namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138 dataId: client.properties #这里是nacos的配置文件,本应该是seataServer.properties username: nacos #但我这里做了一个抽离成新的配置文件,这两个文件内容可以放一起 password: nacos registry: #配置的是TC服务在nacos上的地址,TC服务的注册中心 type: nacos nacos: application: SeataServer #TC服务名称 server-addr: 47.98.138.53:8848 group: DEFAULT_GROUP #TC所在的组,需要与业务微服务同组 namespace: a3a5fee0-15a2-4023-894c-34fbde0f5138 username: nacos password: nacos service: vgroup-mapping: seata-demo: HZ # 事务组与nacos中集群的映射关系,集群指的是TC所在的集群 disable-global-transaction: false
上面提到了client.properties,这个文件中的内容可以放在之前配置的seataServer.properties文件中
这里做了一个拆分,下面是client.properties中的内容# 事务组映射关系 service.vgroupMapping.seata-demo=HZ service.enableDegrade=false service.disableGlobalTransaction=false # 与TC服务的通信配置 transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableClientBatchSendRequest=false 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 # RM配置 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 # TM配置 client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.defaultGlobalTransactionTimeout=60000 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 # undo日志配置 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 client.undo.compress.threshold=64k client.log.exceptionRate=100
这里有个特别重要的配置:service.vgroupMapping.seata-demo=HZ
seata-demo是随意取的TC的事务组名,HZ则表示TC服务在nacos上的集群名称
我们不仅要在nacos配置文件上配置事务组与TC服务集群名称的映射,还要在我们微服务上,也就是seata客户端上配置映射tx-service-group: seata-demo service: vgroup-mapping: seata-demo: HZ # 事务组与nacos中集群的映射关系,集群指的是TC所在的集群
我们还需要seata-server的registry.conf文件中registry的cluster的值和微服务上配置文件的值一致
registry.conf中是TC集群名称,nacos配置文件上配置是事务组和集群名称,微服务配置的是事务组和集群名称
以上就是seata客户端的配置
seata一些需要注意的地方
seata1.4.2之后,需要回滚的表中日期类型不能使用datetime,可以使用timestamp
当使用@globalTransactional注解开启一个TM全局事务时,如果内部使用feign调用其他微服务,
则当调用失败时,应该直接报错,而不是进入熔断降级方法,不然seata认为并没有报错,从而不回滚
换句话说,使用seata时,内部的feign不能配置降级异常处理方法,报错要上报到TM方法中还记得在sentinel为feign接口配置熔断降级的时候,feign包下有个interceptor包吗,这里放的是feign
的拦截器,有些情况下,或许是seata版本不同,或许是在某个特定场景下,会出现微服务数据库不回滚
这是因为AT模式中全局锁XID没有传递到其他微服务中,需要我们配置feign拦截器在请求时添加上xid
下面是feign的拦截器类:FeignInterceptor@Configuration @Slf4j public class FeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { //解决seata的xid未传递 String xid = RootContext.getXID(); if (StringUtils.isNotBlank(xid)) { log.info("feign传递分布式事务xid:{}", xid); template.header(RootContext.KEY_XID, xid); } } }
然后在feign注解上添加configuration选项,例如:
@FeignClient(value = "AccountService" /*,configuration = FeignInterceptor.class*/ //用于传递xid的配置,对seata无影响,暂时还不知道作用 /*,fallbackFactory = AccountServiceFallback.class*/ //异常处理对seata有影响 ) @RequestMapping("/account") public interface AccountServiceFeign { @PutMapping("/") public CommonResult<Integer> reduceAccount( @RequestParam("userId") String userId, @RequestParam("money") String money ); }
因为本案例中并没有出现xid不传递现象,所以无需配置feign拦截器 - 有些人说seata事务不回滚是因为mybatis使用了数据库主键自增导致的,我们执行sql语句时 需要自己设置数据的id,不能使用数据库自增,但是我的案例没有这样的问题,可能还是某个版本的缘故 我也不清楚,或者是别人自己本身的原因,或者是seata模式不同的原因。
一些博客
案例中不完美的地方
- nacos没有配置集群,如果配置nacos集群则需要更改存储到外部mysql
- sentinel没有持久化到nacos
- gateway的熔断限流,黑白名单没有配置
- seata的TC集群没有配置
案例资料
本篇博客基本结束,也宣告了微服务这块暂告一段落,虽说很多东西都没有做一个深层次的了解,但学习起来
只是时间问题,接下去我应该去学习之前遗漏的知识点,主要是多线程一块吧,还有毕设要写。
本案例的资料,放在github上,有需要可以查看一下:案例资料