# 微服务 **Repository Path**: liuhaonan71t/microservices ## Basic Information - **Project Name**: 微服务 - **Description**: 第五阶段微服务学习用 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2023-12-21 - **Last Updated**: 2024-10-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # **_微服务商城项目_** ## **父项目** 每次新建一个子项目都要进行父子相认 ![img.png](photo/img.png) 修改pom文件 注意parent标签中 version版本要更改(idea2023并没有java8的选项 要更改服务器为阿里云的) ![img_1.png](photo/img_1.png) ### 子项目 # Nacos 我使用的时1.4.2的版本,启动时进入到nacos的bin目录 ```shell startup.cmd -m -standalone ``` # stock模块 创建子项目模块,在模块的pom中修改parent标签为父项目的这一部分 ```xml cn.tedu csmall 0.0.1-SNAPSHOT ``` ![img_2.png](photo/img_2.png) 到这里父子相认就完成了 ## 2、父项目添加依赖 ### 又称锁版本 定义依赖版本的变量,父pom中指定版本,子pom中就不需要指定版本了 除非有额外的版本需要,才在子项目中添加版本信息 ```xml 1.8 2.2.2 ``` 添加完毕之后开始添加依赖,在version标签中添加之前定义的变量${*}的格式来使用变量 ```xml org.mybatis.spring.boot mybatis-spring-boot-starter ${mybats.version} ``` 子项目中不需要添加版本信息,直接添加所需要的依赖就可以自动匹配父项目的版本 ![img_3.png](photo/img_3.png) 添加依赖之后可能会出现找不到依赖包的报错,这种时候可以尝试使用下面的方法 就是把报错的dependency标签赋值到dependencyManagement外面的dependencies(就是跟我的lombok同级,并不是在原来的依赖管理中) 然后刷新maven就可以下载了 ![img.png](photo/img5.png) # commons模块 maven搞定之后就新建一个模块为commons ```xml com.github.xiaoymin knife4j-spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-json org.springframework.boot spring-boot-starter-tomcat ``` 改完pom之后删除启动类 resources包和test包 exclusions标签的作用是保留注解,排除启动的依赖 因为commons包不需要启动,是被其他模块调用的 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-json org.springframework.boot spring-boot-starter-tomcat ``` 创建一个包 在commons包下,创建一个pojo包,在pojo包下创建一个cart,在cart下创建一个dto包 ,dto包下创建一个CartAddDto实体类 **Dto**是前端传给后端的参数,**Vo**是前端返回给后端的参数,**entity/model/domain/bean/pojo/其他**的实体类为真正的实体类 持久层**mapper/dao/repository** 业务逻辑层较为统一**service/biz**(非常老) 控制层**controller/servlet** 与数据库字段一一对应 ## 购物车实体类 ```java @Data @ApiModel("购物车添加DTO") public class CartAddDto implements Serializable { @ApiModelProperty(value = "商品编号",name = "commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "商品价格",name = "price",example = "20") private Integer price; @ApiModelProperty(value = "商品数量",name = "count",example = "2") private Integer count; @ApiModelProperty(value = "用户id",name = "userId",example = "UU100") private String userId; } ``` ```java @Data @ApiModel("购物车实体类") public class Cart implements Serializable { @ApiModelProperty(value = "购物车id",name = "id",example = "1") private Integer id; @ApiModelProperty(value = "商品编号",name = "commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "商品价格",name = "price",example = "20") private Integer price; @ApiModelProperty(value = "商品数量",name = "count",example = "2") private Integer count; @ApiModelProperty(value = "用户id",name = "userId",example = "UU100") private String userId; } ``` ## 订单类 ```java @ApiModel(value = "新增订单的DTO") @Data public class OrderAddDTO implements Serializable { @ApiModelProperty(value = "用户id",name="userId",example = "UU100") private String userId; @ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "购买数量",name="count",example = "5") private Integer count; @ApiModelProperty(value = "总金额",name="money",example = "500") private Integer money; } ``` ```java @Data @ApiModel(value = "订单实体类") public class Order implements Serializable { private Integer id; @ApiModelProperty(value = "用户id",name="userId",example = "UU100") private String userId; @ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "购买数量",name="count",example = "5") private Integer count; @ApiModelProperty(value = "总金额",name="money",example = "500") private Integer money; } ``` ## 库存类 ```java @ApiModel(value = "商品减库存DTO") @Data public class StockReduceCountDTO implements Serializable { @ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "减库存数",name="reduceCount",example = "5") private Integer reduceCount; } ``` ```java @Data @ApiModel(value = "商品库存") public class Stock implements Serializable { @ApiModelProperty(value = "库存Id",name="commodityCode",example = "11") private Integer id; @ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100") private String commodityCode; @ApiModelProperty(value = "减库存数",name="reduceCount",example = "5") private Integer reduceCount; } ``` ## 异常处理类 ```java /** * 错误代码枚举类型 */ public enum ResponseCode { OK(200), BAD_REQUEST(400), UNAUTHORIZED(401), FORBIDDEN(403), NOT_FOUND(404), NOT_ACCEPTABLE(406), CONFLICT(409), INTERNAL_SERVER_ERROR(500); private Integer value; ResponseCode(Integer value) { this.value = value; } public Integer getValue() { return value; } } ``` ## 关于枚举 异常处理类使用的就是enum枚举, 这是一个相对特殊的类, 一般情况下用来表示一组常量, 对于一些固定值并且数量有限的情况, 可以使用枚举。 ## 业务异常类 ```java /** * 业务异常 */ @Data @EqualsAndHashCode(callSuper = false) public class CoolSharkServiceException extends RuntimeException { private ResponseCode responseCode; public CoolSharkServiceException(ResponseCode responseCode, String message) { super(message); setResponseCode(responseCode); } } ``` super用法,子调父的私有属性不可直接调用,可以通过super来调用父级的私有属性 # business模块 还是删除test测试类 添加配置[application.yml](csmall-business%2Fsrc%2Fmain%2Fresources%2Fapplication.yml) ```yaml server: port: 20000 #公共配置 mybatis: configuration: cache-enabled: false # 不启用mybatis缓存 map-underscore-to-camel-case: true # 映射支持驼峰命名法 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 将运行的sql输出到控制台 knife4j: # 开启增强配置 enable: true # 生产环境屏蔽,开启将禁止访问在线API文档 production: false # Basic认证功能,即是否需要通过用户名、密码验证后才可以访问在线API文档 basic: # 是否开启Basic认证 enable: false # 用户名,如果开启Basic认证却未配置用户名与密码,默认是:admin/123321 username: root # 密码 password: root spring: profiles: active: dev ``` server.port:端口号20000,mybatis关闭二级缓存,开启驼峰命名法,把运行的sql输出到控制台 knife4j: 在线api测试文档 spring.profiles.active:表示在这个yml文件加载之后还要在加载一个后缀为dev的yml文件 ## 配置类 ```java //当前类是配置Spring扫描环境的配置类,必须添加此注解 @Configuration //扫描全局异常处理类的包,使其生效 @ComponentScan("cn.tedu.csmall.commons.exception") public class CommonsConfiguration { } ``` ```java /** * Knife4j(Swagger2)的配置 */ @Configuration @EnableSwagger2WebMvc public class Knife4jConfiguration { /** * 【重要】指定Controller包路径 */ private String basePackage = "cn.tedu.csmall.business.controller"; /** * 分组名称 */ private String groupName = "base-business"; /** * 主机名 */ private String host = "http://java.tedu.cn"; /** * 标题 */ private String title = "酷鲨商城项目案例在线API文档--基础businessr-web实例"; /** * 简介 */ private String description = "构建基础business-web项目,实现购买"; /** * 服务条款URL */ private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0"; /** * 联系人 */ private String contactName = "项目研发部"; /** * 联系网址 */ private String contactUrl = "http://java.tedu.cn"; /** * 联系邮箱 */ private String contactEmail = "java@tedu.cn"; /** * 版本号 */ private String version = "1.0-SNAPSHOT"; @Autowired private OpenApiExtensionResolver openApiExtensionResolver; @Bean public Docket docket() { String groupName = "1.0-SNAPSHOT"; Docket docket = new Docket(DocumentationType.SWAGGER_2) .host(host) .apiInfo(apiInfo()) .groupName(groupName) .select() .apis(RequestHandlerSelectors.basePackage(basePackage)) .paths(PathSelectors.any()) .build() .extensions(openApiExtensionResolver.buildExtensions(groupName)); return docket; } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(title) .description(description) .termsOfServiceUrl(termsOfServiceUrl) .contact(new Contact(contactName, contactUrl, contactEmail)) .version(version) .build(); } } ``` ## service 在business模块下新建service层和impl层 ```java public interface IBusinessService { void buy(); } ``` ## impl 因为是测试模块功能,所以不连数据库没有mapper层,直接在impl层写业务逻辑 ```java @Service @Slf4j //控制套输出使用到的注解 public class BusinessServiceImpl implements IBusinessService{ @Override public void buy() { //编写新增订单雏形 // 先实例化一个新增的订单对象 OrderAddDTO orderAddDTO = new OrderAddDTO(); orderAddDTO.setUserId("UU100"); orderAddDTO.setCount(10); orderAddDTO.setMoney(199); orderAddDTO.setCommodityCode("PC100"); //这个实例化的DTO对象,需要传递给订单order模块,让订单模块去新增订单 log.info("订单模块新增订单,传递的参数是:"+orderAddDTO); } } ``` ## controller ```java @RestController @RequestMapping("/base/business") @Api(tags = "业务触发模块") public class BusinessController { @Resource private IBusinessService businessService; @PostMapping("/buy") @ApiOperation("执行业务触发的方法") public JsonResult buy(){ businessService.buy(); return JsonResult.ok("完美成功"); } } ``` 通过Knife4j测试(正确返回如下) ![img_4.png](photo/img_4.png) 现在要开始将项目注册到nacos注册中心,现在business模块中增加依赖 ```yaml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 然后修改[application-dev.yml](csmall-business%2Fsrc%2Fmain%2Fresources%2Fapplication-dev.yml)文件 ```yaml spring: application: name: csmall-business #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 ``` 然后启动nacos再启动项目(顺序很关键) ## 心跳机制 nacos有一个心跳机制, 服务每五秒会向nacos发送一次信息交互,当我们把服务注册到nacos会发送一个心跳包, nacos会根据心跳包检查当前服务是否在nacos中存在, 如果不存在就按照新业务进行注册,否则代表当前服务是健康状态 如果一个服务连续三次心跳(默认是15秒)没有和Nacos进行信息的交互,就会把当前服务标记为不健康状态 如果连续六次心跳(默认30秒)没有和Nacos进行信息的交互,就会把当前服务从Nacos中删除 这些时间是可以通过配置修改的 ## 实例类型分类 实际上nacos的服务类型是有分类的 * 临时实例 * 持久化实例(永久实例) 持久化实例启动时像nacos注册,心跳包的规则和临时实例是一样的, 只是不会将该服务从列表中剔除,一边情况下默认是临时实例, 只有项目的主干业务才会设置为永久实例 # cart模块 新建cart模块,导入pom依赖 ```xml org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 把busniess模块的配置文件复制过来 ```yaml server: port: 20001 #公共配置 mybatis: configuration: cache-enabled: false # 不启用mybatis缓存 map-underscore-to-camel-case: true # 映射支持驼峰命名法 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 将运行的sql输出到控制台 knife4j: # 开启增强配置 enable: true # 生产环境屏蔽,开启将禁止访问在线API文档 production: false # Basic认证功能,即是否需要通过用户名、密码验证后才可以访问在线API文档 basic: # 是否开启Basic认证 enable: false # 用户名,如果开启Basic认证却未配置用户名与密码,默认是:admin/123321 username: root # 密码 password: root spring: profiles: active: dev ``` ```yaml spring: application: name: csmall-cart #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false ``` 配置好了开始连接数据库,导入准备好的sql文件[csmall_db.sql](sql%2Fcsmall_db.sql),我使用的是MySQL8 ,依然是导入两个配置 ![img_5.png](photo/img_5.png) 需要注意一点 knife4j中的扫描包名等要改为cart(如果是从busniess CV来的话) 写好配置之后就可以开始写业务逻辑了 ## Mapper ```java package cn.tedu.csmall.cart.mapper; import cn.tedu.csmall.commons.pojo.cart.entity.Cart; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository public interface CartMapper { //向购物车表中新增商品数据 @Insert("insert into cart_tbl(commodity_code,price,count,user_id) " + "values (#{commodityCode},#{price},#{count},#{userId})") int insertCart(Cart cart); //删除购物车中的商品信息 @Delete("delete from cart_tbl where user_id=#{userId} and commodity_code=#{commodityCode}") int deleteCartByUserIdAndCommodityCode(@Param("userId") String userId, @Param("commodityCode") String commodityCode); } ``` ## service ```java package cn.tedu.csmall.cart.service; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; import cn.tedu.csmall.commons.pojo.cart.entity.Cart; public interface ICartService { void cartAdd(CartAddDto cartAddDto); void deleteUserCart(String userId, String commodityCode); } ``` ## impl ```java package cn.tedu.csmall.cart.service.impl; import cn.tedu.csmall.cart.mapper.CartMapper; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; import cn.tedu.csmall.commons.pojo.cart.entity.Cart; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class CartServiceImpl implements ICartService { @Autowired private CartMapper cartMapper; //新增购物车方法 @Override public void cartAdd(CartAddDto cartAddDto) { //当前方法是CartAddDto对象 //mapper接受的是cart对象,所以要进行转换 Cart cart = new Cart(); //利用BeanUtils。copyProperties方法进行转换 BeanUtils.copyProperties(cartAddDto, cart); // 调用mapper的方法,将cart对象插入到数据库中 int rows = cartMapper.insertCart(cart); //打印日志,可以通过rows的值来判断是否成功 log.info("新增购物车商品成功,受影响的行数为:{}", rows); } //删除购物车的方法 @Override public void deleteUserCart(String userId, String commodityCode) { // 根据用户id和商品编号删除购物车中的商品信息 int i = cartMapper.deleteCartByUserIdAndCommodityCode(userId, commodityCode); log.info("删除购物车商品成功,受影响的行数为:{}", i); } } ``` ## Controller ```java package cn.tedu.csmall.cart.controller; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; import cn.tedu.csmall.commons.restful.JsonResult; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/cart") public class CartController { @Autowired private ICartService cartService; @PostMapping("/add") @ApiOperation("新增商品到购物车") public JsonResult add(CartAddDto cartAddDto) { cartService.cartAdd(cartAddDto); return JsonResult.ok("新增商品到购物车成功!"); } @PostMapping("delete") @ApiOperation("删除购物车中的商品") public JsonResult delete(String userId, String commodityCode) { cartService.deleteUserCart(userId, commodityCode); return JsonResult.ok("删除购物车中的商品成功!"); } } ``` # Order模块 新建Order模块,导入依赖,父子相认,与Cart模块一样 ```xml org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 编写代码,实现新增订单功能,因为订单不需要删除所以只需要写新增方法就可以 ## Mapper ```java package cn.tedu.csmall.order.mapper; import cn.tedu.csmall.commons.pojo.order.entity.Order; import org.apache.ibatis.annotations.Insert; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper { @Insert("insert into order_tbl(user_id,commodity_code,count,money) " + "values(#{userId},#{commodityCode},#{count},#{money})") int insertOrder(Order order); } ``` ## Service ```java package cn.tedu.csmall.order.service; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; public interface IOrderService { void OrderAdd(OrderAddDTO orderAddDTO); } ``` ## Impl ```java package cn.tedu.csmall.order.service.impl; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.order.mapper.OrderMapper; import cn.tedu.csmall.order.service.IOrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; @Override public void OrderAdd(OrderAddDTO orderAddDTO) { //1.这里要先进行减少数据库中库存的操作(stock模块) //2.还要从购物车中删除用户选中的商品(cart模块) Order order = new Order(); BeanUtils.copyProperties(orderAddDTO,order); int i = orderMapper.insertOrder(order); log.info("新增订单成功,插入了:{}条",i); } } ``` ## Controller ```java package cn.tedu.csmall.order.controller; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.order.service.IOrderService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") @Api(tags = "订单业务触发模块") public class OrderController { @Autowired private IOrderService orderService; @PostMapping("/add") @ApiOperation("新增订单") public JsonResult OrderAdd(OrderAddDTO orderAddDTO){ orderService.OrderAdd(orderAddDTO); return JsonResult.ok("新增订单成功!"); } } ``` # Stock模块 编写Stock模块,第一步添加依赖 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu csmall-stock 0.0.1-SNAPSHOT csmall-stock csmall-stock org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 第二步,修改配置文件 ```yaml server: port: 20003 #公共配置 mybatis: configuration: cache-enabled: false # 不启用mybatis缓存 map-underscore-to-camel-case: true # 映射支持驼峰命名法 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 将运行的sql输出到控制台 knife4j: # 开启增强配置 enable: true # 生产环境屏蔽,开启将禁止访问在线API文档 production: false # Basic认证功能,即是否需要通过用户名、密码验证后才可以访问在线API文档 basic: # 是否开启Basic认证 enable: false # 用户名,如果开启Basic认证却未配置用户名与密码,默认是:admin/123321 username: root # 密码 password: root spring: profiles: active: dev ``` ```yaml spring: application: name: nacos-stock #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false datasource: url: jdbc:mysql://localhost:3306/csmall_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root ``` ## 配置类 和Order模块与Cart模块一样 CV就可以 ## Mapper 需要注意一些问题,首先@Param在多参数时必须添加,否则会报错,减库存的逻辑与订单和购物车有所区别,首先是需要一些逻辑运算 要判断库存是否足够 ```java package cn.tedu.csmall.stock.mapper; import cn.tedu.csmall.commons.pojo.stock.entity.Stock; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; @Repository public interface StockMapper { @Update("update stock_tbl set count = count - #{countNum} " + " where commodity_code = #{commodityCode} AND " + " count>=#{countNum}") int stockUpdate(@Param("commodityCode") String commodityCode, @Param("countNum") int countNum); } ``` ## Service service依旧接参为DTO ```java package cn.tedu.csmall.stock.service; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; public interface IStockService { void reduceStock(StockReduceCountDTO stockReduceCountDTO); } ``` ## Impl 这里要判断他是否成功,如果失败要抛出异常,如果不抛异常可能会造成程序服务停机 ```java package cn.tedu.csmall.stock.service.impl; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.pojo.stock.entity.Stock; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.mapper.StockMapper; import cn.tedu.csmall.stock.service.IStockService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class StockServiceImpl implements IStockService { @Autowired private StockMapper stockMapper; @Override public void reduceStock(StockReduceCountDTO stockReduceCountDTO) { int rows = stockMapper.stockUpdate(stockReduceCountDTO.getCommodityCode(), stockReduceCountDTO.getReduceCount()); if (rows==0){ throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,"库存不足或商品不存在"); } log.info("库存扣减成功"); } } ``` ## Controller ```java package cn.tedu.csmall.stock.controller; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.stock.service.IStockService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/stock") @Api(tags = "库存业务模块") public class StockController { @Autowired private IStockService stockService; @PostMapping("/reduce") @ApiOperation(value = "扣减库存") public void reduceStock(StockReduceCountDTO stockReduceCountDTO) { stockService.reduceStock(stockReduceCountDTO); } } ``` # Dubbo ![img_6.png](photo/img_6.png) 默认使用Dubbo协议,支持很多序列化协议,认默使用Hessian2,默认情况下支持的协议有如下特征: * 采用NIO单一长连接 * 优秀的并发性能,但是处理大型文件的能力差 Dubbo方便支持高并发和高性能 ![img_7.png](photo/img_7.png) consumer服务的消费者,指的是服务的调用者(使用者),也是需要注册到注册中心,provider启动之后把服务都给到注册中心, consumer启动之后能够看到注册中心所有的服务,发现之后就可以根据RPC远程过程调用的方式调用服务,Dubbo中,远程调用 依据是服务的提供者在Nacos中的注册服务名称,只需服务名称,不需要ip地址和端口号等信息 ![img_8.png](photo/img_8.png) * order模块调用stock模块的额减少库存 * order调用cart模块的删除购物车 * business调用order新增订单的功能 # 重构stock模块 stock是典型的生产者,需要被其他模块调用,为了减轻对项目的负担, 业界通常做法是拆分为两个模块 ## stock-service stock-service是一个独立的服务,只提供服务,不提供任何的web接口,只提供服务, 导入对应依赖,因为不需要启动只是被调用所以可以删除所有的启动文件,resource,test文件 ```xml 4.0.0 cn.tedu csmall-stock 0.0.1-SNAPSHOT csmall-stock-service 0.0.1-SNAPSHOT csmall-stock-service csmall-stock-service cn.tedu csmall-commons 0.0.1-SNAPSHOT ``` ### 导入Service文件 ```java package cn.tedu.csmall.stock.service; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; public interface IStockService { void reduceStock(StockReduceCountDTO stockReduceCountDTO); } ``` ## stock-webapi stock-webapi是一个独立的服务,只提供web接口,不提供任何的服务 添加Dubbo依赖,添加配置文件 ```xml 4.0.0 cn.tedu csmall-stock 0.0.1-SNAPSHOT csmall-stock-webapi 0.0.1-SNAPSHOT csmall-stock-webapi csmall-stock-webapi org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery cn.tedu csmall-commons 0.0.1-SNAPSHOT com.alibaba.cloud spring-cloud-starter-dubbo cn.tedu csmall-stock-service 0.0.1-SNAPSHOT ``` 再把csmall-stock中的src文件夹下所有的文件都copy到webapi的src下,然后删除csmall-stock中的src文件夹 ### 修改dev配置文件 默认是从20880开始,可用就使用,不可用就+1,直到可用,也可手动填写端口号 ```yaml spring: application: name: nacos-stock #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false datasource: url: jdbc:mysql://localhost:3306/csmall_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root dubbo: protocol: #协议 #设置为-1 表示启动dubbo自动寻找可用端口号的功能 port: -1 name: dubbo #指定注册到哪个Nacos注册中心 registry: address: nacos://localhost:8849#注册中心地址 #当前项目启动时,是否检查本项目需要的所有远程服务是否启动 consumer: check: false ``` ### 修改impl类 添加DubboService注解,他与Service注解不冲突,一个是spring的,一个是Dubbo的 ```java package cn.tedu.csmall.stock.webapi.service.impl; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.service.IStockService; import cn.tedu.csmall.stock.webapi.mapper.StockMapper; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j @DubboService public class StockServiceImpl implements IStockService { @Autowired private StockMapper stockMapper; @Override public void reduceStock(StockReduceCountDTO stockReduceCountDTO) { int rows = stockMapper.stockUpdate(stockReduceCountDTO.getCommodityCode(), stockReduceCountDTO.getReduceCount()); if (rows==0){ throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,"库存不足或商品不存在"); } log.info("库存扣减成功"); } } ``` ### 修改启动类 当前项目是dubbo调用中的生产者必须添加@EnableDubbo注解,添加之后在服务启动时,当前项目提供的所有服务,才能正常被消费 ```java package cn.tedu.csmall.stock.webapi; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication //当前项目是dubbo调用中的生产者必须添加@EnableDubbo注解,添加之后在服务启动时,当前项目提供的所有服务,才能正常被消费 @EnableDubbo public class CsmallStockWebapiApplication { public static void main(String[] args) { SpringApplication.run(CsmallStockWebapiApplication.class, args); } } ``` # 重构Cart模块 cart与stock模块基本一摸一样,所以再执行一次stock的所有拆分重构操作即可 ## cart-service ### Service ```java package cn.tedu.csmall.cart.service; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; public interface ICartService { void cartAdd(CartAddDto cartAddDto); void deleteUserCart(String userId, String commodityCode); } ``` ### pom ```xml 4.0.0 cn.tedu csmall-cart 0.0.1-SNAPSHOT cn.tedu csmall-cart-service 0.0.1-SNAPSHOT csmall-cart-service csmall-cart-service cn.tedu csmall-commons 0.0.1-SNAPSHOT ``` ## cart-webapi ### 依赖项 ```xml 4.0.0 cn.tedu csmall-cart 0.0.1-SNAPSHOT csmall-cart-webapi 0.0.1-SNAPSHOT csmall-cart-webapi csmall-cart-webapi org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-dubbo cn.tedu csmall-cart-service 0.0.1-SNAPSHOT ``` ### 配置文件 添加dubbo的配置 ```yaml spring: application: name: nacos-cart #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false datasource: url: jdbc:mysql://localhost:3306/csmall_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root dubbo: protocol: port: -1 name: dubbo registry: address: nacos://localhost:8849 consumer: check: false ``` ### config ```java package cn.tedu.csmall.cart.webapi.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** *@author 刘浩男 */ //当前类是配置Spring扫描环境的配置类,必须添加此注解 @Configuration //扫描全局异常处理类的包,使其生效 @ComponentScan("cn.tedu.csmall.commons.exception") public class CommonsConfiguration { } ``` ```java package cn.tedu.csmall.cart.webapi.config; import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; /** * Knife4j(Swagger2)的配置 */ @Configuration @EnableSwagger2WebMvc public class Knife4jConfiguration { /** * 【重要】指定Controller包路径 */ private String basePackage = "cn.tedu.csmall.cart.webapi.controller"; /** * 分组名称 */ private String groupName = "base-cart"; /** * 主机名 */ private String host = "http://java.tedu.cn"; /** * 标题 */ private String title = "酷鲨商城项目案例在线API文档--基础cart-web实例"; /** * 简介 */ private String description = "构建基础cart-web项目,实现购物车管理"; /** * 服务条款URL */ private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0"; /** * 联系人 */ private String contactName = "项目研发部"; /** * 联系网址 */ private String contactUrl = "http://java.tedu.cn"; /** * 联系邮箱 */ private String contactEmail = "java@tedu.cn"; /** * 版本号 */ private String version = "Cart-web-v1.0"; @Autowired private OpenApiExtensionResolver openApiExtensionResolver; @Bean public Docket docket() { String groupName = "Cart"; Docket docket = new Docket(DocumentationType.SWAGGER_2) .host(host) .apiInfo(apiInfo()) .groupName(groupName) .select() .apis(RequestHandlerSelectors.basePackage(basePackage)) .paths(PathSelectors.any()) .build() .extensions(openApiExtensionResolver.buildExtensions(groupName)); return docket; } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(title) .description(description) .termsOfServiceUrl(termsOfServiceUrl) .contact(new Contact(contactName, contactUrl, contactEmail)) .version(version) .build(); } } ``` ```java package cn.tedu.csmall.cart.webapi.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("cn.tedu.csmall.cart.webapi.mapper") public class MyBatisConfiguration { } ``` ### controller ```java package cn.tedu.csmall.cart.webapi.controller; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; import cn.tedu.csmall.commons.restful.JsonResult; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/cart") @Api(tags = "购物车业务触发模块") public class CartController { @Autowired private ICartService cartService; @PostMapping("/add") @ApiOperation("新增商品到购物车") public JsonResult add(CartAddDto cartAddDto) { cartService.cartAdd(cartAddDto); return JsonResult.ok("新增商品到购物车成功!"); } @PostMapping("delete") @ApiOperation("删除购物车中的商品") @ApiImplicitParams({ @ApiImplicitParam(name = "userId", value = "用户id", required = true,example = "UU100"), @ApiImplicitParam(name = "commodityCode", value = "商品编号", required = true,example = "PC100") }) public JsonResult delete(String userId, String commodityCode) { cartService.deleteUserCart(userId, commodityCode); return JsonResult.ok("删除购物车中的商品成功!"); } } ``` ### impl ```java package cn.tedu.csmall.cart.webapi.service.impl; import cn.tedu.csmall.cart.webapi.mapper.CartMapper; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.pojo.cart.dto.CartAddDto; import cn.tedu.csmall.commons.pojo.cart.entity.Cart; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @DubboService @Service @Slf4j public class CartServiceImpl implements ICartService { @Autowired private CartMapper cartMapper; //新增购物车方法 @Override public void cartAdd(CartAddDto cartAddDto) { //当前方法是CartAddDto对象 //mapper接受的是cart对象,所以要进行转换 Cart cart = new Cart(); //利用BeanUtils。copyProperties方法进行转换 BeanUtils.copyProperties(cartAddDto, cart); // 调用mapper的方法,将cart对象插入到数据库中 int rows = cartMapper.insertCart(cart); //打印日志,可以通过rows的值来判断是否成功 log.info("新增购物车商品成功,受影响的行数为:{}", rows); } //删除购物车的方法 @Override public void deleteUserCart(String userId, String commodityCode) { // 根据用户id和商品编号删除购物车中的商品信息 int i = cartMapper.deleteCartByUserIdAndCommodityCode(userId, commodityCode); log.info("删除购物车商品成功,受影响的行数为:{}", i); } } ``` ### mapper ```java package cn.tedu.csmall.cart.webapi.mapper; import cn.tedu.csmall.commons.pojo.cart.entity.Cart; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository public interface CartMapper { //向购物车表中新增商品数据 @Insert("insert into cart_tbl(commodity_code,price,count,user_id) " + "values (#{commodityCode},#{price},#{count},#{userId})") int insertCart(Cart cart); //删除购物车中的商品信息 @Delete("delete from cart_tbl where user_id=#{userId} and commodity_code=#{commodityCode}") int deleteCartByUserIdAndCommodityCode(@Param("userId") String userId, @Param("commodityCode") String commodityCode); } ``` ### 启动类 ```java package cn.tedu.csmall.cart.webapi; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableDubbo public class CsmallCartWebapiApplication { public static void main(String[] args) { SpringApplication.run(CsmallCartWebapiApplication.class, args); } } ``` # 重构Order模块 与前两个模块基本一致,有区别的是order既是消费者又是生产者,所以需要再依赖里添加对其他模块service的调用 ```xml 4.0.0 cn.tedu csmall-order 0.0.1-SNAPSHOT csmall-order-webapi 0.0.1-SNAPSHOT csmall-order-webapi csmall-order-webapi 1.8 UTF-8 UTF-8 2.7.6 org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery cn.tedu csmall-order-service 0.0.1-SNAPSHOT cn.tedu csmall-cart-service 0.0.1-SNAPSHOT cn.tedu csmall-stock-service 0.0.1-SNAPSHOT com.alibaba.cloud spring-cloud-starter-dubbo ``` ### impl 添加DubboReference注解,表示当前业务逻辑层需要消费其他模块 ```java package cn.tedu.csmall.order.webapi.impl; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.order.webapi.mapper.OrderMapper; import cn.tedu.csmall.order.service.IOrderService; import cn.tedu.csmall.stock.service.IStockService; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @DubboService @Service @Slf4j public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; //添加@DubboReference注解,表示当前业务逻辑层需要消费其他模块 //直接下面生命的业务逻辑接口,必须在Nacos中有对应的注册实现类 //调用时,Dubbo可以自动获取Nacos中注册的业务逻辑层实现类对象 @DubboReference private IStockService stockService; @DubboReference private ICartService cartService; @Override public void OrderAdd(OrderAddDTO orderAddDTO) { //1.这里要先进行减少数据库中库存的操作(stock模块) StockReduceCountDTO stockReduceCountDTO = new StockReduceCountDTO(); stockReduceCountDTO.setReduceCount(orderAddDTO.getCount()); stockReduceCountDTO.setCommodityCode(orderAddDTO.getCommodityCode()); stockService.reduceStock(stockReduceCountDTO); //2.还要从购物车中删除用户选中的商品(cart模块) cartService.deleteUserCart(orderAddDTO.getUserId(),orderAddDTO.getCommodityCode()); //上面操作完成后在进行订单的新增 Order order = new Order(); BeanUtils.copyProperties(orderAddDTO,order); //执行新增 int i = orderMapper.insertOrder(order); log.info("新增订单成功,插入了:{}条",i); } } ``` ### controller 之前为了测试服务没有引用service层的方法,记得加上 ```java package cn.tedu.csmall.order.webapi.controller; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.order.service.IOrderService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") @Api(tags = "订单管理模块") public class OrderController { @Autowired private IOrderService orderService; @PostMapping("/add") @ApiOperation("新增订单") public JsonResult OrderAdd(OrderAddDTO orderAddDTO){ orderService.OrderAdd(orderAddDTO); return JsonResult.ok("新增订单成功!"); } } ``` Duboo在实现远端调用的过程中,是通过负载均衡算法,能够尽可能让请求在相对空闲的服务器上运行 在不同的项目中可能选用不同的负载均衡策略,以达到最好的效果 ![img_9.png](photo/img_9.png) **Loadbalance**: 就是负载均衡的意思 **平滑权重运行**: 在一段时间内,让权重逐渐增加,直到最大值,然后再逐渐减少,直到最小值,然后再逐渐增加,直到最大值,如此循环 ![img_10.png](photo/img_10.png) **活跃度自动感知**: 记录每个服务器处理一次请求的时间 ,按照时间比例来分配任务数,运行一次需要时间多的分配的请求数较少 **一致性hash算法分配**: 根据请求的参数进行hash运算,以后每次相同参数的请求会访问固定的服务器,因为根据参数选择服务器所以不能平均分配,使用不多 # 更新Business模块 因为Business是单纯的消费者模块,所以不需要项目拆分,只需要添加Dubbo依赖调用其他生产者就可以 ## 配置文件 ```yaml dubbo: protocol: port: -1 name: dubbo registry: address: nacos://localhost:8849 consumer: check: false ``` ## pom文件 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu csmall-business 0.0.1-SNAPSHOT csmall-business csmall-business org.springframework.boot spring-boot-starter-web com.github.xiaoymin knife4j-spring-boot-starter cn.tedu csmall-commons 0.0.1-SNAPSHOT com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-dubbo cn.tedu csmall-order-service 0.0.1-SNAPSHOT ``` ## Impl ```java package cn.tedu.csmall.business.service.impl; import cn.tedu.csmall.business.service.IBusinessService; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.order.service.IOrderService; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.stereotype.Service; @Service @DubboService @Slf4j public class BusinessServiceImpl implements IBusinessService { @DubboReference private IOrderService DubboOrderService; @Override public void buy() { // 先实例化一个新增的订单对象 OrderAddDTO orderAddDTO = new OrderAddDTO(); orderAddDTO.setUserId("UU100"); orderAddDTO.setCount(10); orderAddDTO.setMoney(199); orderAddDTO.setCommodityCode("PC100"); //这个实例化的DTO对象,需要传递给订单order模块,让订单模块去新增订单 DubboOrderService.OrderAdd(orderAddDTO); } } ``` ## 启动类 ```java package cn.tedu.csmall.business; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableDubbo public class CsmallBusinessApplication { public static void main(String[] args) { SpringApplication.run(CsmallBusinessApplication.class, args); } } ``` # Seata Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 也是Spring Cloud Alibaba提供的组件 Seata官方文档 https://seata.io/zh-cn/ 在微服务的项目中,业务逻辑涉及远程调用,当前模块发生已成,无法操作远程服务器回滚 这时想要让远端调用也支持事务功能,就需要使用分布式事务组件Seata Seata保证微服务远程调业务的原子性 ## AT模式 AT模式是Seata默认的事务模式,也是最常用的事务模式 ![img_11.png](photo/img_11.png) 这是一个比较典型的远端调用结构 Seata构成部分包含 * TC:事务协调器,负责协调全局事务,并驱动全局事务提交或回滚 * TM:事务管理器,用于开启一个全局事务,并最终发起全局提交或回滚 * RM:资源管理器,用于控制分支事务,并驱动分支事务的提交或回滚 具体的流程大概是TM发起全局事务,TC接收到全局事务后,向所有的RM发起分支事务, 然后RM执行本地事务,并将本地事务的执行结果返回给TC,TC收集到所有的RM的执行结果后, 决定是提交还是回滚,如果有一个RM失败,会把信息返回给TC,TC再通知其他RM进行回滚 ![img_12.png](photo/img_12.png) ## 启动Seata 我使用的时Seata-server-1.4.2版本 启动命令类似nacos 不同的是再bin目录下通过seata-server.bat脚本启动 ```shell seata-server.bat -p 8091 -m file ``` ## 添加依赖 这里以stock模块为例,其他模块也相同,就不一一复制了 ```xml 4.0.0 cn.tedu csmall-stock 0.0.1-SNAPSHOT csmall-stock-webapi 0.0.1-SNAPSHOT csmall-stock-webapi csmall-stock-webapi org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-dubbo cn.tedu csmall-stock-service 0.0.1-SNAPSHOT io.seata seata-spring-boot-starter com.github.pagehelper pagehelper-spring-boot-starter com.alibaba fastjson ``` ## 配置文件 ```yaml spring: application: name: nacos-stock #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false datasource: url: jdbc:mysql://localhost:3306/csmall_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root dubbo: protocol: #协议 #设置为-1 表示启动dubbo自动寻找可用端口号的功能 port: -1 name: dubbo #指定注册到哪个Nacos注册中心 registry: address: nacos://localhost:8849 #注册中心地址 #当前项目启动时,是否检查本项目需要的所有远程服务是否启动 #假如两个服务为互相调用关系,那么这两个服务启动时,都会去检查对方是否启动,如果都设置为true,那么就会出现死锁 #设置为false表示不检查,这样就不会因为需要的服务没启动而报错了 consumer: check: false seata: # 定义一个事务的分组名称,同一个微服务项目的各个模块名称应该一致,这个名称就是用来区分不同项目的 tx-service-group: csmall_group service: vgroup-mapping: # 设置csmall_group分组使用的事务策略,default表示使用默认策略配置 csmall_group: default grouplist: #设置seata的ip地址和端口号 default: localhost:8091 ``` **添加注解,只需要再business的实现类上添加即可,因为全局事务只需要一个起点** ```java package cn.tedu.csmall.business.service.impl; import cn.tedu.csmall.business.service.IBusinessService; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.order.service.IOrderService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.stereotype.Service; @Service @DubboService @Slf4j public class BusinessServiceImpl implements IBusinessService { @DubboReference private IOrderService DubboOrderService; //Global:全局 Transactional:事务 //业务逻辑层实现类方法上添加这个注解 //相当于标记设置了seata分布式事务的起点,是AT模型的TM(事务管理器) //效果:从这个方法开始,眼神出的所有远程调用(Dubbo)对数据库的操作,都在同一个事务中 //在同一个事务中的所有数据库操作,要么都成功,要么都失败 @GlobalTransactional @Override public void buy() { // 先实例化一个新增的订单对象 OrderAddDTO orderAddDTO = new OrderAddDTO(); orderAddDTO.setUserId("UU100"); orderAddDTO.setCount(10); orderAddDTO.setMoney(199); orderAddDTO.setCommodityCode("PC100"); //这个实例化的DTO对象,需要传递给订单order模块,让订单模块去新增订单 log.info("开始调用订单模块的新增订单的方法,传递的参数是:{}",orderAddDTO); DubboOrderService.OrderAdd(orderAddDTO); } } ``` 测试一下seata是否生效,再order订单这层写一个故意抛出的异常来检测seata是否生效 ```java package cn.tedu.csmall.order.webapi.impl; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.order.webapi.mapper.OrderMapper; import cn.tedu.csmall.order.service.IOrderService; import cn.tedu.csmall.stock.service.IStockService; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @DubboService @Service @Slf4j public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; //添加@DubboReference注解,表示当前业务逻辑层需要消费其他模块 //直接下面生命的业务逻辑接口,必须在Nacos中有对应的注册实现类 //调用时,Dubbo可以自动获取Nacos中注册的业务逻辑层实现类对象 @DubboReference private IStockService stockService; @DubboReference private ICartService cartService; @Override public void OrderAdd(OrderAddDTO orderAddDTO) { //1.这里要先进行减少数据库中库存的操作(stock模块) StockReduceCountDTO stockReduceCountDTO = new StockReduceCountDTO(); stockReduceCountDTO.setReduceCount(orderAddDTO.getCount()); stockReduceCountDTO.setCommodityCode(orderAddDTO.getCommodityCode()); stockService.reduceStock(stockReduceCountDTO); //2.还要从购物车中删除用户选中的商品(cart模块) cartService.deleteUserCart(orderAddDTO.getUserId(),orderAddDTO.getCommodityCode()); if(Math.random()>0.5){ throw new CoolSharkServiceException( ResponseCode.INTERNAL_SERVER_ERROR,"故意抛出的异常" ); } //3.上面操作完成后在进行订单的新增 Order order = new Order(); BeanUtils.copyProperties(orderAddDTO,order); //执行新增 int i = orderMapper.insertOrder(order); log.info("新增订单成功,插入了:{}条",i); } } ``` 可以看到我执行了两次操作 第一次成功提示 commit status: Committed 事务提交 第二次失败提示 rollback status: Rollbacked 事务回滚 ![img_13.png](photo/img_13.png) ## AT模式 AT模式的运行有一个非常明显的前提条件,这个条件不满足,就无法使用AT模式 需要注意的一点是 只有关系型数据库才支持提交和回滚,其他非关系型数据库都是直接影响数据(例如Redis) 所以如果业务过程中有一个节点操作的是Redis或其他非关系型数据库,就无法使用AT模式 ## TCC模式 TCC简单来说就是自己编写代码完成事务的提交和回滚,在TCC模式下,我们需要为参与事务的业务逻辑编辑一组共三个方法, 相当于一个模块的方法拆分成三分,本别对应 * prepare: 准备 * commit: 提交 * rollback: 回滚 **prepare方法是每个模块都会运行的方法** **prepare方法都运行正常时,运行commit** **prepare运行错误时,运行rollback** ## SAGA模式 SAGA模式的思想是对应每个业务逻辑层编写一个新的类,可以设置指定的业务逻辑层方法发生异常时,运行新编写的类中的代码 相当于将TCC 模式中的rollback方法定义在一个新的类中 这样写代码就不影响写好的业务逻辑代码 每一个事务的分支都需要一个新的类来回滚 会造成类特别多,开发的需求量特别大,属于下下策基本不会使用 # Sentinel **Sentinel** 也是Spring Cloud Alibaba的组件 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。**Sentinel**以流量为切入点,从流量控制、熔断降级、系统负载保护 等多个维度保护服务的稳定性 为了保证服务器运行的稳定性,在请求书达到设计最大值时,将过剩的请求限流,保证在设计的请求书内的请求能够稳定完成处理 * 丰富的应用场景 双十一,秒杀模块,12306抢火车票 * 完备的实时状态监控 可以支持显示当前项目各个服务的运行和压力状态,分析出每台服务器处理的秒级别的数据 * 广泛的开源生态 很多技术可以和Sentinel进行整合,SpringCloudDubbo,而且依赖少配置简单 * 完善的SPI扩展 Sentinel支持程序设置各种自定义的规则,可以根据自己的业务需求,自定义限流规则 ## 启动Sentinel Sentinel的启动也是通过脚本启动,在bin目录下执行sentinel.bat脚本 java -jar sentinel-dashboard-1.8.2.jar --server.port=8090 指定端口号启动,因为我本地还有其他项目使用8080端口,所以指定其他端口启动 ## Sentinel页面 ![img_14.png](photo/img_14.png) **默认账号密码都是sentinel** ![img_15.png](photo/img_15.png) ## 配置Sentinel 我们的限流针对的是**控制器方法** 我们找一个模块来测试和观察一下效果 这里以stock模块为例 添加依赖 ```xml 4.0.0 cn.tedu csmall-stock 0.0.1-SNAPSHOT cn.tedu csmall-stock-webapi 0.0.1-SNAPSHOT csmall-stock-webapi csmall-stock-webapi org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid mysql mysql-connector-java cn.tedu csmall-commons 0.0.1-SNAPSHOT com.github.xiaoymin knife4j-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-dubbo cn.tedu csmall-stock-service 0.0.1-SNAPSHOT io.seata seata-spring-boot-starter com.github.pagehelper pagehelper-spring-boot-starter com.alibaba fastjson com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` stock-webapi配置文件 ```yaml spring: application: name: nacos-stock #设置当前项目的名称,注册到nacos的名称与这个相同 cloud: sentinel: #开启sentinel transport: #配置sentinel的数据仪表台的位置 dashboard: localhost:8090 #sentinel的控制台地址 port: 8719 #sentinel的客户端的端口号 nacos: discovery: server-addr: localhost:8849 #nacos服务地址 #ephemeral: false datasource: url: jdbc:mysql://localhost:3306/csmall_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root ``` 开始使用sentinel进行限流 ![img_16.png](photo/img_16.png) ![img_17.png](photo/img_17.png) 这时候发送请求,连续点击会出现 ![img_18.png](photo/img_18.png) 情况,代表限流成功 ### 自定限流提示 可以看到我之前的虽然限流成功但是返回的message为null,这在常规开发中是不对的,需要给定一些信息来提醒用户 也就是所谓的错误信息 ### QPS 在控制器方法下添加一个限流方法规则 ```java package cn.tedu.csmall.stock.webapi.controller; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.service.IStockService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/stock") @Api(tags = "库存业务模块") public class StockController { @Autowired private IStockService stockService; @PostMapping("/reduce") @ApiOperation(value = "扣减库存") //是添加在控制器上的方法,在这个方法运行一次后 //会在Sentinel仪表盘中显示,仙逝后可以设置这个方法的限流策略 //如果方法不运行,仪表台也不显示,也无法配置 @SentinelResource(value="减少库存的方法",blockHandler = "blockError") public JsonResult reduceStock(StockReduceCountDTO stockReduceCountDTO) { stockService.reduceStock(stockReduceCountDTO); return JsonResult.ok("库存扣减成功"); } //Sentinel自定义限流方法规则 //1 访问修饰符必须是public //2 返回值类型必须与被限流的控制器返回类型一致 //3 方法名必须和@SentinelResource(value="减少库存的方法",blockHandler = "blockError")中的blockHandler名称一致 //4 参数列表也和控制器方法一致,在末尾添加一个BlockException类似的参数 public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO, BlockException e){ return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"服务器忙!请稍后再试"); } } ``` 再次测试成功出现报错信息 ![img_19.png](photo/img_19.png) ### 并发线程数 再测试一下并发线程数的使用方法,限流1,值得注意的是我们只有一条线程,所以需要让这个线程长一点,添加一个休眠 ```Java package cn.tedu.csmall.stock.webapi.controller; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.service.IStockService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/stock") @Api(tags = "库存业务模块") public class StockController { @Autowired private IStockService stockService; @PostMapping("/reduce") @ApiOperation(value = "扣减库存") //是添加在控制器上的方法,在这个方法运行一次后 //会在Sentinel仪表盘中显示,仙逝后可以设置这个方法的限流策略 //如果方法不运行,仪表台也不显示,也无法配置 @SentinelResource(value="减少库存的方法",blockHandler = "blockError") public JsonResult reduceStock(StockReduceCountDTO stockReduceCountDTO) { stockService.reduceStock(stockReduceCountDTO); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } return JsonResult.ok("库存扣减成功"); } //Sentinel自定义限流方法规则 //1 访问修饰符必须是public //2 返回值类型必须与被限流的控制器返回类型一致 //3 方法名必须和@SentinelResource(value="减少库存的方法",blockHandler = "blockError")中的blockHandler名称一致 //4 参数列表也和控制器方法一致,在末尾添加一个BlockException类似的参数 public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO, BlockException e){ return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"服务器忙!请稍后再试"); } } ``` 测试结果 ![img_20.png](photo/img_20.png) 成功限流,Knief4j使用的时候要注意一下,他没法同时发请求,可以开两个网页同时发 ### 扩展 Sentinel限流算法 QPS使用的是**令牌桶**算法 并发线程数是**线程池**算法 ## 熔断 ![img_21.png](photo/img_21.png) ### 慢调用比例 最大RT就是指如果这个请求超过多少毫秒,就代表他是慢调用比例; 比例阈值是指比如有五个请求, 其中有一个请求超过了最大RT,那么这个比例就是1/5,也就是20%,就填0.2; 熔断时长就是指停止时长,比如填写5秒,就代表,当有5个请求请求时,有2个超过30毫秒,就停止5秒; ![img_22.png](photo/img_22.png) ### 异常比例 异常比例就是指,如果有五个请求,其中有一个请求发生了异常,那么这个比例就是1/5,也就是20%,就填0.2; ![img_23.png](photo/img_23.png) ### 异常数 异常数就是指,如果有五个请求,其中有一个请求发生了异常,那么这个数就是1,就填1; ### 自定义降级方法 所谓的降级就是正常运行方法过程中,发生了异常,Sentinel支持我们运行别的方法来处理异常,或运行别的业务流程, 和统一异常处理类有类似的地方 但是Sentinel降级方法优先级高,而且针对单一控制器方法编写 **@SentinelResource**注解中可以定义处理降级情况的方法 ## Stock模块 ### controller ```java package cn.tedu.csmall.stock.webapi.controller; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.service.IStockService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/stock") @Api(tags = "库存业务模块") public class StockController { @Autowired private IStockService stockService; @PostMapping("/reduce") @ApiOperation(value = "扣减库存") //是添加在控制器上的方法,在这个方法运行一次后 //会在Sentinel仪表盘中显示,仙逝后可以设置这个方法的限流策略 //如果方法不运行,仪表台也不显示,也无法配置 @SentinelResource(value="减少库存的方法",blockHandler = "blockError",fallback = "fallbackError") //fallback属性是设置在当前方法运行异常时,运行的自定义降级方法名称 //fallback比blockHnadler优先级高 public JsonResult reduceStock(StockReduceCountDTO stockReduceCountDTO) { stockService.reduceStock(stockReduceCountDTO); // try { // Thread.sleep(5000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } return JsonResult.ok("库存扣减成功"); } //Sentinel自定义限流方法规则 //1 访问修饰符必须是public //2 返回值类型必须与被限流的控制器返回类型一致 //3 方法名必须和@SentinelResource(value="减少库存的方法",blockHandler = "blockError")中的blockHandler名称一致 //4 参数列表也和控制器方法一致,在末尾添加一个BlockException类型的参数 public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO, BlockException e){ return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"服务器忙!请稍后再试"); } //Sentinel自定义降级方法规则 //1 访问修饰符必须是public //2 返回值类型必须与被限流的控制器返回类型一致 //3 方法名必须和@SentinelResource(value="减少库存的方法",fallback = "fallbackError")中的fallback名称一致 //4 参数列表也和控制器方法一致,在末尾添加一个Throwable类型的参数,因为限流方法是抛出一个限流的异常,但是降级方法可能抛出的异常类型不确定,所以这里使用Throwable public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO, Throwable e){ //先输出异常的信息 e.printStackTrace(); return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"运行方法发生异常,执行降级方法:"+e.getMessage()); } } ``` ## Order模块 ### controller ```java package cn.tedu.csmall.order.webapi.controller; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.order.service.IOrderService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") @Api(tags = "订单管理模块") public class OrderController { @Autowired private IOrderService orderService; @PostMapping("/add") @ApiOperation("新增订单") @SentinelResource(value = "新增订单方法",blockHandler = "OrderAddHandler",fallback = "OrderAddFallbackHandler") public JsonResult OrderAdd(OrderAddDTO orderAddDTO){ orderService.OrderAdd(orderAddDTO); // try { // Thread.sleep(5000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } return JsonResult.ok("新增订单成功!"); } public JsonResult OrderAddHandler(OrderAddDTO orderAddDTO, BlockException e){ return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"服务器忙,请稍后再试!"); } public JsonResult OrderAddFallbackHandler(OrderAddDTO orderAddDTO,Throwable e){ e.printStackTrace(); return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"运行发生异常,执行降级方法:"+e.getMessage()); } } ``` **注意注意!** Orde模块本身是有随机异常的,但是我注掉了,记得把他改回来,这样就不需要再写一个 异常来测试降级方法了 ```java package cn.tedu.csmall.order.webapi.impl; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.order.webapi.mapper.OrderMapper; import cn.tedu.csmall.order.service.IOrderService; import cn.tedu.csmall.stock.service.IStockService; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @DubboService @Service @Slf4j public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; //添加@DubboReference注解,表示当前业务逻辑层需要消费其他模块 //直接下面生命的业务逻辑接口,必须在Nacos中有对应的注册实现类 //调用时,Dubbo可以自动获取Nacos中注册的业务逻辑层实现类对象 @DubboReference private IStockService stockService; @DubboReference private ICartService cartService; @Override public void OrderAdd(OrderAddDTO orderAddDTO) { //1.这里要先进行减少数据库中库存的操作(stock模块) StockReduceCountDTO stockReduceCountDTO = new StockReduceCountDTO(); stockReduceCountDTO.setReduceCount(orderAddDTO.getCount()); stockReduceCountDTO.setCommodityCode(orderAddDTO.getCommodityCode()); stockService.reduceStock(stockReduceCountDTO); //2.还要从购物车中删除用户选中的商品(cart模块) cartService.deleteUserCart(orderAddDTO.getUserId(),orderAddDTO.getCommodityCode()); ===> if(Math.random()>0.5){ ===> throw new CoolSharkServiceException( ===> ResponseCode.INTERNAL_SERVER_ERROR,"故意抛出的异常" ===> ); ===> } //3.上面操作完成后在进行订单的新增 Order order = new Order(); BeanUtils.copyProperties(orderAddDTO,order); //执行新增 int i = orderMapper.insertOrder(order); log.info("新增订单成功,插入了:{}条",i); } } ``` ## 扩展 在2020年之前奈非的框架非常火,它有一些和spring cloud 对应的组件或服务 ![img_24.png](photo/img_24.png) # 网关Gateway Gateway是Spring提供的,不是阿里写的 网关:就是值网络中的关口\关卡,网关就是当前微服务项目的统一入口. 这里是一个demo,主要是为了讲解网关,因为原项目不好配合gateway讲解 后面会有csmall-gateway项目 ## 网关的原理 方便对所有的接口进行统一管理 ![img_25.png](photo/img_25.png) ## 网关的作用 * 将所有请求统一经过网关 * 网关可以对这些请求进行检查 * 网关方便记录所有请求的日志 * 网关可以统一将所有请求路由到正确的模块\服务上 "路由"的近义词就是"分配" 图中显示的,只有外部的请求才走网关,内部之间的请求不需要走网关(例如Dubbo) ## gateway-demo ### 配置文件 ```yaml server: port: 9000 spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: #------------------------动态路由-------------------------- #可以同时开启动态路由和手动路由 discovery: locator: # 开启动态路由功能,默认是关闭的 # 动态路由规则:在网关端口号后,先编写要路由目标服务器注册到Nacos的名称 # 在编写访问这个服务器的具体路径 enabled: true #------------------------手动路由-------------------------- # routes就是路由的意思,下面就是配置路由信息 # 一个网关项目大多会配置很多路由 # 所以这个网关配置是一个List集合类型 routes: - id: gateway-shanghai uri: lb://shanghai predicates: - Path=/sh/** # List类型元素赋值时,每个元素都要以"-"开头,在这个"-"之后, # 编写的所有内容,都是同一个对象的属性值 # id设置当前路由的名称,也是唯一标识,和其它配置没有对应关系,注意不能和之后的id名称重复即可 - id: gateway-beijing # uri属性配置的是路由目标服务器的名称,"beijing"指注册到Nacos名称为"beijing"的模块 # lb就是负载均衡LoadBalance的缩写,标识路由支持负载均衡 uri: lb://beijing # predicates也是一个List类型的属性,所以他赋值也要以"-"开头 # predicates是断言的意思,是指满足某些条件,执行某些操作 predicates: #下面是断言的内容,Path表示判断路径,"/bj/**"表示判断路径是否以"/bj/"开头 # 条件满足时就会按照上面的Uri配置,路由到该服务器模块 - Path=/bj/** ``` ### controller ```java package cn.tedu.gate.shanghai.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.ZonedDateTime; @RestController @RequestMapping("/sh") public class ShanghaiController { @GetMapping("/show") public String show(){ return "这里是上海!"; } } ``` 启动gate-shanghai模块和gateway模块,前提是启动nacos,因为gateway需要读取nacos中其他模块的名称来进行路由 ![img_26.png](photo/img_26.png) 启动之后通过网关的9000端口访问shanghai模块的控制器,访问成功 Path只是SpringGateway提供的多种断言中的一个,还有很多其他的断言 * **After** 先打印一个时间格式,因为gateway只接受这种时间格式 ```yaml server: port: 9000 spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8849 gateway: # discovery: # locator: # # 开启动态路由功能,默认是关闭的 # # 动态路由规则:在网关端口号后,先编写要路由目标服务器注册到Nacos的名称 # # 在编写访问这个服务器的具体路径 # enabled: true # routes就是路由的意思,下面就是配置路由信息 # 一个网关项目大多会配置很多路由 # 所以这个网关配置是一个List集合类型 routes: - id: gateway-shanghai uri: lb://shanghai predicates: - Path=/sh/** # After是时间断言,判断当前时间是否晚于配置时间 # 如果成立正常访问,不成立,返回404,多个断言是"与"的关系 # Path也是断言 - After=//2024-01-08T15:00:52.030+08:00[Asia/Shanghai] #这个Shanghai跟上海模块没有关系,是Spring决定的,只是凑巧 # List类型元素赋值时,每个元素都要以"-"开头,在这个"-"之后, # 编写的所有内容,都是同一个对象的属性值 # id设置当前路由的名称,也是唯一标识,和其它配置没有对应关系,注意不能和之后的id名称重复即可 - id: gateway-beijing # uri属性配置的是路由目标服务器的名称,"beijing"指注册到Nacos名称为"beijing"的模块 # lb就是负载均衡LoadBalance的缩写,标识路由支持负载均衡 uri: lb://beijing # predicates也是一个List类型的属性,所以他赋值也要以"-"开头 # predicates是断言的意思,是指满足某些条件,执行某些操作 predicates: #下面是断言的内容,Path表示判断路径,"/bj/**"表示判断路径是否以"/bj/"开头 # 条件满足时就会按照上面的Uri配置,路由到该服务器模块 - Path=/bj/** ``` 可以看右下角时间15:01 访问404 ![img_27.png](photo/img_27.png) 15:03 访问正常显示 ![img_28.png](photo/img_28.png) ```java package cn.tedu.gate.shanghai.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.ZonedDateTime; @RestController @RequestMapping("/sh") public class ShanghaiController { @GetMapping("/show") public String show(){ //2024-01-08T14:54:52.030+08:00[Asia/Shanghai] System.out.println(ZonedDateTime.now()); return "这里是上海!"; } } ``` * Before ```yaml - Before=2024-01-09T09:33:00.030+08:00[Asia/Shanghai] ``` * Between ```yaml - Between=2024-01-09T09:33:00.030+08:00[Asia/Shanghai],2024-01-09T09:38:00.030+08:00[Asia/Shanghai] ``` * Cookie * Header * Host * Method * Path * Query Query之后如果是get请求需要浏览器地址是 http://localhost:9000/sh/show?username ```yaml - Query=username #Query是查询参数,判断是否有username=admin的参数,如果没有发生404错误 ``` * Remoteaddr ### 内置过滤器 filters: - AddRequestParameter=age,18是代表默认会给一个age的参数值为18,即使你传或者在浏览器上不写url路径依然会传一个age属性给后端 ```yaml routes: - id: gateway-shanghai uri: lb://shanghai filters: #过滤器 - AddRequestParameter=age,18 predicates: - Path=/sh/** # After是时间断言,判断当前时间是否晚于配置时间 # 如果成立正常访问,不成立,返回404,多个断言是"与"的关系 # Path也是断言 # - After=2024-01-08T15:02:52.030+08:00[Asia/Shanghai] #这个Shanghai跟上海模块没有关系,是Spring决定的,只是凑巧 # - Before=2024-01-09T09:33:00.030+08:00[Asia/Shanghai] #- Between=2024-01-09T09:33:00.030+08:00[Asia/Shanghai],2024-01-09T09:38:00.030+08:00[Asia/Shanghai] - Query=username #Query是查询参数,判断是否有username=admin的参数,如果没有发生404错误 # List类型元素赋值时,每个元素都要以"-"开头,在这个"-"之后, # 编写的所有内容,都是同一个对象的属性值 # id设置当前路由的名称,也是唯一标识,和其它配置没有对应关系,注意不能和之后的id名称重复即可 - id: gateway-beijing # uri属性配置的是路由目标服务器的名称,"beijing"指注册到Nacos名称为"beijing"的模块 # lb就是负载均衡LoadBalance的缩写,标识路由支持负载均衡 uri: lb://beijing # predicates也是一个List类型的属性,所以他赋值也要以"-"开头 # predicates是断言的意思,是指满足某些条件,执行某些操作 predicates: #下面是断言的内容,Path表示判断路径,"/bj/**"表示判断路径是否以"/bj/"开头 # 条件满足时就会按照上面的Uri配置,路由到该服务器模块 - Path=/bj/** ``` ## gateway 创建一个gateway模块 ### 配置依赖 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu gateway 0.0.1-SNAPSHOT gateway gateway org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-loadbalancer com.github.xiaoymin knife4j-spring-boot-starter ``` ### 配置文件 ```yaml server: port: 19000 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: localhost:8849 gateway: discovery: locator: enabled: true main: web-application-type: reactive #防止springMvc和SpringGateway依赖冲突的配置 ``` ### config ```java package cn.tedu.gateway.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @Component public class SwaggerProvider implements SwaggerResourcesProvider { /** * 接口地址 */ public static final String API_URI = "/v2/api-docs"; /** * 路由加载器 */ @Autowired private RouteLocator routeLocator; /** * 网关应用名称 */ @Value("${spring.application.name}") private String applicationName; @Override public List get() { //接口资源列表 List resources = new ArrayList<>(); //服务名称列表 List routeHosts = new ArrayList<>(); // 获取所有可用的应用名称 routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null) .filter(route -> !applicationName.equals(route.getUri().getHost())) .subscribe(route -> routeHosts.add(route.getUri().getHost())); // 去重,多负载服务只添加一次 Set existsServer = new HashSet<>(); routeHosts.forEach(host -> { // 拼接url String url = "/" + host + API_URI; //不存在则添加 if (!existsServer.contains(url)) { existsServer.add(url); SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setUrl(url); swaggerResource.setName(host); resources.add(swaggerResource); } }); return resources; } } ``` ### controller ```java package cn.tedu.gateway.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import springfox.documentation.swagger.web.*; import java.util.Optional; @RestController @RequestMapping("/swagger-resources") public class SwaggerController { @Autowired(required = false) private SecurityConfiguration securityConfiguration; @Autowired(required = false) private UiConfiguration uiConfiguration; private final SwaggerResourcesProvider swaggerResources; @Autowired public SwaggerController(SwaggerResourcesProvider swaggerResources) { this.swaggerResources = swaggerResources; } @GetMapping("/configuration/security") public Mono> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("/configuration/ui") public Mono> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("") public Mono swaggerResources() { return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); } } ``` ### filter ```java package cn.tedu.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @Component public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory { private static final String HEADER_NAME = "X-Forwarded-Prefix"; private static final String URI = "/v2/api-docs"; @Override public GatewayFilter apply(Object config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); if (!StringUtils.endsWithIgnoreCase(path,URI )) { return chain.filter(exchange); } String basePath = path.substring(0, path.lastIndexOf(URI)); ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build(); ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); }; } } ``` 按顺序启动Stock\Cart、Order、Business、gateway 先访问Stock模块,看一下是否正常 正常就通过网关再访问 http://localhost:19000/nacos-cart/doc.html ### 网关冲突 ```yaml main: web-application-type: reactive #防止springMvc和SpringGateway依赖冲突的配置 ``` 之前在配置文件中添加过一段依赖,是因为SpringMvc和SpringGateway依赖冲突,所以需要添加这个配置 他们是两个服务器,一个是Tomcat一个是Netty服务器, # ElasticSearch ## 什么是ElasticSearch ElasticSearch是一个基于Lucene的搜索服务器,功能类似一个数据库,能高效的从大量数据中搜索匹配指定关键字的内容 它也将数据保存在硬盘中,本质是一个java项目,使用它进行数据的增删改查就是访问这个项目的控制器(url路径),这个API提供了全文搜索引擎核心操作的接口,相当于搜索引擎 的核心支持,ES是再Lucene的基础上进行完善,实现了开箱即用 和ES类似的软件有 **Solr/MongoDB** 数据库进行模糊查询的时候效率严重低下,所有关系型数据库都有这个缺点 ![img_29.png](photo/img_29.png) 数据库索引分为两大类 * 聚集索引 聚集索引就是数据库保存数据的物理顺序依据,默认是主键Id,所以按id查询数据苦衷的数据效率非常高 * 非聚集索引 非聚集索引就是如果想在非主键列添加索引,就是非聚集索引了 1. 创建索引会占用硬盘空间 2. 创建索引会对数据库的增删改查操作产生影响,所以索引不是越多越好,而是要根据业务需求来创建索引 3. 对数据库进行批量新增时,先删除索引,增加完毕后再创建 4. 不要对数据样本少地列添加索引 5. 模糊查询时,查询条件前模糊的情况无法启用索引 6. 每次查询的数据比例越高,索引的效率越低 ## ElasticSearch的运行原理 要使用ES提高模糊查询效率,首先要把数据库的数据复制到ES中,再新增数据到ES的过程中,ES可以对指定的列进行分词索引 形成倒排索引结构 ## ElasticSearch的使用 我使用的7.6.2 已经上传在Tools文件夹了,找到bin目录启动elasticsearch.bat就可以了 ## search模块 新建一个search模块,依旧是老一套的父子相认,然后注意不要删除依赖了 要保留test依赖,因为后续需要测试用 ### pom依赖 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu search 0.0.1-SNAPSHOT search search org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test ``` ### 测试文件 在模块下新建一个elasticsearch.http,我是idea2023版本,并且汉化了, ![img_30.png](photo/img_30.png) 打开文件开始测试,测试之前要先启动ES,然后在测试文件中输入 ```http request ### 三个# 表示多个请求之间的分割,也可以用作注释,否则会报错 GET http://localhost:9200 ``` 注意 **###** 是必须有的,表示分割请求也是注释, **如果没有的话会报错** ![img_31.png](photo/img_31.png) 再来测试一下分词功能 ```http request ### 三个# 表示多个请求之间的分割,也可以用作注释,否则会报错 GET http://localhost:9200 ### 测试es分词功能 POST http://localhost:9200/_analyze Content-Type: application/json { "analyzer": "standard", "text": "my name is hanmeimei" } ``` 可以看到输出的结果 ```json HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 158 { "tokens": [ { "token": "my", "start_offset": 0, "end_offset": 2, "type": "", "position": 0 }, { "token": "name", "start_offset": 3, "end_offset": 7, "type": "", "position": 1 }, { "token": "is", "start_offset": 8, "end_offset": 10, "type": "", "position": 2 }, { "token": "hanmeimei", "start_offset": 11, "end_offset": 20, "type": "", "position": 3 } ] } 响应文件已保存。 > 2024-01-10T150444.200.json Response code: 200 (OK); Time: 18ms (18 ms); Content length: 344 bytes (344 B) ``` 但是standard是不支持中文分词的,所以如果我们输入一组中文,他只会把每个字拿出来,并不会组成成语 不满足我们后续的要求,所以需要使用一个中文分词的词库elasticsearch-analysis-ik 我使用的时7.6.2版本,**注意!**,这个ik词库版本必须与ES版本一致,否则会出现错误,所以当我们使用的时候,需要下载对应版本的ik词库 ### 中文分词库-ik使用 ![img_32.png](photo/img_32.png) 把这些文件复制到plugins/ik文件夹中,没有ik就自己新建一个文件夹 ![img_33.png](photo/img_33.png) 复制完后重启Elasticsearch 再次测试分词功能 ```http request ### 测试es分词功能 POST http://localhost:9200/_analyze Content-Type: application/json { "analyzer": "ik_smart", "text": "我叫樱木花道" } ``` ```json HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 153 { "tokens": [ { "token": "我", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "叫", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "樱木花道", "start_offset": 2, "end_offset": 6, "type": "CN_WORD", "position": 2 } ] } 响应文件已保存。 > 2024-01-10T152543.200.json Response code: 200 (OK); Time: 149ms (149 ms); Content length: 243 bytes (243 B) ``` 可以看到樱木花道的词语已经可以显示了,说明在词库中有樱木花道的词语,所以可以分词出来 ```http request ### 测试es分词功能 POST http://localhost:9200/_analyze Content-Type: application/json { "analyzer": "ik_smart", "text": "我叫王祖贤" } ``` ```json HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 168 { "tokens": [ { "token": "我", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "叫", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "王", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 2 }, { "token": "祖", "start_offset": 3, "end_offset": 4, "type": "CN_CHAR", "position": 3 }, { "token": "贤", "start_offset": 4, "end_offset": 5, "type": "CN_CHAR", "position": 4 } ] } 响应文件已保存。 > 2024-01-10T152745.200.json Response code: 200 (OK); Time: 5ms (5 ms); Content length: 392 bytes (392 B) ``` 可以看到,这个分词库也并不是很全面,并没有王祖贤这个词语,所以分词出来的结果就是每个字 ```json POST http://localhost:9200/_analyze HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 214 { "tokens": [ { "token": "明天", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "我", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 1 }, { "token": "要去", "start_offset": 3, "end_offset": 5, "type": "CN_WORD", "position": 2 }, { "token": "电影院", "start_offset": 5, "end_offset": 8, "type": "CN_WORD", "position": 3 }, { "token": "看", "start_offset": 8, "end_offset": 9, "type": "CN_CHAR", "position": 4 }, { "token": "灌篮高手", "start_offset": 9, "end_offset": 13, "type": "CN_WORD", "position": 5 } ] } 响应文件已保存。 > 2024-01-10T152843.200.json Response code: 200 (OK); Time: 3ms (3 ms); Content length: 476 bytes (476 B) ``` ```http request ### 测试es分词功能 POST http://localhost:9200/_analyze Content-Type: application/json { "analyzer": "ik_smart", "text": "明天我要去电影院看灌篮高手" } ``` 就不多做尝试了,感兴趣的可以自己试 有两种查询方式,一种是我演示的ik_smart 另一种是ik_max_word ik_smart 优点是速度快,缺点是分词不够细致,可能跳过一部分重要分词,导致查询的结果不全面,查全率低 ik_max_word 有点是详细的文字片段,查询时查全率高,不容易遗漏数据 缺点是速度慢,分词太详细,导致有一些无用分词,占用空间大 ```http request ### 设置index中的文档属性采用ik分词 POST http://localhost:9200/questions/_mapping Content-Type: application/json { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" } } } ``` properties是属性的意思,title和content两个属性的分词方式为ik_max_word ### 新增分词 ```http request ### questions 中添加文档 POST http://localhost:9200/questions/_create/1 Content-Type: application/json { "id":1, "title":"Java基本数据类型有哪些", "content":"面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/2 Content-Type: application/json { "id":2, "title":"int类型的范围", "content":"为啥要了解int类型的范围呢?" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/3 Content-Type: application/json { "id":3, "title":"常用集合类有哪些", "content":"为啥企业经常问集合呀?该如何回复呢" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/4 Content-Type: application/json { "id":4, "title":"线程的run方法和start方法有啥区别", "content":"run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?" } ``` ```json POST http://localhost:9200/questions/_create/4 HTTP/1.1 201 Created Location: /questions/_doc/4 content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 142 { "_index": "questions", "_type": "_doc", "_id": "4", "_version": 3, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 5, "_primary_term": 1 } 响应文件已保存。 > 2024-01-17T133856.201.json Response code: 201 (Created); Time: 7ms (7 ms); Content length: 157 bytes (157 B) ``` ### 查询分词 ```http request ### 查询数据 GET http://localhost:9200/questions/_doc/4 ``` ```json GET http://localhost:9200/questions/_doc/4 HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 258 { "_index": "questions", "_type": "_doc", "_id": "4", "_version": 1, "_seq_no": 3, "_primary_term": 1, "found": true, "_source": { "id": 4, "title": "线程的run方法和start方法有啥区别", "content": "run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?" } } 响应文件已保存。 > 2024-01-17T132547.200.json Response code: 200 (OK); Time: 3ms (3 ms); Content length: 218 bytes (218 B) ``` ### 删除分词 ```http request ### 删除questions中的一个文档 DELETE http://localhost:9200/questions/_doc/4 ``` ```json DELETE http://localhost:9200/questions/_doc/4 HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 141 { "_index": "questions", "_type": "_doc", "_id": "4", "_version": 2, "result": "deleted", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 4, "_primary_term": 1 } 响应文件已保存。 > 2024-01-17T133853.200.json Response code: 200 (OK); Time: 14ms (14 ms); Content length: 157 bytes (157 B) ``` ### 更新分词(修改) ```http request ### 更新questions索引中的文档 POST http://localhost:9200/questions/_doc/4/_update Content-Type: application/json { "doc": { "title": "Java线程的run方法和start方法有啥区别" } } ``` ```json POST http://localhost:9200/questions/_doc/4/_update HTTP/1.1 200 OK Warning: 299 Elasticsearch-7.6.2-ef48eb35cf30adf4db14086e8aabd07ef6fb113f "[types removal] Specifying types in document update requests is deprecated, use the endpoint /{index}/_update/{id} instead." content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 143 { "_index": "questions", "_type": "_doc", "_id": "4", "_version": 4, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 6, "_primary_term": 1 } 响应文件已保存。 > 2024-01-17T133952.200.json Response code: 200 (OK); Time: 16ms (16 ms); Content length: 157 bytes (157 B) ``` ### 收集分词 ```http request ### 收索 ES POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "match": {"title": "类型" } } } ``` ```json POST http://localhost:9200/questions/_search HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 467 { "took": 1064, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 3, "relation": "eq" }, "max_score": 2.1249857, "hits": [ { "_index": "questions", "_type": "_doc", "_id": "2", "_score": 2.1249857, "_source": { "id": 2, "title": "int类型的范围", "content": "为啥要了解int类型的范围呢?" } }, { "_index": "questions", "_type": "_doc", "_id": "1", "_score": 1.7996869, "_source": { "id": 1, "title": "Java基本数据类型有哪些", "content": "面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } }, { "_index": "questions", "_type": "_doc", "_id": "3", "_score": 0.7841127, "_source": { "id": 3, "title": "常用集合类有哪些", "content": "为啥企业经常问集合呀?该如何回复呢" } } ] } } 响应文件已保存。 > 2024-01-17T134240.200.json Response code: 200 (OK); Time: 1073ms (1 s 73 ms); Content length: 616 bytes (616 B) ``` 可以看到,查询到了三条数据,并且按照_score排序,分数越高,说明匹配度越高 ### 多字段搜索 查询的是所有title包含java类型分词的数据,或者content包含java类型分词的数据 ```http request POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "bool": { "should": [ { "match": { "title": "java类型" }}, { "match": { "content": "java类型"}} ] } } } ``` ```json POST http://localhost:9200/questions/_search HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 564 { "took": 4, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 4, "relation": "eq" }, "max_score": 4.901363, "hits": [ { "_index": "questions", "_type": "_doc", "_id": "1", "_score": 4.901363, "_source": { "id": 1, "title": "Java基本数据类型有哪些", "content": "面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } }, { "_index": "questions", "_type": "_doc", "_id": "2", "_score": 4.7406473, "_source": { "id": 2, "title": "int类型的范围", "content": "为啥要了解int类型的范围呢?" } }, { "_index": "questions", "_type": "_doc", "_id": "4", "_score": 0.9028312, "_source": { "id": 4, "title": "Java线程的run方法和start方法有啥区别", "content": "run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?" } }, { "_index": "questions", "_type": "_doc", "_id": "3", "_score": 0.7841127, "_source": { "id": 3, "title": "常用集合类有哪些", "content": "为啥企业经常问集合呀?该如何回复呢" } } ] } } 响应文件已保存。 > 2024-01-17T134953.200.json Response code: 200 (OK); Time: 9ms (9 ms); Content length: 787 bytes (787 B) ``` should表示或者,只要有一个条件满足就可以,所以这里查询到了四条数据,并且按照_score排序,分数越高,说明匹配度越高 如果是must就是在title中包含java类型分词的数据,并且content中也包含java类型分词的数据,这样就只有一条数据了 ```http request ### 多字段搜索 POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "bool": { "must": [ { "match": { "title": "java类型" }}, { "match": { "content": "java类型"}} ] } } } ``` ```json POST http://localhost:9200/questions/_search HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-encoding: gzip content-length: 391 { "took": 6, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 4.901363, "hits": [ { "_index": "questions", "_type": "_doc", "_id": "1", "_score": 4.901363, "_source": { "id": 1, "title": "Java基本数据类型有哪些", "content": "面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } }, { "_index": "questions", "_type": "_doc", "_id": "2", "_score": 4.7406473, "_source": { "id": 2, "title": "int类型的范围", "content": "为啥要了解int类型的范围呢?" } } ] } } 响应文件已保存。 > 2024-01-17T135331.200.json Response code: 200 (OK); Time: 10ms (10 ms); Content length: 466 bytes (466 B) ``` ### SpringBoot 操作 Elasticsearch #### Spring Data 简介 原生状态下,我们使用JDBC连接数据库,因为代码太频繁,所以使用Mybatis框架 在ES的原生状态下,我们需要使用socket访问ES,但是也很繁琐,所以可以使用SpringData简化 Spring Data是Spring提供的一套连接各种第三方数据源的框架集 我们需要使用的是其中的Spring Data Elasticsearch https://spring.io/projects/spring-data/ Spring Data官网 #### 依赖文件 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu search 0.0.1-SNAPSHOT search search org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.data spring-data-elasticsearch ``` #### 配合文件 ```properties # 配置ES的ip和端口 spring.elasticsearch.rest.uris=http://localhost:9200 # 设置项目日志门槛 logging.level.cn.tedu.search=debug # 设置SpringDataElasticsearch框架内部另一个能输入运行日志信息的门槛 logging.level.org.elasticsearch.client.RestClient=debug ``` #### 实体类 注意这个注解,既需要无参也需要有参,因为默认是无参方法,但是加了全参构造方法之后就不会再使用无参方法了,所以需要无参构造方法的注解 ```java package cn.tedu.search.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; @Data @Accessors(chain = true) // 链式set赋值 @AllArgsConstructor //生成包含全部参数的构造方法 @NoArgsConstructor //生成无参的构造方法 @Document(indexName = "items") //是SpringDataElasticSearch的注解,作用在类级别上,用来指定文档的索引名 运行时索引不存在indexName会自动创建 public class item { //SpringData通过id标记实体类的主键 @Id private Long id; //这个是要分词的字段 //@Field标记ES中的一个字段 @Field(type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word") private String title; //这个是不需要分词的字符串类型 @Field(type = FieldType.Keyword) private String category; @Field(type = FieldType.Keyword) private String brand; //不需要分词的Double数据类型 @Field(type = FieldType.Double) private Double price; //图片的路径不会作为查询条件,所以不需要创建索引 // index = false 就是设置当前字段不创建索引的配置,可以节省一定空间 // 但是imgPath Es还是进行保存的 @Field(type = FieldType.Keyword,index = false) private String imgPath; } ``` Spring Data框架对持久层的命名规则有所不同,mapper是mybatis的命名规则,spring Data的命名规则为repository 创建这个包,包中创建接口ItemRepository #### 接口 ```java package cn.tedu.search.repository; import cn.tedu.search.entity.Item; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; //Repository是Spring家族对持久层的命名规范 @Repository public interface ItemRepository extends ElasticsearchRepository { //ItemRepository继承了ElasticsearchRepository父接口,这个父接口由SpringData提供 //只要使用springData连接ES,就必须继承ElasticsearchRepository //ElasticsearchRepository的泛型参数,第一个是实体类类型,第二个是主键类型ElasticsearchRepository<实体类,主键> //继承之后,当前接口就可以直接使用父接口中声明的方法,包含了Item实体类基础的增删改查 } ``` #### 测试类 在测试类中测试新增修改查询删除操作 ```java package cn.tedu.search; import cn.tedu.search.entity.Item; import cn.tedu.search.repository.ItemRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.ArrayList; import java.util.List; import java.util.Optional; @SpringBootTest class SearchApplicationTests { @Autowired private ItemRepository itemRepository; @Test //新增es数据 void addOne() { //@Accessors(chain = true) 注解就可以这样直接通过.set来赋值,不用每次都调用对象的set方法 Item item = new Item() .setId(1L) .setTitle("罗技激光无线游戏鼠标") .setCategory("鼠标") .setBrand("罗技") .setPrice(199.0) .setImgPath("/img_1.png"); //利用Spring Data Elasticsearch 框架提供的新增方法,完成新增到Es itemRepository.save(item); System.out.println("新增成功"); } @Test //根据id查询es中数据 void getOne() { //Spring Data 提供了俺id查询es中数据的方法 //findById()方法的返回值是Optional,这个是JDK8的新特性,用来解决空指针异常 //返回值是Optional类型,并且定义了泛型,可以理解为一个只能储存一个元素的list Optional optional = itemRepository.findById(3L); Item item = optional.get(); System.out.println(item); } //批量新增 @Test void addList(){ List list = new ArrayList<>(); list.add(new Item(2L,"罗技激光有线办公鼠标","鼠标","罗技",88.0,"/img_2.png")); list.add(new Item(3L,"雷蛇机械无线游戏键盘","键盘","雷蛇",268.0,"/img_3.png")); list.add(new Item(4L,"微软有线静音办公鼠标","鼠标","微软",166.0,"/img_4.png")); list.add(new Item(5L,"罗技机械有线背光键盘","键盘","罗技",208.0,"/img_5.png")); itemRepository.saveAll(list); System.out.println("批量新增成功"); } @Test void getList(){ //findAll就是SpringData提供的从指定索引中全查所有数据方法 //Iterable items = itemRepository.findAll(); Iterable items = itemRepository.findAll(); for(Item item : items){ System.out.println(item); } System.out.println("============================================"); items.forEach(k -> System.out.println(k)); } } ``` ### SpringData自定义查询 SpringData框架提供的基本增删改查不能满足业务需要,如果是针对当前Es数据,进行个性化的自定义查询,那还是需要自己写查询代码 就像我们要实现根据关键词查询商品信息一样,完成类似数据库的模糊查询 #### 单条件查询 我们查询需求为输出所有数据中title属性包括"游戏"的数据 ```sql 参考数据库中模糊查询 select * from item where title like '%游戏%' ``` 我们使用SpringDataES进行查询,本质上还是相当于ES文档中执行的查询语句 在SpringData框架下,接口中的实现更加简单 ##### 接口 ```java package cn.tedu.search.repository; import cn.tedu.search.entity.Item; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; //Repository是Spring家族对持久层的命名规范 @Repository public interface ItemRepository extends ElasticsearchRepository { //ItemRepository继承了ElasticsearchRepository父接口,这个父接口由SpringData提供 //只要使用springData连接ES,就必须继承ElasticsearchRepository //ElasticsearchRepository的泛型参数,第一个是实体类类型,第二个是主键类型ElasticsearchRepository<实体类,主键> //继承之后,当前接口就可以直接使用父接口中声明的方法,包含了Item实体类基础的增删改查 //SpringData自定义查询 //我们可以编写一个符合SpringData框架要求格式的方法名 //框架会自动根据方法名生成对应的查询语句,并且执行查询,SpringData会根据我们定义方法名称推测出我们的意图 Iterable queryItemsByTitleMatches(String title); } ``` ##### 测试类 ```java //单条件自定义查询 @Test void queryOne(){ //查询Items中,title字段包含"游戏"的数据 itemRepository.queryItemsByTitleMatches("游戏").forEach(k -> System.out.println(k)); } ``` #### 多条件查询 在相对复杂的查询逻辑下,经常使用多个条件来定位查询需要的数据 这样就需要逻辑运算符"and"/"or" ItemRepository接空中添加多条件的查询方法 目标Title字段为"游戏"并且brand字段为"罗技"的数据\ 当查询条件为And时,查询语句关键字为must 当查询条件为Or时,查询语句关键字为should ##### 接口 ```java ackage cn.tedu.search.repository; import cn.tedu.search.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; //Repository是Spring家族对持久层的命名规范 @Repository public interface ItemRepository extends ElasticsearchRepository { //ItemRepository继承了ElasticsearchRepository父接口,这个父接口由SpringData提供 //只要使用springData连接ES,就必须继承ElasticsearchRepository //ElasticsearchRepository的泛型参数,第一个是实体类类型,第二个是主键类型ElasticsearchRepository<实体类,主键> //继承之后,当前接口就可以直接使用父接口中声明的方法,包含了Item实体类基础的增删改查 //SpringData自定义查询 //我们可以编写一个符合SpringData框架要求格式的方法名 //框架会自动根据方法名生成对应的查询语句,并且执行查询,SpringData会根据我们定义方法名称推测出我们的意图 //query:表示当前方法是一个进行查询的方法,类似数据库中的select //Items/item:表示查询的实体,如果可能返回集合应该在类型后加s //By(根据):类似数据库中的where,表示查询条件开始的关键字 //Title:表示查询的字段,只能是对象中存在的字段 //Matches:表示匹配,类似数据库中的like,如果不写会自动添加 Iterable queryItemsByTitleMatches(String title); //多条件自定义查询 //多个条件之间使用And或Or进行分割,表示多个条件间的逻辑关系 //我们要使用Title和Brand字段进行多条件查询 //参数的顺序和方法名中的顺序一致,第一个参数就给第一个条件,第二个就给第二个条件,与参数名称并无关系,即使第一个参数叫brand也是给TitleMatches传值 //参数的数量要匹配,几个条件就要有几个参数 //自定义查询方法时可以使用list,但是直接使用ElasticsearchRepository的方法时,必须使用Iterable,否则会出现异常 List queryItemsByTitleMatchesAndBrandMatches(String title, String brand); ``` ##### 测试类 ```java //多条件自定义查询 @Test void queryTwo(){ itemRepository.queryItemsByTitleMatchesAndBrandMatches("游戏","罗技").forEach(k -> System.out.println(k)); } ``` #### 排序查询 默认查询时,ES会按照相关度进行排序,相关度越高,排名越靠前 如果想改变这个排序就需要在查询方法上添加新的关键字 ##### 接口 ```java package cn.tedu.search.repository; import cn.tedu.search.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; //Repository是Spring家族对持久层的命名规范 @Repository public interface ItemRepository extends ElasticsearchRepository { //ItemRepository继承了ElasticsearchRepository父接口,这个父接口由SpringData提供 //只要使用springData连接ES,就必须继承ElasticsearchRepository //ElasticsearchRepository的泛型参数,第一个是实体类类型,第二个是主键类型ElasticsearchRepository<实体类,主键> //继承之后,当前接口就可以直接使用父接口中声明的方法,包含了Item实体类基础的增删改查 //SpringData自定义查询 //我们可以编写一个符合SpringData框架要求格式的方法名 //框架会自动根据方法名生成对应的查询语句,并且执行查询,SpringData会根据我们定义方法名称推测出我们的意图 //query:表示当前方法是一个进行查询的方法,类似数据库中的select //Items/item:表示查询的实体,如果可能返回集合应该在类型后加s //By(根据):类似数据库中的where,表示查询条件开始的关键字 //Title:表示查询的字段,只能是对象中存在的字段 //Matches:表示匹配,类似数据库中的like,如果不写会自动添加 Iterable queryItemsByTitleMatches(String title); //多条件自定义查询 //多个条件之间使用And或Or进行分割,表示多个条件间的逻辑关系 //我们要使用Title和Brand字段进行多条件查询 //参数的顺序和方法名中的顺序一致,第一个参数就给第一个条件,第二个就给第二个条件,与参数名称并无关系,即使第一个参数叫brand也是给TitleMatches传值 //参数的数量要匹配,几个条件就要有几个参数 //自定义查询方法时可以使用list,但是直接使用ElasticsearchRepository的方法时,必须使用Iterable,否则会出现异常 List queryItemsByTitleMatchesAndBrandMatches(String title, String brand); //排序查询 //在方法最后添加OrderBy关键字,然后指定排序依据的字段,最后设置升序(ASC)降序(Desc) List queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(String title, String brand); ``` ##### 测试类 ```java //自定义排序查询 @Test void queryOrder() { itemRepository.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc("游戏", "罗技").forEach(k -> System.out.println(k)); } ``` #### 分页查询 ##### 接口 ```java package cn.tedu.search.repository; import cn.tedu.search.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; //Repository是Spring家族对持久层的命名规范 @Repository public interface ItemRepository extends ElasticsearchRepository { //ItemRepository继承了ElasticsearchRepository父接口,这个父接口由SpringData提供 //只要使用springData连接ES,就必须继承ElasticsearchRepository //ElasticsearchRepository的泛型参数,第一个是实体类类型,第二个是主键类型ElasticsearchRepository<实体类,主键> //继承之后,当前接口就可以直接使用父接口中声明的方法,包含了Item实体类基础的增删改查 //SpringData自定义查询 //我们可以编写一个符合SpringData框架要求格式的方法名 //框架会自动根据方法名生成对应的查询语句,并且执行查询,SpringData会根据我们定义方法名称推测出我们的意图 //query:表示当前方法是一个进行查询的方法,类似数据库中的select //Items/item:表示查询的实体,如果可能返回集合应该在类型后加s //By(根据):类似数据库中的where,表示查询条件开始的关键字 //Title:表示查询的字段,只能是对象中存在的字段 //Matches:表示匹配,类似数据库中的like,如果不写会自动添加 Iterable queryItemsByTitleMatches(String title); //多条件自定义查询 //多个条件之间使用And或Or进行分割,表示多个条件间的逻辑关系 //我们要使用Title和Brand字段进行多条件查询 //参数的顺序和方法名中的顺序一致,第一个参数就给第一个条件,第二个就给第二个条件,与参数名称并无关系,即使第一个参数叫brand也是给TitleMatches传值 //参数的数量要匹配,几个条件就要有几个参数 //自定义查询方法时可以使用list,但是直接使用ElasticsearchRepository的方法时,必须使用Iterable,否则会出现异常 List queryItemsByTitleMatchesAndBrandMatches(String title, String brand); //排序查询 //在方法最后添加OrderBy关键字,然后指定排序依据的字段,最后设置升序(ASC)降序(Desc) List queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(String title, String brand); //分页查询 //要执行分页查询,首先要将返回值修改为Page类型 //Page类型的功能是技能保存查询出的数据,又能保存分页信息 //分页信息就是本次查询结果中分页相关的数据,包含总页数,当前页码,每页显示的条数,总条数等 //参数方面,在当前所有已经存在的参数最后,再添加一个Pageable类型的参数 //在调用时,为这个pageable参数赋值要查询的页码和每页的条数 Page queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc( String title, String brand, Pageable pageable); } ``` ##### 测试类 ```java package cn.tedu.search; import cn.tedu.search.entity.Item; import cn.tedu.search.repository.ItemRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import java.util.ArrayList; import java.util.List; import java.util.Optional; @SpringBootTest class SearchApplicationTests { @Autowired private ItemRepository itemRepository; @Test //新增es数据 void addOne() { //@Accessors(chain = true) 注解就可以这样直接通过.set来赋值,不用每次都调用对象的set方法 Item item = new Item() .setId(1L) .setTitle("罗技激光无线游戏鼠标") .setCategory("鼠标") .setBrand("罗技") .setPrice(199.0) .setImgPath("/img_1.png"); //利用Spring Data Elasticsearch 框架提供的新增方法,完成新增到Es itemRepository.save(item); System.out.println("新增成功"); } @Test //根据id查询es中数据 void getOne() { //Spring Data 提供了俺id查询es中数据的方法 //findById()方法的返回值是Optional,这个是JDK8的新特性,用来解决空指针异常 //返回值是Optional类型,并且定义了泛型,可以理解为一个只能储存一个元素的list Optional optional = itemRepository.findById(3L); Item item = optional.get(); System.out.println(item); } //批量新增 @Test void addList() { List list = new ArrayList<>(); list.add(new Item(2L, "罗技激光有线办公鼠标", "鼠标", "罗技", 88.0, "/img_2.png")); list.add(new Item(3L, "雷蛇机械无线游戏键盘", "键盘", "雷蛇", 268.0, "/img_3.png")); list.add(new Item(4L, "微软有线静音办公鼠标", "鼠标", "微软", 166.0, "/img_4.png")); list.add(new Item(5L, "罗技机械有线背光键盘", "键盘", "罗技", 208.0, "/img_5.png")); itemRepository.saveAll(list); System.out.println("批量新增成功"); } @Test void getList() { //findAll就是SpringData提供的从指定索引中全查所有数据方法 //Iterable items = itemRepository.findAll(); Iterable items = itemRepository.findAll(); for (Item item : items) { System.out.println(item); } System.out.println("============================================"); items.forEach(k -> System.out.println(k)); } //单条件自定义查询 @Test void queryOne() { //查询Items中,title字段包含"游戏"的数据 itemRepository.queryItemsByTitleMatches("游戏") .forEach(k -> System.out.println(k)); } //多条件自定义查询 @Test void queryTwo() { itemRepository.queryItemsByTitleMatchesAndBrandMatches("游戏" , "罗技") .forEach(k -> System.out.println(k)); } //自定义排序查询 @Test void queryOrder() { itemRepository.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc("游戏" , "罗技") .forEach(k -> System.out.println(k)); } //分页查询 @Test void queryPage() { int page = 2; int pageSize = 2; Page items = itemRepository.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc("游戏", "罗技", PageRequest.of(page - 1, pageSize)); items.forEach(k -> System.out.println(k)); System.out.println("总页数:" + items.getTotalPages()); System.out.println("总条数:" + items.getTotalElements()); System.out.println("当前页:" + (items.getNumber() + 1)); System.out.println("每页条数:" + items.getSize()); System.out.println("是否为首页:" + items.isFirst()); System.out.println("是否为末页:" + items.isLast()); } } ``` ## 知识补充 Iterable是Collection的父类,Collection的子类有List和Set ## 数据库分页PageHelper ### 什么是PageHelper PageHelper只能搭配Mybatis,如果使用其他的数据库连接方法等,需要使用其他分页插件 **原始分页查询sql** ```sql SELECT * FROM order_tbl LIMIT 0,10; ``` 分页逻辑无论什么业务都是类似的,所以有框架帮助我们高效实现分页功能 PageHelper框架可以实现我们提供页码和每页条数,自动实现分页效果,收集分页信息 PageHelper的分页原理就是在程序运行时,在sql语句尾部添加limit关键字,并按照分页信息向limit后追加分页数据 ### PageHelper使用 之前学习seata时已经在依赖中添加过PageHelper的依赖,所以这里不需要再添加依赖了 PageHelper是插件,所以不需要在配置文件中配置,只需要在代码中使用即可 #### pom.xml ```xml com.github.pagehelper pagehelper-spring-boot-starter ``` #### Mapper ```java //分页查询所有订单的方法 //PageHelper就是在持久层没有任何迹象 //使用Pagehelper框架完成分页查询的原理实在sql语句运行时,在sql语句后添加Limit关键字 //所以在持久层编写方法时,没有任何分页查询的特征,也无需关注分页业务(包括在Xml中) @Select("SELECT id,user_id,commodity_code,count,money FROM order_tbl") List findAllOrders(); ``` #### impl ```java public PageInfo getAllOrdersByPage(Integer page, Integer pageSize) { //PageHelper框架实现分页的方法,就是在方法执行查询前,设置分页条件 //使用PageHelper.startPage方法设置本次查询要查询的页码和每页条数 // PageHelper的页码从1开始,也就是page是1,就查询第一页 PageHelper.startPage(page, pageSize); //上面的分页条件设置完毕后,下面进行的查询,就会在sql语句后自动添加limit关键字 List allOrders = orderMapper.findAllOrders(); // 上面查询的list就是要查询的当页数据,但是不包括分页信息 //所以作为分页工具,必须返回包含这个分页信息的对象,也就是生命的返回值PageInfo // 当前方法返回时,直接实例化PageInfo对象,会自动计算分页信息 // 同时传入list作为参数,将list中的数据赋值给PageInfo return new PageInfo<>(allOrders); } ``` #### Controller ```java @GetMapping() @ApiOperation("查询所有订单") @ApiImplicitParams({ @ApiImplicitParam(value = "页码",name="page",example = "1"), @ApiImplicitParam(value = "每页条数",name="pageSize",example = "10") }) public JsonResult> pageOrder(Integer page,Integer pageSize){ PageInfo allOrdersByPage = orderService.getAllOrdersByPage(page, pageSize); return JsonResult.ok("查询完成",allOrdersByPage); } ``` 当前我们分页查询返回的类型是PageInfo 如果用这个类型来做业务逻辑层的返回值,当当前方法作为dubbo生产者对外提供服务时消费者调用该服务需要使用Pagelnfo类型对象来接收, 这样要求消费者也添加PageHepler依赖,这是不合理的 所以我们设计在commons模块中,添加一个专门用于返回分页结果的类JsonPage, 代替Pagelnfo例如之前SpringDataElasticsearch框架也支持分页,返回类型为Page, 它也可以替换为JsonPage这样当前微服务项目中,所有分页或类似的操作, 就都可以使用这个类了 因为需要在commons模块中使用Pagelnfo类型,所以commons模块要添加pageHelper的依赖 ## commons ### 依赖文件 ```xml 4.0.0 cn.tedu csmall 0.0.1-SNAPSHOT cn.tedu csmall-commons 0.0.1-SNAPSHOT csmall-common csmall-common com.github.xiaoymin knife4j-spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-json org.springframework.boot spring-boot-starter-tomcat com.github.pagehelper pagehelper 5.2.0 ``` ### 实体类 在restful文件夹下新建一个JsonPage类,手写转换方法 ```java package cn.tedu.csmall.commons.restful; import com.github.pagehelper.PageInfo; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.List; @Data public class JsonPage implements Serializable { //JsonPage是用于统一代替PageInfo或Page这样分页查询结果类型的 //其中要包含分页信息和查询到的数据两方面 //我们这里只声明少量分页信息即可,实际开发中,有额外需要在添加额外属性 @ApiModelProperty(value = "总页数", name = "totalPages") private Integer totalPages; @ApiModelProperty(value = "总条数", name = "totalCount") private Long totalCount; @ApiModelProperty(value = "当前页码", name = "page") private Integer page; @ApiModelProperty(value = "每页条数", name = "pageSize") private Integer pageSize; //还要声明一个属性,保存查询到的数据 @ApiModelProperty(value = "分页数据", name = "list") private List list; //编写一个方法,将PageInfo类型对象转换为JsonPage类型对象返回 public static JsonPage restPage(PageInfo pageInfo) { //转换代码,要将PageInfo中相同意义的属性赋值到JsonPage中 JsonPage jsonPage = new JsonPage<>(); jsonPage.setTotalCount(pageInfo.getTotal()); jsonPage.setTotalPages(pageInfo.getPages()); jsonPage.setPage(pageInfo.getPageNum()); jsonPage.setPageSize(pageInfo.getPageSize()); //分页数据也要赋值进去 jsonPage.setList(pageInfo.getList()); //返回转换完成的对象 return jsonPage; } } ``` ## Order模块 先修改service层的代码,然后Impl层会报错 ### srvice ```java package cn.tedu.csmall.order.service; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.restful.JsonPage; public interface IOrderService { void OrderAdd(OrderAddDTO orderAddDTO); //声明返回JsonPage类型的分页查询订单的方法 JsonPage getAllOrdersByPage(Integer page, Integer pageSize); } ``` ### impl ```java package cn.tedu.csmall.order.webapi.impl; import cn.tedu.csmall.cart.service.ICartService; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.JsonPage; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.order.service.IOrderService; import cn.tedu.csmall.order.webapi.mapper.OrderMapper; import cn.tedu.csmall.stock.service.IStockService; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * @author 刘浩男 */ @DubboService @Service @Slf4j public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; //添加@DubboReference注解,表示当前业务逻辑层需要消费其他模块 //直接下面生命的业务逻辑接口,必须在Nacos中有对应的注册实现类 //调用时,Dubbo可以自动获取Nacos中注册的业务逻辑层实现类对象 @DubboReference private IStockService stockService; @DubboReference private ICartService cartService; @Override public void OrderAdd(OrderAddDTO orderAddDTO) { //1.这里要先进行减少数据库中库存的操作(stock模块) StockReduceCountDTO stockReduceCountDTO = new StockReduceCountDTO(); stockReduceCountDTO.setReduceCount(orderAddDTO.getCount()); stockReduceCountDTO.setCommodityCode(orderAddDTO.getCommodityCode()); stockService.reduceStock(stockReduceCountDTO); //2.还要从购物车中删除用户选中的商品(cart模块) cartService.deleteUserCart(orderAddDTO.getUserId(), orderAddDTO.getCommodityCode()); if (Math.random()>0.5){ throw new CoolSharkServiceException( ResponseCode.INTERNAL_SERVER_ERROR, "故意抛出的异常" ); } //3.上面操作完成后在进行订单的新增 Order order = new Order(); BeanUtils.copyProperties(orderAddDTO, order); //执行新增 int i = orderMapper.insertOrder(order); log.info("新增订单成功,插入了:{}条", i); } public JsonPage getAllOrdersByPage(Integer page, Integer pageSize) { //PageHelper框架实现分页的方法,就是在方法执行查询前,设置分页条件 //使用PageHelper.startPage方法设置本次查询要查询的页码和每页条数 // PageHelper的页码从1开始,也就是page是1,就查询第一页 PageHelper.startPage(page,pageSize); //上面的分页条件设置完毕后,下面进行的查询,就会在sql语句后自动添加limit关键字 List allOrders = orderMapper.findAllOrders(); // 上面查询的list就是要查询的当页数据,但是不包括分页信息 //所以作为分页工具,必须返回包含这个分页信息的对象,也就是生命的返回值PageInfo // 当前方法返回时,直接实例化PageInfo对象,会自动计算分页信息 // 同时传入list作为参数,将list中的数据赋值给PageInfo return new JsonPage<>(); } } ``` ### controller 把控制器的返回值类型改为JsonResult> **注意!!!!!** @Autowired private IOrderService orderService; 别忘记改回来 ```java package cn.tedu.csmall.order.webapi.controller; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.commons.pojo.order.entity.Order; import cn.tedu.csmall.commons.restful.JsonPage; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.order.service.IOrderService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.github.pagehelper.PageInfo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") @Api(tags = "订单管理模块") public class OrderController { @Autowired private IOrderService orderService; @PostMapping("/add") @ApiOperation("新增订单") @SentinelResource(value = "新增订单方法",blockHandler = "OrderAddHandler",fallback = "OrderAddFallbackHandler") public JsonResult OrderAdd(OrderAddDTO orderAddDTO){ orderService.OrderAdd(orderAddDTO); // try { // Thread.sleep(5000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } return JsonResult.ok("新增订单成功!"); } public JsonResult OrderAddHandler(OrderAddDTO orderAddDTO, BlockException e){ return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"服务器忙,请稍后再试!"); } public JsonResult OrderAddFallbackHandler(OrderAddDTO orderAddDTO,Throwable e){ e.printStackTrace(); return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,"运行发生异常,执行降级方法:"+e.getMessage()); } @GetMapping() @ApiOperation("查询所有订单") @ApiImplicitParams({ @ApiImplicitParam(value = "页码",name="page",example = "1"), @ApiImplicitParam(value = "每页条数",name="pageSize",example = "10") }) public JsonResult> pageOrder(Integer page, Integer pageSize){ JsonPage allOrdersByPage = orderService.getAllOrdersByPage(page, pageSize); return JsonResult.ok("查询完成",allOrdersByPage); } } ``` ## 解耦 为什么不直接使用Impl层,而是通过调用service层来实现业务逻辑,目的是为了解耦 耦合性高/低,直接使用Impl为耦合性高就是指模块之间的依赖性高,耦合性低就是指模块之间的依赖性低,所以才需要建立接口 ## Quartz 在stock模块中测试 ### 依赖 ```xml org.springframework.boot spring-boot-starter-quartz ``` ### config ```java // 我们要将job和Trigger绑定在一起,明确触发关系 // 绑定成功后,会有Scheduler来调度,这个对象在程序运行时,会被SpringBoot创建 // 所以在配置中,我们只需要将JobDetail和Trigger对象保存到Spring容器即可 @Configuration public class QuartzConfig { // @Bean注解标记的方法返回值会在SpringBoot项目启动时保存到Spring容器 @Bean public JobDetail showTime() { // JobDetail类使专用于封装Job接口实现类的类型 // JobBuilder.newJob就是管理Job接口实现类的方法 return JobBuilder.newJob(QuartzJob.class) // 给当前任务起名,不能和其他任务重名 .withIdentity("shouTime") // 在默认情况下,JobDetail实例化后,没有Trigger绑定,可能会被GC回收 // 设置storeDurably()方法之后,即使JobDetail对象没有被绑定,也不会被回收 .storeDurably() .build(); } @Bean public Trigger showTimeTrigger() { CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule("* * 14 23 2 ?"); return TriggerBuilder.newTrigger() .forJob(showTime()) .withSchedule(cron) .withIdentity("showTimeTrigger") .build(); } @Bean public JobDetail reduceStock() { return JobBuilder .newJob(StockJob.class) .withIdentity("reduceStock") .storeDurably() .build(); } @Bean public Trigger reduceStockTrigger(){ CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule("0/20 * * * * ?"); return TriggerBuilder.newTrigger() .forJob(reduceStock()) .withIdentity("reduceStockTrigger") .withSchedule(cron) .build(); } } ``` ### QuartzJob ```java @Slf4j public class QuartzJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { // 当前job实现类只是为了测试Quartz的效果,对业务复杂度没有要求,所以操作一些简单的即可 log.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~{}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", LocalDateTime.now()); } } ``` ### StockJob 模拟一下操作数据库,让她每隔20秒减少一次库存 ```java @Slf4j public class StockJob implements Job { @Autowired private StockMapper stockMapper; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { stockMapper.stockUpdate("PU201",2); log.info("库存减少完成"); } } ``` 到此本项目的基础学习就结束了接下来可以进行实战项目的开发了 https://gitee.com/liuhaonan71t/csmall-repo.git