# 微服务
**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
# **_微服务商城项目_**
## **父项目**
每次新建一个子项目都要进行父子相认

修改pom文件
注意parent标签中 version版本要更改(idea2023并没有java8的选项 要更改服务器为阿里云的)

### 子项目
# Nacos
我使用的时1.4.2的版本,启动时进入到nacos的bin目录
```shell
startup.cmd -m -standalone
```
# stock模块
创建子项目模块,在模块的pom中修改parent标签为父项目的这一部分
```xml
cn.tedu
csmall
0.0.1-SNAPSHOT
```

到这里父子相认就完成了
## 2、父项目添加依赖
### 又称锁版本
定义依赖版本的变量,父pom中指定版本,子pom中就不需要指定版本了
除非有额外的版本需要,才在子项目中添加版本信息
```xml
1.8
2.2.2
```
添加完毕之后开始添加依赖,在version标签中添加之前定义的变量${*}的格式来使用变量
```xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybats.version}
```
子项目中不需要添加版本信息,直接添加所需要的依赖就可以自动匹配父项目的版本

添加依赖之后可能会出现找不到依赖包的报错,这种时候可以尝试使用下面的方法
就是把报错的dependency标签赋值到dependencyManagement外面的dependencies(就是跟我的lombok同级,并不是在原来的依赖管理中)
然后刷新maven就可以下载了

# 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测试(正确返回如下)

现在要开始将项目注册到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
,依然是导入两个配置

需要注意一点 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

默认使用Dubbo协议,支持很多序列化协议,认默使用Hessian2,默认情况下支持的协议有如下特征:
* 采用NIO单一长连接
* 优秀的并发性能,但是处理大型文件的能力差
Dubbo方便支持高并发和高性能

consumer服务的消费者,指的是服务的调用者(使用者),也是需要注册到注册中心,provider启动之后把服务都给到注册中心,
consumer启动之后能够看到注册中心所有的服务,发现之后就可以根据RPC远程过程调用的方式调用服务,Dubbo中,远程调用
依据是服务的提供者在Nacos中的注册服务名称,只需服务名称,不需要ip地址和端口号等信息

* 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在实现远端调用的过程中,是通过负载均衡算法,能够尽可能让请求在相对空闲的服务器上运行
在不同的项目中可能选用不同的负载均衡策略,以达到最好的效果

**Loadbalance**:
就是负载均衡的意思
**平滑权重运行**:
在一段时间内,让权重逐渐增加,直到最大值,然后再逐渐减少,直到最小值,然后再逐渐增加,直到最大值,如此循环

**活跃度自动感知**:
记录每个服务器处理一次请求的时间 ,按照时间比例来分配任务数,运行一次需要时间多的分配的请求数较少
**一致性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默认的事务模式,也是最常用的事务模式

这是一个比较典型的远端调用结构
Seata构成部分包含
* TC:事务协调器,负责协调全局事务,并驱动全局事务提交或回滚
* TM:事务管理器,用于开启一个全局事务,并最终发起全局提交或回滚
* RM:资源管理器,用于控制分支事务,并驱动分支事务的提交或回滚
具体的流程大概是TM发起全局事务,TC接收到全局事务后,向所有的RM发起分支事务,
然后RM执行本地事务,并将本地事务的执行结果返回给TC,TC收集到所有的RM的执行结果后,
决定是提交还是回滚,如果有一个RM失败,会把信息返回给TC,TC再通知其他RM进行回滚

## 启动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 事务回滚

## 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页面

**默认账号密码都是sentinel**

## 配置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进行限流


这时候发送请求,连续点击会出现

情况,代表限流成功
### 自定限流提示
可以看到我之前的虽然限流成功但是返回的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,"服务器忙!请稍后再试");
}
}
```
再次测试成功出现报错信息

### 并发线程数
再测试一下并发线程数的使用方法,限流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,"服务器忙!请稍后再试");
}
}
```
测试结果

成功限流,Knief4j使用的时候要注意一下,他没法同时发请求,可以开两个网页同时发
### 扩展
Sentinel限流算法
QPS使用的是**令牌桶**算法
并发线程数是**线程池**算法
## 熔断

### 慢调用比例
最大RT就是指如果这个请求超过多少毫秒,就代表他是慢调用比例;
比例阈值是指比如有五个请求, 其中有一个请求超过了最大RT,那么这个比例就是1/5,也就是20%,就填0.2;
熔断时长就是指停止时长,比如填写5秒,就代表,当有5个请求请求时,有2个超过30毫秒,就停止5秒;

### 异常比例
异常比例就是指,如果有五个请求,其中有一个请求发生了异常,那么这个比例就是1/5,也就是20%,就填0.2;

### 异常数
异常数就是指,如果有五个请求,其中有一个请求发生了异常,那么这个数就是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 对应的组件或服务

# 网关Gateway
Gateway是Spring提供的,不是阿里写的
网关:就是值网络中的关口\关卡,网关就是当前微服务项目的统一入口.
这里是一个demo,主要是为了讲解网关,因为原项目不好配合gateway讲解
后面会有csmall-gateway项目
## 网关的原理
方便对所有的接口进行统一管理

## 网关的作用
* 将所有请求统一经过网关
* 网关可以对这些请求进行检查
* 网关方便记录所有请求的日志
* 网关可以统一将所有请求路由到正确的模块\服务上
"路由"的近义词就是"分配"
图中显示的,只有外部的请求才走网关,内部之间的请求不需要走网关(例如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中其他模块的名称来进行路由

启动之后通过网关的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

15:03 访问正常显示

```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**
数据库进行模糊查询的时候效率严重低下,所有关系型数据库都有这个缺点

数据库索引分为两大类
* 聚集索引
聚集索引就是数据库保存数据的物理顺序依据,默认是主键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版本,并且汉化了,

打开文件开始测试,测试之前要先启动ES,然后在测试文件中输入
```http request
### 三个# 表示多个请求之间的分割,也可以用作注释,否则会报错
GET http://localhost:9200
```
注意 **###** 是必须有的,表示分割请求也是注释,
**如果没有的话会报错**

再来测试一下分词功能
```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使用

把这些文件复制到plugins/ik文件夹中,没有ik就自己新建一个文件夹

复制完后重启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