主要技术点
1.项目是基于RESTful风格来进行设计的,可以时代码更美观和有层次。提供一组设计原则和约束条件
2.由于是微服务架构,选型是选择的nacos作为注册中心来存储和获取服务信息
3.全局的异常处理是使用SpringBOOt提供的异常处理类来进行统一处理的,所有的异常都是在controller层来统一抓取处理
4.基础的增删改查是使用的JPA来操作的 可以简化dao层的基础单表代码的编写,但是对于一些复杂逻辑的查询业务还是要自己写
5.使用FastFDS搭建集群,支持对用户上传的文件进行上传、下载等操作
6.用网关来分配请求和做逻辑判断 如 是否有权限访问该资源
7.对商品的SKU、SPU、Spec等商品表的字段分析和设计
8.保证服务器的高可用,进行了缓存预热和热点数据的二级缓存
9.海量数据存储使用的是ElasticSearch,并且使用canal来进行监听来同步ES数据库的数据
10.Thymeleaf来生成静态化页面
项目第一天
主要电商模式
项目二主要是以B2C模式
B2B 商家对商家 阿里巴巴(企业级)、慧聪网
# 指进行电子商务交易的供需双方都是商家(或企业、公 司),她(他)们使用了互联网的技术或各种商务网络平台,完成商务交易的过程。
C2C 客户对客户 咸鱼、瓜子二手车
# 意思就是消费者个人间的电子 商务行为。比如一个消费者有一台电脑,通过网络进行交易,把它出售给另外一个消费者,此 种交易类型就称为C2C电子商务。
B2C 商家对客户 唯品会
# 企业通过互联网为消费 者提供一个新型的购物环境——网上商店,消费者通过网络在网上购物、网上支付等消费行为。
C2B 消费者对企业
# 先有消费者需求产生而后有企业生产,即先有消费者提出需求,后有生产企业按需求组织生产。互联网经济时代新的商业模式
O2O (Online To Offline) 线上线下 美团
# 将线下的商务机会与互联网结 合,让互联网成为线下交易的平台,这个概念最早来源于美国
F2C 工厂到客户
# 即从厂商到消费者的电子商务模式。
B2B2C 天猫 京东
# 电子商务类型的网络购物商业模式,B是BUSINESS的简称,C是CUSTOMER的简 称,第一个B指的是商品或服务的供应商,第二个B指的是从事电子商务的企业,C则是表示消费者。
项目构建
1.结构说明
# 父级依赖下的二级模块
1)changgou_gateway 网关服务 # 作为独立服务存在,同一处理用户请求,分配各个请求的目的地。
2)changgou_service # 微服务架构存放处,继成所有微服务模块
3)changgou_service_api # service服务的整体访问支持,主要用来提供对外依赖
4)changgou_transaction_fescar # 分布式事务模块,将分布式事务抽取到该工程中,任何工程使用分布式事务,只需依赖该工程即可
5) changgou_web # web服务工程,对应功能模块若要调用多个微服务,可以将他们写入到该模块中,例如网站 后台、网站前台等
2.父级依赖
作用: 统一定义springBoot、springCloud、编码
<!--boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<properties>
<!--跳过测试-->
<skipTests>true</skipTests>
<java.version>1.8</java.version>
<!-- 设置编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 设置jre版本为 1.8 -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- cloud -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 编译 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<!-- 设置jre版本为 1.8 -->
<source>1.8</source>
<target>1.8</target>
<!-- 设置编码为 UTF-8 -->
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
技术点
1.JPA
javax.persistence 是一套接口 与通用类mapper一同使用 在pojo上定义数据库的字段名来自动映射, 与通用类联合使用
mapper作用: 提供众多可以直接操作数据库的API 对于单表查询不需要再写sql语句
1 在启动类上方要扫对应的mapper包 @MapperScan(basePackages = {"包全路径名"})
1.
//由通用类提供,来自于Feign
//定义 dao层 继承Mapper<实体类>
public interface BrandMapper extends Mapper<Brand> {
}
2.
//实体类使用JPA来定义
@Table(name="tb_brand") //由persistence依赖提供 指定数据库的表
public class Brand implements Serializable {
@Id//指定主键id ,由persistence依赖提供
private Integer id;//品牌id
private String name;//品牌名称
private String image;//品牌图片地址
private String letter;//品牌的首字母
private Integer seq;//排序
<!--含有mapper功能-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<scope>provided</scope>
</dependency>
<!--JPA依赖-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
1.常用API
API名称 | 作用 |
---|---|
selectAll | 查询所有 |
selectByPrimaryKey | 按照id查询 |
insert / insertSelective | 插入一条数据(不含null / 含null) |
updateByPrimaryKey | 按id更新 |
deleteByPrimaryKey | 按id删除 |
selectByExample | 使用example对象按指定的条件查询 |
2.示例1基础使用
//模糊查询+分页查询实示例
public PageResult<Brand> findPage(Map<String, Object> searchMap, int page, int size) {
//也可以传入一个map 自动按照map的字段进行对应处理 如查询 按照map的value值来查询
Example example = new Example(Brand.class); //传入对象来找到对应的表和封装的类
Example.Criteria criteria = example.createCriteria(); //创建criteria来封装条件
//判断条件 如果 搜索结果为空 则直接返回
//封装查询条件
if(searchMap.size()!=0){
//品牌名称(模糊) like %
if (searchMap.get("name")!=null && !"".equals(searchMap.get("name"))){
//封装模糊查询条件
criteria.andLike("name","%"+searchMap.get("name")+"%");
}
//按照品牌首字母进行查询(精确)
if (searchMap.get("letter")!=null && !"".equals(searchMap.get("letter"))){
//封装精确查询条件
criteria.andEqualTo("letter",searchMap.get("letter")); //定义字段名和指定查询条件
}
}else {
return null;
}
//准备查询页数条件
PageHelper.startPage(page,size);
//正式查询
Page<Brand> p = (Page<Brand>) brandMapper.selectByExample(example);
//返回
return new PageResult(p.getTotal(),p.getResult());
}
3.示例2多表查询使用
//直接在dao的接口中定义多表查询的纯注解形式即可
@Select("SELECT b.name,b.image,b.letter,b.seq FROM tb_brand b,tb_category_brand cb ,tb_category c WHERE \n" +
"b.id=cb.brand_id AND cb.category_id=c.id AND c.name=#{categoryName}")
List<Map<String,Brand>> findBrandByCategoryName(@Param("categoryName") String categoryName);
//按照分类名查询规格
@Select("SELECT s.name,s.options FROM tb_category c , tb_spec s WHERE c.template_id=s.template_id AND c.name=#{categoryName}")
List<Map> findSpecByCategoryName(@Param("categoryName") String categoryName);
2.SpringBoot异常类处理❤️
只对controller来进行拦截,service层的异常往上抛即可
前提: 在不使用feign的前提下使用统一异常处理,如果是使用feign作为远程调用则使用降级处理
可以拦截多个异常,如果有比Exception小的异常类,优先使用小的异常捕获方法
//定义异常类
@ControllerAdvice
public class Exception4Same {
//定义全局异常捕获方法
@ExceptionHandler(value = Exception.class) //定义拦截异常类型
@ResponseBody
public Result catchException(Exception e){
e.printStackTrace();
return new Result(false,StatusCode.ERROR,"忙",null);
}
//如果捕获到IO异常,则优先选择下面的处理逻辑进行返回!
@ExceptionHandler(value = IOException.class) //定义拦截IO的异常
@ResponseBody
public Result catchException(Exception e){
e.printStackTrace();
return new Result(false,StatusCode.ERROR,"稍后重试!",null);
}
}
3.RESTful风格设计
增删改查 Post、 Delete 、Update、 GetMapping
如果是post请求 则还是使用@RequestBody 来接受
get请求 使用@PathVariable来接受参数 ,且只接受mapping上定义的参数
不使用@PathVariable来接受参数,则接受?name=“xx” 的键值对
@RestController
@RequestMapping("/brand") //定义同意访问
public class BrandController {
//自动注入
@Autowired
private BrandService brandService;
//添加品牌
@PostMapping()
public Result addBrand(@RequestBody Brand brand){ //是 post请求则使用@RequestBody定义 获取json
}
//删除品牌
@DeleteMapping("/{id}")
public Result deleteBrand(@PathVariable Integer id){ //是get请求 则使用@PathVariable 映射地址栏的值
}
//修改品牌
//此处未指定id,默认在json中提供
@PutMapping()
public Result updateBrand(@RequestBody Brand brand){
}
//查询所有品牌
@GetMapping()
public Result<List<Brand>> findAllBrand(){
}
//按id查询品牌
@GetMapping("/{id}")
public Result findBrandById(@PathVariable Integer id){
}
//模糊查询
@GetMapping("/search")
public Result findBrandByCondition(@RequestBody Map map){
}
//分页查询
@GetMapping("/search/{p}/{size}")
public PageResult findPage(@PathVariable Integer p, @PathVariable Integer size){
}
//分页+模糊
@GetMapping("/searchPage/{p}/{size}")
public PageResult findPage(@RequestBody Map map ,@PathVariable Integer p, @PathVariable Integer size){
}
}
4.nacos作为注册中心❤️
使用步骤:
1.启动服务器,解压压缩包,进入bin文件 开启 startup.sh/cmd即可
2.启动类上添加注解@EnableDiscoveryClient
3.导入依赖
4.配置注册中心的基本信息
1) 配置自己这个工程在注册中心中的工程名
2) 定义数据库信息
3) 定义nacos的服务器ip、端口
<!--注意:如果出现错误,多数情况是依赖出现问题,建议重新下载-->
<!--nacos依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.1.0</version>
</dependency>
# 配置文件
spring:
application:
name: nacos-provider-goods # 定义名字
# main:
# allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 定义jdbc
# 定义数据库信息
url: jdbc:mysql://192.168.93.133:3307/shopping?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 定义nacos的服务器
项目第二天
跨域
跨域是出于浏览器的同源策略限制。同源会阻止一个域与另外一个域的js脚本进行交互。同源指的是两个页面具有相同的协议、端口、ip
出现跨域访问时所报异常:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:xxx' is therefore not allowed access. The response had HTTP status code 400.
Cros
全称是"跨域资源共享"。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。它允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
首先会通过第一次的跨域请求,双方都支持该跨域操作后才会真正的进行通信。
跨域请求解决方案: @CrossOrigin
在需要开启跨域访问支持的controller上方,添加该注解
# 如果使用网关来统一修改跨域问题 在yml文件中 层级关系: spring - cloud - gateway - globalcors
gateway:
#配置跨域访问
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
#批准四种操作的跨域请求
- GET
- POST
- PUT
- DELETE
浏览器与服务器的交互过程:
技术点
1.FastFDS❤️
定义:开源的 分布式文件存储系统
功能:文件的存储、同步、访问(上传、下载)
作用:存储大量文件 、冗余备份(使用多台电脑对用户的文件进行多重备份)、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件 上传、下载等服务。
分类:
1. Tracker 接收用户请求、给出用户响应,收集Storage信息并且根据信息来决定用户请求存储到那一台机器(管理storage)
2. Storage 支持线性扩容,导致理论上内存是无上限的,内存不够就加入一个新的卷(机器集群 )
3. 客户端 发送请求给tracker
1.工作原理
storage会定期的传送![在这里插入图片描述](https://img-blog.csdnimg.cn/20201013090850537.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JsYW5rbHI=,size_16,color_FFFFFF,t_70#pic_center)
和ip对storage发起请求进行存储操作。
# tracker 在整个过程中 仅担任调度和负载均衡的职责 !!!!!!记得看 作用!
# storage 所有的文件最终都是存储在storage上
2.上传流程
接上述。 当客户端接收到正确响应后,会将用户的请求以流的方式对storage进行传输,storage接收后则进行存储,并且会生成一个索引信息,以后需要通过此信息来找到对应的文件。
文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。(一般会称为两部分 组名、文件存储路径)
问:为什么要返回给客户端信息 让其发送给storage 而不是直接在tracker上转发
此设定主要是考虑到网络请求和资源占用问题。
1.资源占用:如果所有的请求都携带要保存的文件一同交给tracker,会大量占用tracker的系统资源,当到达临界值是会出现服务器崩溃或不能准确获取到storage发送的心跳信息,导致错误判断逻辑。
2.如果所有的请求都要经过tracker来进行转发,会造成大量请求堆积和带宽不足的现象。
3.搭建与使用
此博客具体介绍了如何进行安装和使用
https://blog.csdn.net/canyun9798/article/details/83795789
1.导包
<!--fastdfs客户端-->
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
2.配置文件1
yml文件,统一配置
spring:
application:
name: nacos-provider-file #定义工程名
servlet: #此处的配置是针对SpringMVC的 接收只接收指定大小的文件
multipart:
max-file-size: 10MB # 指定最大文件
max-request-size: 10MB #指定最大请求大小
# 此处未显示注册中心信息
3.配置文件2
根目录下的fdfs_client.conf文件: 配置fastdfs的内部配置信息
connect_timeout = 60
network_timeout = 60
charset = UTF-8
http.tracker_http_port = 8410 对外访问的storage的traker的port
tracker_server = 192.168.93.133:8300 对外访问的storage的traker服务器地址和端口
4.编码
//传入的参数名必须叫file,否则不予识别
public Result uploadFile(MultipartFile file){
try{
//判断file是否为空
if (file == null){
throw new RuntimeException("文件不存在");
}
//判断文件名是否为空
if(file.getName().equals("")){
throw new RuntimeException("文件名不可空");
}
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)){
throw new RuntimeException("文件不存在");
}
//获取该文件的后缀
String original = file.getOriginalFilename();
String ext = original.substring(original.lastIndexOf(".")+1);
//创建FileDFS对象
FastDFSFile fastDFSFile = new FastDFSFile(file.getName(), file.getBytes(), ext);
//调用工具类上传方法
String[] upload = FastDFSClient.upload(fastDFSFile);
//使用返回的上传结果拼接最终请求结果
String totalURL = FastDFSClient.getTrackerUrl()+upload[0]+"/"+upload[1];
//返回
return new Result(true,StatusCode.OK,"文件上传成功",totalURL);
}catch (Exception e){
e.printStackTrace();
return new Result(false, StatusCode.ERROR,"文件上传失败");
}
工具类
package com.changgou.file.util;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FastDFSClient {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class);
/***
* 初始化加载FastDFS的TrackerServer配置
*/
static {
try {
String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
ClientGlobal.init(filePath);
} catch (Exception e) {
logger.error("FastDFS Client Init Fail!",e);
}
}
/***
* 文件上传
* @param file
* @return 1.文件的组名 2.文件的路径信息
*/
public static String[] upload(FastDFSFile file) {
//获取文件的作者
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] = new NameValuePair("author", file.getAuthor());
//接收返回数据
String[] uploadResults = null;
StorageClient storageClient=null;
try {
//创建StorageClient客户端对象
storageClient = getTrackerClient();
/***
* 文件上传
* 1)文件字节数组
* 2)文件扩展名
* 3)文件作者
*/
uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
} catch (Exception e) {
logger.error("Exception when uploadind the file:" + file.getName(), e);
}
if (uploadResults == null && storageClient!=null) {
logger.error("upload file fail, error code:" + storageClient.getErrorCode());
}
//获取组名
String groupName = uploadResults[0];
//获取文件存储路径
String remoteFileName = uploadResults[1];
return uploadResults;
}
/***
* 获取文件信息
* @param groupName:组名
* @param remoteFileName:文件存储完整名
* @return
*/
public static FileInfo getFile(String groupName, String remoteFileName) {
try {
StorageClient storageClient = getTrackerClient();
return storageClient.get_file_info(groupName, remoteFileName);
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件下载
* @param groupName
* @param remoteFileName
* @return
*/
public static InputStream downFile(String groupName, String remoteFileName) {
try {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
InputStream ins = new ByteArrayInputStream(fileByte);
return ins;
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件删除
* @param groupName
* @param remoteFileName
* @throws Exception
*/
public static void deleteFile(String groupName, String remoteFileName)
throws Exception {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//删除文件
int i = storageClient.delete_file(groupName, remoteFileName);
}
/***
* 获取Storage组
* @param groupName
* @return
* @throws IOException
*/
public static StorageServer[] getStoreStorages(String groupName)
throws IOException {
//创建TrackerClient
TrackerClient trackerClient = new TrackerClient();
//获取TrackerServer
TrackerServer trackerServer = trackerClient.getConnection();
//获取Storage组
return trackerClient.getStoreStorages(trackerServer, groupName);
}
/***
* 获取Storage信息,IP和端口
* @param groupName
* @param remoteFileName
* @return
* @throws IOException
*/
public static ServerInfo[] getFetchStorages(String groupName,
String remoteFileName) throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
}
/***
* 获取Tracker服务地址
* @return
* @throws IOException
*/
public static String getTrackerUrl() throws IOException {
return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port()+"/";
}
/***
* 获取Storage客户端
* @return
* @throws IOException
*/
private static StorageClient getTrackerClient() throws IOException {
TrackerServer trackerServer = getTrackerServer();
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient;
}
/***
* 获取Tracker
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerServer;
}
}
package com.changgou.file.util;
//定义文件对象
public class FastDFSFile {
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;
public FastDFSFile(String name, byte[] content, String ext, String height,
String width, String author) {
super();
this.name = name;
this.content = content;
this.ext = ext;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
super();
this.name = name;
this.content = content;
this.ext = ext;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
5.postman测试
如果要上传文件
改为对应RESTful请求方式 --> body --> form-data --> text切换为file类型 --> 指定文件 key必须与后端接收名相同 --> 发送
项目第三天
1.网关
导包: 网关是自主的一套体系结构,所以不能与web结构重合使用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<!--有web包会导致依赖冲突-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置
此处的配置文件中包含
1.令牌桶
2.跨域访问
server:
port: 8100 #定义端口
spring:
application:
name: nacos-provider-gateway # 定义名字
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 定义nacos的服务器
# 网关配置
gateway:
routes:
- id: gateway-provider1
# 动态路由
uri: lb://nacos-provider-goods
predicates:
- Path=/goods/**
#定义过滤器
filters:
- StripPrefix= 1 # 在当前访问路径中 减去/后面的第一级访问
- name: RequestRateLimiter
args:
key-resolver: "#{@currentLimit}" # 引用该bean类
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒往桶中存放的令牌数
redis-rate-limiter.burstCapacity: 1 # 令牌桶的总容量
#配置跨域访问
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
#批准四种操作的跨域请求
- GET
- POST
- PUT
- DELETE
# redis 配置 令牌桶基于redis
redis:
port: 6379
host: "192.168.93.133"
2.网关限流❤️
在网关中对请求访问的拦截。
令牌桶算法
令牌桶原理
1.按照设置的速率定期向同种存放令牌,达到桶上限后就不再接受令牌
2.用户访问时会从桶中获取令牌,再进行后续的逻辑访问
3.如果用户未获取到令牌则会直接返回给用户并显示429错误
注意:该算法有很多实现方式,Guava(读音: 瓜哇)是其中之一,redis客户端也有其实现。 此处是使用redis
实现限流与算法拦截
1.导包
<!--redis的令牌桶算法-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
2.在配置文件中配置redis的服务器和桶的令牌上线和接收速率
#在网关的某个id下定义过滤器
filters:
- StripPrefix= 1 #不配置此项 则会拿着整体路径去访问后台,则报404
- name: RequestRateLimiter #此处的名字不可变动
args:
key-resolver: "#{@currentLimit}" # 解析该bean的方法名 用SpEL表达式#{@beanName}从容器中获取Bean对象
# 以下配置 表示一秒一个请求
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒往桶中存放的令牌数
redis-rate-limiter.burstCapacity: 1 # 令牌桶的总容量
# redis 配置
redis:
port: 6379
host: "192.168.93.133"
3.编码。定义在IOC中定义bean 可以在启动类中定义@BEAN 也可以在核心配置类中定义
@Configuration
public class GApplication {
//定义限流,获取令牌
@Bean
public KeyResolver currentLimit(){
return new KeyResolver(){
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
//获取访问的ip,对其进行限流。从桶中拿一个令牌走。选定策略,每个IP只能拿走一个!
Mono<String> just = Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
return just;
}
};
}
//lambda方式
@Bean
public KeyResolver currentLimit(){
return exchange -> {
//获取访问的ip,对其进行限流。从桶中拿一个令牌走
Mono<String> just = Mono.just(exchange.getRequest().getRemoteAddress().getHostName());//获取
return just;
};
}
}
}
3.BCrypt
BCrypt官网 http://www.mindrot.org/projects/jBCrypt/
- 作用:用于加密生成内容
- 特点:
- 生成的加密内容不可逆
- 常用方法:
- genslat:生成盐
- hashpw:生成密码
- checkpw:校验密码
MD5和BCrypt比较流行。相对来说,BCrypt比MD5更安全。
特性
1.内部引入加盐机制(随机生成一个29个字符长度的字符串)
2.不可逆
3.同一个数据。每次编译后的密文均不同
org.springframework.security.crypto.bcrypt.BCrypt; //导入此包
BCrypt.gensalt(); //获得盐
BCrypt.hashpw("123", gensalt对象); //加密
BCrypt.checkpw("123",hashpw对象); //解密
4.加密算法工具类!❤️
可逆
加密后的密文可以通过算法来逆向还原来真实值
AES DES HS256 需要找到工具类并测试调用
对称性加密 //在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要 解密方事先知道加密密钥。
RSA、SHA-256 需要找到工具类并测试调用
非对称性加密 // 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.
不可逆
一旦加密就不能反向解密得到密码原文. (常见:MD5、BCrypt )
Base64编码
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。Base64编码可用于在 HTTP环境下传递较长的标识信息。采用Base64编码解码具有不可读性,即所编码的数据 不会被人用肉眼所直接看到。注意:Base64只是一种编码方式,不算加密方法。
5.JWT鉴权❤️
JWT定义: 一个用于客户端与服务器中间传递信息的协议规范
微服务鉴权定义: 在网关中直接对这个请求判定是否已经具有权限对要访问的地址进行访问
是JSON Web Token的简称
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
1.头部:描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以 被表示成一个JSON对象
2.载荷:载荷就是存放有效信息的地方。 由 有效信息+base64组成 (主要需要设置的内容)
3.签名:签证由header加密后 、payload加密后 、secret 组成一个字符串,防止内容被篡改
JJWT
官方文档 https://github.com/jwtk/jjwt
使用格式
//创建一个JJWT
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID 解码后就是这个内容
.setSubject(subject) // 主题 可以是JSON数据 定义用途
.setIssuer("admin") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为密钥
.setExpiration(expDate);// 设置过期时间
//解析
JwtBuilder parseBuilder = Jwts.parser()
.setSigningKey(secretKey) //创建时指定的密钥
.parseClaimsJws(jwt) //令牌
.getBody(); //生成
依赖包
<!--鉴权-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
登录校验
1.过滤器只要访问此地址就要放行
2.对用户输入的账户密码进行判定
3.如果正确则存入一个JJWT 返回,下次用该令牌的key和value来通过过滤器拦截
//登录校验
@RequestMapping("/login")
public Result adminLogin(@RequestBody Admin admin){
//查询数据库 获取该账户密码
Boolean status = adminService.findByAdmin(admin);
//判断返回值是否有查询到
if(!status){
//没有查询到,返回错误结果集
return new Result(false,StatusCode.ERROR,"账户密码不正确",null);
}
//创建容器
HashMap<String, Object> map = new HashMap<>();
//生成令牌
String jwt = JwtUtil.createJWT("wz", "授权", null); //指定编码内容、指定本次jwt主题、指定存活时间
//存入令牌
map.put("AUTHORIZE_TOKEN",jwt);
//携带容器返回 key value值
return new Result(true,StatusCode.OK,"登录成功",map);
}
网关过滤器
@Component
//定义过滤器
public class GatewayFilter2 implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求和响应
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//判断请求的请求头是否为登录页面的请求,如果是直接放行
//表示该用户是第一次登录
if(request.getURI().getPath().equals("/admin/login")){
return chain.filter(exchange);
}
//获取请求头
HttpHeaders headers = request.getHeaders();
//获取令牌
String token = headers.getFirst("AUTHORIZE_TOKEN"); //此权限存在于请求头中,在使用postman时要手动添加该键值对
//判断是否存在此令牌
if(token==null){
//不存在则设置拒绝状态并返回
response.setStatusCode(HttpStatus.UNAUTHORIZED); //设置状态信息 未授权
return response.setComplete(); //发送请求
}
//解析令牌
try {
JwtUtil.parseJWT(token); //使用工具类进行解析
} catch (Exception e) {
e.printStackTrace();
//出错则返回
//不存在则设置拒绝状态并返回
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//放行
return chain.filter(exchange);
}
//定义这是所有网关过滤器中的第一个过滤器
@Override
public int getOrder() {
return 1;
}
}
6.常见的HTTP状态码
- 401:没有授权
- 403:没有权限
- 429:访问太频繁
项目第四天
1.分布式ID主键生成
作用 :保证分表后的主键唯一、存储在缓存数据库中时更好标识
1.为提高性能,在分库分表的时候 由于同一张表要分为几张,此时就不能出现同一ID的情况。
2.在查询出的数据存入缓存数据库时,便于key值的存储。
数据库不是同步数据么 只有一台机器在进行增删改
分布式数据库都主键自增会引起相同主键id 为了避免这种情况就要使用分布式ID来避免这种情况
分布式ID解决方案
1.UUID 不使用原因:字符串类型 排序不便
优势
1.简单、方便
2.生成的id性能好
3.全球唯一id
劣势
1.不可排序、不可读
2.字符串存储 如果海量数据则要考虑存储空间问题且查询效率低
3.传输的字符串传 导致网络传输效率低
2.REDIS/ZK
这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现
1.不使用原因:经过网络请求
3.雪花算法
雪花算法
本地随机生成的一组唯一数字主键的算法
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想 是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器 ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最 后还有一个符号位,永远是0
IdWorker idWorker=new IdWorker(0,0); //参数只能在0-31之间,底层会用这个数字来分解为两个5位数加入返回值中
薄雾算法
2.SPU与SKU❤️
电商的两大理论 : SPU和SKU 、电商模式
SPU 是一组商品集合
SKU 是具体细分的每一种商品的类型。是物理上不可分割的最小存货单元。
此项目中的brand则是每一种商品
SPU添加时的需求
1 设置分布式唯一主键
2 合法性校验
- name不能为空,且长度32汉字
- image不能为空
- 关联的数据ID对应的数据应该校验存在性
3后台默认初始化字段值
- 销量默认为0
- 评论数默认为0
- 是否上架为0,未上架
- 是否启用规格为1,启用
- 是否删除为0,未删除
- 是否审核为0,为审核
SKU添加的需求
1 设置分布式唯一主键
2 合法性校验
- 库存数量大于0
- 商品图片不能为空
3 后台默认初始化字段值
- SKU的名称=SPU名称+空格+规格
- 创建时间,当前时间
- 更新时间,当前时间
- 冗余存储类目名称、品牌名称
- 销量默认为0
- 评论数默认为0
- 商品状态默认为1 ,1-正常,2-下架,3-删除
Dto接收数据
前端与后端的报文 需要使用Dto来接收或者响应数据 一般在controller层使用 本质是一个POJO 由于没有合适的POJO所以孕育而生
命名规范是以Dto结尾
什么是报文: 前端传输的数据量跨距多表的封装 来传送给后端
//实际上就是一个pojo 只是没有对应的字段 使用不同的实体类封装对应数据
public class Goods implements Serializable {
private Spu spu;
private List<Sku> sku;
}
Spu特点
对某些字段进行冗余存在 作用:避免多次查询
不要冗余存在的字段: 经常改动、长度过长的字段
3.商品上下架
注意: transaction注解的使用中 不可以有try…catch的时候 否则不会被事务识别
重要概念: 逻辑删除 (定义某一字段,对该字段进行判断) 彻底删除(再次判断字段、还要删除则彻底删除)
### 商品的修改
- SPU的信息修改,可直接修改表信息即可
- SKU的信息修改,则先删除后再进行新增
### 商品的审核
> 商品审核之后,商品自动上架
- 条件
- 未被逻辑删除 is_delete=0
- 未审核
- 动作
- status 修改为已审核1
- is_marketable 修改已上架状态1
### 商品下架
- 条件
- 没有被逻辑删除0
- 是已上架状态1
- 动作
- is_marketable 修改为下架状态0
### 商品上架
- 条件
- 审核通过1
- 没有被逻辑删除0
- 是下架状态0
- 动作
- is_marketable 修改为上架状态1
### 商品逻辑删除
- 条件
- 是下架状态0
- 是未删除状态0
- 动作
- is_delete修改为已删除1
- status修改了为未审核状态0[可选]
### 商品还原
- 条件
- 是已删除状态1
- 动作
- is_delete修改为未删除0
- status修改了为未审核状态0[可选]
### 商品物理删除
- 条件
- 是已删除状态1
- 动作
- 从数据库中直接删除
项目第五天
网站高可用 Nginx 、Lua 搭配使用是一种方案
1.lua
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目 的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
功能不仅仅是在nginx 还可以使用到redis中的事务管理 因为redis是不支持事务的,需要使用lua脚本来进行事务的回滚
基本语法
使用ngx.say()来排错
导入cjson依赖 可以转换为json来进行挑错 如果乱码
单行注释
--
多行注释
--[[
--]]
变量
全局变量
a=1
局部变量
local a =1
保留关键字
流程控制
‐‐[ 0 为 true ]
if(0) then
print("0 为 true")
else
print("0 不为true")
end
函数定义和调用
‐‐[[ 函数返回两个值的最大值 ‐‐]]
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end
require函数
导入其它脚本文件,功能类似java的import
2.nginx
是作为一台服务器 理论承载量为50000 tomcat为500
可以与网关同时使用 但是一般企业不可承受 太消耗性能!
支持的功能 负载均衡、反向代理、静态资源(动静分离)
3.openRestry
openRestry里面封装了nginx,并且集成了LUA脚本,开发人员只需要简单用其提供的模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写 lua的脚本,再进行调用了。
tomcat 500 nginx 10000 openrestry 50000 自己写的程序很大,但是受tomcat
安装
# 对应的文件夹下执行此命令,下载该版本
wget -c https://openresty.org/download/openresty-1.15.8.1rc2.tar.gz
# 具体操作流程看此网页
https://blog.csdn.net/guo_qiangqiang/article/details/90175403
上述操作完成后就可以进行配置
# 根据笔记文档继续执行即可
lua脚本定义
此处提供lua文件 其余按照文档操作。 lua中的…表示+ 字符串拼接
read.lua 获取缓存
--设置响应头类型
ngx.header.content_type="application/json;charset=utf8"
-- 获取请求、获取请求中的positi广告位参数
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
local cache_ngx = ngx.shared.dis_cache;
--获取redis中的key 查看可以获取到
local adCache = cache_ngx:get('ad_cache_'..position);
--空值判断
if adCache == "" or adCache == nil then
--引入redis库
local redis = require("resty.redis");
--创建redis对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--连接
local ok, err = red:connect("192.168.93.133", 6379)
--获取key的值
local rescontent=red:get("ad_"..position)
-- 向控制台输出获取到的内容
ngx.say(rescontent)
--关闭连接
red:close()
-- 存入redis中 设置过期时间
cache_ngx:set('ac_cache_'..position,rescontent,10*60)
else
--输出到返回响应中
ngx.say(rescontent)
end
update.lua 存储缓存数据
ngx.header.content_type="application/json;charset=utf8"
--导包
local cjson = require("cjson")
local mysql = require("resty.mysql")
--获取请求及参数
local uri_args = ngx.req.get_uri_args()
local position = uri_args["position"]
-- 新建mysql
local db = mysql:new()
-- 设置超市
db:set_timeout(1000)
--设置参数
local props = {
host = "192.168.93.133",
port = 3307,
database = "shopping",
user = "root",
password = "root"
}
-- 连接
local res = db:connect(props)
--定义查询语句
local select_sql = "select url,image from tb_ad where status ='1' and position='"..position.."' and start_time<= NOW() AND end_time>= NOW()"
--开始查询,获得结果
res = db:query(select_sql)
--关闭
db:close()
--以下是redis信息
--导包
local redis = require("resty.redis")
--新建和设置超时
local red = redis:new()
red:set_timeout(2000)
--设置参数
local ip ="192.168.93.133"
local port = 6379
--连接
red:connect(ip,port)
-- 存入redis中 指定key、val
red:set("ad_"..position,cjson.encode(res))
red:close()
-- 向控制台发送语句
ngx.say("{\"flag\":true,\"position\":\""..position.."\"}")
设置脚本访问路径
一般该文件存储在
/usr/local/openresty/nginx/conf/nginx.conf
脚本文件放置到/root目录下,因为在conf文件的第一行要定义
user root root
在Nignx.conf文件中配置访问lua脚本的请求URI
#/ad_update后 注意有一个空格!
# /root/lua/ad_update.lua; 是绝对路径
location /ad_update {
content_by_lua_file /root/lua/ad_update.lua;
}
重启NGINX
./nginx -s reload
-s stop
4.缓存预热
概念:在网页的首页等高访问量的网站可以考虑使用缓存预热,将sql的数据优先读取存入redis缓存
缓存预热是一种解决方案,最终是通过上述的update脚本经过java调用来执行
步骤
项目启动。nginx就会读取lua脚本来查询mysql的对应数据信息,读到以后直接存放在redis中。
这样就可以在访问的时候直接从redis中拿去数据
5.二级缓存解决方案
概念: 在redis的缓存机制上再次加入本地缓存,用户在访问的时候是直接去本地缓存中读取,如果没有才会到redis中去获取
在大数据领域还存在三级四级缓存、但是存在的问题是如何保证缓存的一致性
好处:
1,减小数据库的访问压力
2.降低网络请求次数
注意:在用户访问前端页面的时候 要在对应的页面(如:首页)添加方法来读取Nginx的缓存信息
# conf文件中定义此方法来加载首页
location / {
root /home/html/shopping; # 此处是存放的位置
index index.html index.htm; # 默认页面
}
6.本地缓存
在conf文件末尾处添加即可
#包含redis初始化模块
lua_shared_dict dis_cache 5m; #共享内存开启
7.限流与突发处理
一般情况下,首页的并发量是比较大的,即使有了多级缓存,如果有大量恶意的请求, 也会对系统造成影响。而限流就是保护措施之一。
限制的方式:控制访问速率、控制并发的连接数量两种
控制速率的方式之一就是采用漏桶算法。
漏桶是控制请求出的速度、令牌桶是控制进的速度
漏桶、令牌桶里面存放的都是令牌
步骤: 生成令牌 发放令牌 限流令牌 一个进一个出
# conf文件中定义此方法来加载首页
#漏桶算法的使用 需要重新在conf文件中定义一个server对象 如下:
server{
listen 8081;
server_name localhost;
charset utf-8;
location / {
limit_req zone=myRateLimit burst=5 nodelay # 启动漏铜算法 设置突发请求的最大允许数量并设置为直接访问不排队获取漏桶令牌
root html;
index index.html index.htm;
}
}
#漏桶算法解析
binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流, binary_ 的目的是压缩内存占用量。 zone:定义共享内存区来存储访问信息, myRateLimit:10m 表示一个大小为10M,名字为myRateLimit的内存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访 问信息。 rate 用于设置最大访问速率,rate=10r/s 表示每秒最多处理10个请求。
Nginx 实际上以 毫秒为粒度来跟踪请求信息,因此 10r/s 实际上是限制:每100毫秒处理一个请求。这意味 着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将拒绝处理该请求.我们这里 设置成2 方便测试
#突发流量解析
burst 译为突发、爆发,表示在超过设定的处理速率后能额外处理的请求数,当 rate=2r/s 时,将1s拆成2份,即每500ms可处理1个请求。
此处,burst=5,若同时有6个请求到达,Nginx 会处理第一个请求,剩余5个请求将放 入队列,然后每隔500ms从队列中获取一个请求进行处理。若请求数大于6,将拒绝处理 多余的请求,直接返回503. 不过,单独使用 burst 参数并不实用。假设 burst=50 ,rate为10r/s,排队中的50个请 求虽然每100ms会处理一个,但第50个请求却需要等待 50 * 100ms即 5s,这么长的处 理时间自然难以接受。
因此,burst 往往结合 nodelay 一起使用
注意: 一切conf配置完成后都要对openrestry进行重启
项目第六天
实现功能
优势:canal + rabbitmq 可以很好的解耦和提高扩展性
广告缓存更新
数据库同步索引库
1.canal
定义: 监听mysql数据库变化的软件
原理: 通过修改binlog的启动,使canal可以把自己伪装为mysql的从节点,在主节点上实时监听 (一定要答出binlog模式)
优势:程序解耦
依赖包在中央仓库没有需要安装
1.找到资源,下载,并解压
spring-boot-starter-canal-master
2.解压后,starter-canal文件中存在pom.xml文件
3.在地址栏输入cmd,打开黑窗口
4.mvn install即可 等待下载安装
主要注解2个 、 主要参数 2个
1.开启docker容器mysql的监控支持
1.开启mysql容器
docker run -it -p 3307:3306 --name=c_mysql -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root mysql:5.7
2.进入容器,进入mysql
docker exec -it c_mysql /bin/bash
进入sql
mysql -uroot -proot
3.查看日志
# 如果log_bin的值为OFF是未开启,为ON是已开启。
SHOW VARIABLES LIKE '%log_bin%';
4.退出mysql
到mysql容器中 进入mysqld.cnf文件
# 如果没有vi命令 则安装
apt-get update
apt-get -y install vim
#安装完成后输入此指令,对该文件进行修改
vi /etc/mysql/mysql.conf.d/mysqld.cnf
#如果安装不上、可以选择在window上 新建 mysqld.cnf文件,写入如下配置信息
#再拖入linux中 使用移动指令: 从主机拷贝到容器 docker cp ./文件名 容器名:/对应要移动的容器中文件的位置/
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
symbolic-links=0
# 开启log_bin
log-bin=mysql-bin
binlog-format=ROW
server_id=1
max_allowed_packet=20M
#如果可以安装上
# 在[mysqld]下面添加
# [mysqld]
log-bin=mysql-bin
binlog-format=ROW
server_id=1
5.再次进入mysql
mysql -h localhost -u root -p
6.使用root账号创建用户并授予权限
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
7.重启容器
docker restart mysql
2.cannal安装
1.拉取镜像
docker pull canal/canal-server:latest
2.可以先不做,如果有问题再做。
vi /etc/sysctl.conf
net.ipv4.ip_forward = 1
systemctl restart network
systemctl restart docker
3.启动容器,进入
# 指定canal的访问地址、暴露端口和sql的暴露端口、账户、密码、
docker run --name canal -e canal.instance.master.address=192.168.93.139:3307 -e canal.instance.dbUsername=root -e canal.instance.dbPassword=root -p 11111:11111 -d canal/canal-server
docker exec -it canal /bin/bash
4.进入该配置文件
vi /home/admin/canal-server/conf/canal.properties
5.添加
canal.id=2 (此处的id不能与mysql的id相同)
6.重启
docker restart canal
# 可以设置开机启动 适用于所有容器
docker update --restart=always canal
2.RabbitMq
优势:服务之间解耦、在队列里提高扩展性
配置信息
三个功能: 上下架商品的监控、广告位置更新监控
使用的依赖以及配置文件
canal配置
server:
port: 8200
canal:
client:
instances:
example:
host: 192.168.93.139
port: 11111
batchSize: 1000
spring:
application:
name: nacos-provider-canal
rabbitmq:
host: 192.168.93.139
port: 5672
# 开启消息手动签收模式
listener:
simple:
acknowledge-mode: manual #手动签收
prefetch: 1 # 一次拉去一条 如果发生错误,就一直处理 直到解决
# 开启return机制
publisher-returns: true
#开启confirm机制
publisher-confirm-type: correlated
search监听模块配置
# ES的版本 7.4
#Springboot 版本 2.3.3
#springCloud 版本 Hoxton.SR8
# 技术支持 RabbitMQ Canal ElasticSearch
spring:
application:
name: nacos-provider-search # 定义名字
#定义mq
rabbitmq:
host: 192.168.93.139 #其余属性采用默认
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 定义jdbc
# 定义数据库信息
url: jdbc:mysql://192.168.93.139:3307/shopping?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
#定义es
elasticsearch:
# 新版实现方式
rest:
uris: "http://192.168.93.139:9200"
connection-timeout: 2000000s #消费者 连接服务提供者的连接超时时间
read-timeout: 200000s #调用服务提供者的服务的超时时间
cluster‐name: elasticsearch # 老版本实现方式
cluster‐nodes: 192.168.200.128:9300 # 老版本实现方式
#与spring同级
feign:
hystrix:
enabled: true #开启降级
client:
config:
default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
connectTimeout: 1000 #60000 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
readTimeout: 1000 #20000 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
thread:
# 熔断器超时时间,默认:1000/毫秒
timeoutInMilliseconds: 20000
广告监听
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGoAuUqU-1602551268077)(D:\Java总结文档\图解\项目二\第六天\广告监控流程图.png)]
使用okhttp监控协议❤️
rabbitTemplate 封装了三种请求协议,默认使用http协议
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.0</version>
</dependency>
@CanalEventListener 使用该注解定义监听类
//监听mysql的变化
//使用普通模式直接传输到队列中,此处使用工作模式直接发送至队列
//监听到消息后的调用函数
@CanalEventListener
public class AdListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@ListenPoint(schema = "shopping" ,table = { "tb_ad" }) public void adUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData ){
Map<String, String> beforeMap = CanalUtil.convertToMap( rowData.getBeforeColumnsList() ); //修改前数据
System.out.println( "修改前数据"+JSON.toJSONString(beforeMap));
Map<String, String> afterMap = CanalUtil.convertToMap( rowData.getAfterColumnsList()); //修改后数据
System.out.println( "修改后数据"+JSON.toJSONString(afterMap));
//CanalUtil内容
( //创建map集合,并指定map集合的大小
Map<String, Object> map = new HashMap<>(16);
//遍历list集合,将其中的数据存入map集合中
list.forEach(column -> {
map.put(column.getName(), column.getValue());
});
return map;)
String beforePosition = beforeMap.get( "position" );
if(beforePosition!=null){
System.out.println("beforePosition发送消息到mq"); rabbitTemplate.convertAndSend( "","ad_update_queue" ,beforePosition);
}
String afterPosition = afterMap.get( "position" ); if(afterPosition!=null && !afterPosition.equals( beforePosition) ){
System.out.println("afterPosition发送消息到mq"); rabbitTemplate.convertAndSend( "","ad_update_queue" ,afterPosition);
}
}
}
//监听队列获取消息后,发起远程调用传输给nginx
//发起远程调用,请求远程的广告缓存预加载接口
package com.wz.business.Listener;
import okhttp3.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class AdQueueListener {
//获取队列中的消息,定义监听器
@RabbitListener(queues = "add_queue")
public void getAdQueueInfo(String message){
// int a = 1/0;
//定义Nginx的访问地址
//访问该网址就会执行对应的sql语句去查询对应的字段同步更新到redis中
String url = "http://192.168.93.139/ad_update?position="+message;
//创建ok传输协议对象
OkHttpClient okHttpClient = new OkHttpClient();
//装载请求信息
Request request = new Request.Builder().url(url).build();
//传输信息给nginx
Call call = okHttpClient.newCall(request);
//定义失败/成功的回调函数
call.enqueue(new Callback() {
@Override
//失败的回调
public void onFailure(Call call, IOException e) {
System.out.println("恭喜,打出GG!");
}
@Override
//成功回调
public void onResponse(Call call, Response response) throws IOException {
System.out.println("成功回传");
}
});
}
}
ES
分片 主分片 (3、5)、(5、5)个【不同的版本不同】 副分片就有多少个
上下架商品
1.canal监控
此处以下架删除商品信息为例
可靠性保证
rabbitTemplate的底层是只会执行一次 否则会对消息进行错误处理 所以要定义在生命周期中
//监听已经上架的商品,如果修改其中的任何信息则进行监听,并转发消息给搜索服务器
@CanalEventListener
public class SpuListener implements InitializingBean ,DisposableBean {
@Override
//由于是set方式 所以只能定义一次 使用初始化方法来定义
public void afterPropertiesSet() throws Exception {
//定义confirm稳定性保证。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
}
}
});
//定义return稳定性保证
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("return 执行了....出错回退了");
}
});
}
//这是销毁方法
@Override
public void destroy() throws Exception {
}
// @PostConstruct //这个注解是定义初始化方法
@Autowired
private RabbitTemplate rabbitTemplate;
@ListenPoint(schema = "shopping" ,table = "tb_spu")
public void getSpuListener(CanalEntry.EventType eventType, CanalEntry.RowData rowData){
//只要执行此方法就要获取该改变对象的id
//获取修改前和修改后的
List<CanalEntry.Column> aList = rowData.getAfterColumnsList();
List<CanalEntry.Column> bList = rowData.getBeforeColumnsList();
//获取到之前和之后的数据变化
HashMap<String, String> bMap = new HashMap<>();
bList.forEach(column -> { bMap.put(column.getName(),column.getValue());});
HashMap<String, String> aMap = new HashMap<>();
aList.forEach(column -> { aMap.put(column.getName(),column.getValue());});
//获取最新上架的商品 由0变1就发送消息
if ("0".equals(bMap.get("is_marketable")) && "1".equals(aMap.get("is_marketable"))){
//将商品的spuid发送到mq
//传输id给广播交换机
String id = bMap.get("id");
rabbitTemplate.convertAndSend(MQconfig.GOODS_UP_SpuCHANGE,"",bMap.get("id"));
}
//获取最新下架的商品 1->0
if ("1".equals(bMap.get("is_marketable")) && "0".equals(aMap.get("is_marketable"))){
//将商品的spuid发送到mq
rabbitTemplate.convertAndSend(MQconfig.GOODS_DOWN_SpuCHANGE,"",aMap.get("id"));
}
}
}
面试必答的注解标识你用过
- @CanalEventListener 编辑当前类是一个Cannl的监听器
- @ListenPoint 配置程序所监控的数据表信息
- CanalEntry.RowData 是封装了改动前后的数据信息
- CanalEntry.EventType 是当前数据变化的原因类型
//在配置类中已经定义好交换机、队列、绑定关系。执行发送消息语句时则自动创建
//定义监听类,监听sql变化
@CanalEventListener
public class AdListener {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @param eventType 当前操作数据库的类型
* @param rowData 当前操作数据库的数据
*/
//提供监听方法,监听广告位置变化
@ListenPoint(schema = "shopping",table = "tb_ad")
public void adListen(CanalEntry.EventType eventType,CanalEntry.RowData rowData){//此处两类是核心类
//监控position字段是否发生变化
//如果发生变化则向MQ中传递消息
List<CanalEntry.Column> aList = rowData.getAfterColumnsList();
//循环判定每一个字段是否
aList.forEach(column -> {
if("position".equals(column.getName())){
System.out.println("修改后的position消息是:"+column.getValue());
//使用普通模式直接传输到队列中
rabbitTemplate.convertAndSend("", MQconfig.ADD_QUEUE,column.getValue()); //发送消息给队列
System.out.println("已经完成消息传送");
}
});
}
}
2.消息监听
//定义下架的消息监听
@Component
public class SpuDelListener {
@Autowired
private ElasticService elasticService;
@RabbitListener(queues = MQconfig.GOODS_DEL_SPUQUEUE)
public void getSpuListener(Message pid){
//传递pid给elasticSearch的方法
try {
//获得监听的字节数组
byte[] body = pid.getBody();
//转换为id
String s = new String(body);
//传输,删除
elasticService.delSkusBySpu(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.业务层远程调用/插入es数据
@Service
public class ElasticServiceImpl implements ElasticService {
//注入远程调用feign
@Autowired
private SkuFeign skuFeign;
//注入dao
@Autowired
private ESManagerMapper esMapper;
//删除对应spu的所有sku数据
@Override
public void delSkusBySpu(String pid) {
//查询所有
List<Sku> skuList = skuFeign.findSkuListBySpuId(pid); //使用feign进行远程调用,定义接口,此处不展示接口
//判断空值
if (skuList == null || skuList.size()<=0){
throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
}
//循环删除
for (Sku sku : skuList) {
esMapper.deleteById(Long.parseLong(sku.getId()));
}
}
// ==============保留添加代码===========
// ==============保留添加代码===========
// ==============保留添加代码===========
//添加、修改对应spu的所有sku数据
@Override
public void addSkusBySpu(String pid) throws Exception {
//远程调用查询所有sku
List<Sku> skus = skuFeign.findSkuListBySpuId(pid);
//判断空值
if(skus==null){
throw new Exception("未查询到sku");
}
//skulist转换为json
String jsonSkuList = JSON.toJSONString(skus);
//将json转换为skuinfo
List<SkuInfo> skuInfoList = JSON.parseArray(jsonSkuList, SkuInfo.class);
//遍历循环每一个对象
for (SkuInfo skuInfo : skuInfoList) {
//将规格信息转换为map,因为规格处是使用的一个json字符串格式的存储形式
Map specMap = JSON.parseObject(skuInfo.getSpec(), Map.class);
//将转换好的spec字段
skuInfo.setSpecMap(specMap);
}
//往数据库添加
esMapper.saveAll(skuInfoList);
}
}
4.ES实体类封装
实体类的创建
- @Document指定数据的索引
- @Id 是映射内部_id
- @Field 是指定Field的具体类型或参数
//定义ES数据库类
//使用该注解提前定义索引库和类型
@Document(indexName = "skuinfo", type = "docs")
public class SkuInfo implements Serializable {
//商品id,同时也是商品编号
@Id
//使用该注解定义每个字段的类型
@Field(index = true, store = true, type = FieldType.Keyword)
private Long id;
//SKU名称
@Field(index = true, store = true, type = FieldType.Text, analyzer = "ik_smart")
private String name;
//商品价格,单位为:元
@Field(index = true, store = true, type = FieldType.Double)
private Long price;
//库存数量
@Field(index = true, store = true, type = FieldType.Integer)
private Integer num;
//商品图片
@Field(index = false, store = true, type = FieldType.Text)
private String image;
//商品状态,1-正常,2-下架,3-删除
@Field(index = true, store = true, type = FieldType.Keyword)
private String status;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
//是否默认
@Field(index = true, store = true, type = FieldType.Keyword)
private String isDefault;
//SPUID
@Field(index = true, store = true, type = FieldType.Long)
private Long spuId;
//类目ID
@Field(index = true, store = true, type = FieldType.Long)
private Long categoryId;
//类目名称
@Field(index = true, store = true, type = FieldType.Keyword)
private String categoryName;
//品牌名称
@Field(index = true, store = true, type = FieldType.Keyword)
private String brandName;
//规格
private String spec;
//规格参数
private Map<String, Object> specMap;
//get/set/contruct 省略
}
6.ES封装dao
是一套完整的API封装,支持按照规范自定义方法,快速生成sql语句
//定义es的增删改查封装
public interface ESManagerMapper extends ElasticsearchRepository<SkuInfo,Long> {
}
7.启动类的扫包
@EnableDiscoveryClient // 激活DiscoveryClient 定义为nacos,使用feign降级功能也需要配置
@SpringBootApplication
@EnableFeignClients(basePackages = "com.wz.search.feign") //定义为feign客户端
//使用feign的降级策略,必须要扫包。否则404。扫描含有feign接口的包后,404!
//原因:EnableFeignClients注解只扫描feign的注解,并不会把Spring的注解扫描进ioc
@ComponentScan("com.wz.search")
public class SearchApplication {
public static void main(String[] args) {
SpringApplication.run(SearchApplication.class,args);
}
}
使用RabbitMQ+Cannl解耦程序的方案
- 通过Cannl可以友好的解耦程序
- 通过RabbitMQ的广播模式可以更好的为程序提供高扩展性
拓展
URL组成部分
总共九部分