- 第1章 框架搭建
- 第2章 分布式文件存储
- 第3章 微服务网关限流&鉴权
- 第4章 商品管理
- 第5章 网站首页高可用 nginx+lua
第1章 框架搭建
元动力二奢交易平台:一线互联网大厂的一个二手奢侈品交易平台项目
总共16章 :每章都是相对独立的,难度由简单到复杂。
前置技术:springboot springcloud(alibaba) es rabbitmq 支付 短信
学习目标
目标1:了解电商的技术特点和主要电商模式
目标2:理解元动力二奢平台的需求与系统设计
目标3:能够完成元动力二奢平台工程框架的搭建
目标4:能够完成商品微服务品牌增删改查功能
1. 走进电商-面试了解
1.1 电商行业分析
我国电商发展很快
**萌芽初生阶段**(1991-1999 年)。EDI 电子商务是电商行业的最初雏形。1997 年,我国最早的两家电子商务公司:中国商品交易中心和中国化工网分别上线,B2B 业务开始逐步得到发展。1999年,国内第一家 C2C 电商 8848 正式成立。同年8 月,易趣网成立。11 月,当当网成立。1999 年9 月,招商银行率先在国内全面启动“一网通”网上银行服务,成为国内首先实现全国联通“网上银行”的商业银行。这一时期的中国电商行业仍处于萌芽引入阶段,真正的互联网应用市场还没有形成雏形,电子商务环境远未成熟,网络零售也才刚刚起步。
**初期竞争阶段**(2000-2009年)。2000 年国际范围内的互联网泡沫破灭导致了我国电子商务也受到巨大的影响,8848 逐步没落,易趣被 eBay 收购,一大批新兴的电商网站关闭。但是机遇与挑战并存,2003 年,中国电商发展史上最为重要的两家企业淘宝网、京东相继成立。易趣与淘宝之争是电商发展史上第一次激烈的同业竞争,淘宝网以免费策略与行业老大易趣开展竞争,在 3 年的时间里,淘宝在这次竞争中大获全胜。在这一阶段中,C2C 也逐步成为当时我国网络购物市场(包括 B2C 和 C2C)的主流商业模式。2008 年我国成为全球网民最多的国家,电子商务交易额突破 3 万亿元,2009 年网购人数突破 1 亿。
**高速成长阶段**(2010-2014 年)。2010 年,淘宝迎来第二个双十一购物节,GMV 规模从 2009 年的 0.5 亿元猛增至 9.36 亿元,双十一当天共有 2100 万用户参与了这次历史级别的购物节,全天诞生了 181 家百万级店铺。 2011-2014 年电商交易额继续高速增长,其中网络购物市场依然火爆,占社会商品零售总额的比例大幅度提高,双十一已经成为行业独有的网络购物节。在这一时期,苏宁易购、京东、国美等电商巨头发起了最为激烈的电商价格战,行业在竞争中高速成长。截止 2014年年底,中国电商行业已经全面超越欧盟、日本等经济体,部分领域甚至超越了美国。
**稳定发展阶段**(2015-2017 年)。以腾讯和京东的战略合作达成成为标志,2015 年起,资本推动下的激烈竞争正式结束,多领域的电商平台纷纷走向合作。2015 年出现的从美团大众点评、携程去哪儿、滴滴快的、58 与赶集等生活服务电商均实现了合并。2015-2017 年网购人数、电商交易总额、网络零售交易额与 2010-2014 年相比规模明显增大,但增长速度放缓。电商市场由蓝海逐渐变为红海,发展进入相对稳定的阶段。
**新的变化阶段**(2018-2020 年)。这一阶段之前,淘系与腾讯系京东已经基本坐稳了行业的前两位座次,随着行业发展逐步成熟,两家巨头的规模逐渐扩大,整体增长速度慢慢放缓。进入 2018 年,拼多多的迅速崛起打破了这一平衡,2018 年,成立仅 2 年的拼多多的用户数突破 3 亿,GMV 突破 4700 亿。2019 年,拼多多推出“百亿补贴”这一在电商发展历史上具有重要意义的营销活动,也拉开了又一轮电商行业的激烈竞争。也是在 2018-2020 年,短视频领域迎来爆发,两大巨头纷纷入局电商领域,成为行业在几年内少有的新进大型玩家。电商行业在经历短暂几年的平稳后,持续迎来新的变化。
我们盯住一个垂直行业做
**二手奢侈品行业成新蓝海**
《中国二手奢侈品市场发展研究报告2020》(以下简称报告)显示,二奢的消费主力军**已逐渐从高收入、高阶层群体向普通大众靠拢,呈现年轻化趋势**。2019年,**76%的二奢**被36岁以下的人群买走。
实体店生意如火如荼,二手奢侈品线上赛道也很火热,刺激了资本的入局。
●去年5月12日,二手奢侈品流通服务商胖虎科技完成1.75亿元B轮融资。据悉,胖虎成立于2015年,是一家闲置奢侈品线上平台。2019年胖虎营收约6亿元,GMV(网站成交金额)超过10亿元。截至2020年4月,胖虎APP用户数超过120万。
●另一家二手奢侈品平台妃鱼也在去年2月完成了千万美元的A轮融资。记者了解到,相较于胖虎,妃鱼更专注于二手奢侈品直播电商。早在2018年,妃鱼就开始在淘宝直播卖二手奢侈品。2019年妃鱼整体销售额近十亿元。在60名自营主播中,月销百万的高达50%以上。
●红布林也是二手奢侈品赛道中的一个重量级选手。2019年8月,红布林已完成两千万美元B+轮融资。据悉,红布林采取C2B2C模式,从卖家手中回收,经过鉴定和重新定价之后,再进行售卖。2019年,Plum红布林平台交易量实现了数十倍的增长,月GMV(网站成交金额)数千万。
●除了垂直二手奢侈品电商外,越来越多的时尚或电商平台也加入了这一赛道。例如,时尚电商平台寺库同时售卖全新和二手奢侈品,以分级标注区分两者;奢侈品服务平台包大师由奢侈品养护切入,后延展至二手奢侈品和新品交易;京东2019年7月开设了售价奢侈品服务体验中心“京东奢护”,店内设有二手奢侈品售卖区。
1.2 电商系统技术特点
技术新:单体架构-》微服务 dubbo mycat
技术范围广: java go python 阿里招聘物理学博士 阿里云,改进光纤,水冷、油冷。
分布式:分开部署 user 北京机房
user 上海机房
高并发: 秒杀 玛莎拉蒂 500w->500块 100w请求—》order
集群: 一件事儿->按照逻辑分成多个微服务 下单—-减库存99(10份) 支付 物流服务
负载均衡: 10wqps—— 5w-北京 5w-上海 F5 nginx
高可用: 北京机房 上海机房 深圳机房
海量数据:1亿 10亿 100亿的订单 TB——>PB——->EB
业务复杂:大数据分析 打标签 :手机淘宝 长腿小姐姐 ———》 手机淘宝 小米粥 卫生纸
系统安全:支付宝 改钱:1进去了 2进淘宝了
1.3 主要电商模式
B2B
B2B ( Business to Business)是指进行电子商务交易的供需双方都是商家(或企业、公司),她(他)们使用了互联网的技术或各种商务网络平台,完成商务交易的过程。电子商务是现代 B2B marketing的一种具体主要的表现形式。
案例:阿里巴巴(1688)
C2C
C2C即 Customer to Customer,意思就是消费者个人间的电子商务行为。比如一个消费者有一台电脑,通过网络进行交易,把它出售给另外一个消费者,此种交易类型就称为C2C电子商务。
案例:咸鱼、转转、瓜子二手车
B2C
B2C是Business-to-Customer的缩写,而其中文简称为“商对客”。“商对客”是电子商务的一种模式,也就是通常说的直接面向消费者销售产品和服务商业零售模式。这种形式的电子商务一般以网络零售业为主,主要借助于互联网开展在线销售活动。B2C即企业通过互联网为消费者提供一个新型的购物环境——网上商店,消费者通过网络在网上购物、网上支付等消费行为。
案例:唯品会、乐蜂网
C2B
C2B(Consumer to Business,即消费者到企业),是互联网经济时代新的商业模式。这一模式改变了原有生产者(企业和机构)和消费者的关系,是一种消费者贡献价值(Create Value), 企业和机构消费价值(Consume Value)。
C2B模式和我们熟知的供需模式(DSM, Demand SupplyModel)恰恰相反,真正的C2B 应该先有消费者需求产生而后有企业生产,即先有消费者提出需求,后有生产企业按需求组织生产。通常情况为消费者根据自身需求定制产品和价格,或主动参与产品设计、生产和定价,产品、价格等彰显消费者的个性化需求,生产企业进行定制化生产。
案例:海尔商城
O2O
O2O即Online To Offline(在线离线/线上到线下),是指将线下的商务机会与互联网结合,让互联网成为线下交易的平台,这个概念最早来源于美国。O2O的概念非常广泛,既可涉及到线上,又可涉及到线下,可以通称为O2O。主流商业管理课程均对O2O这种新型的商业模式有所介绍及关注。
案例:美团、饿了吗
F2C
F2C指的是Factory to customer,即从厂商到消费者的电子商务模式。
案例:代工厂
B2B2C
B2B2C(Business to Business to Consumer)是一种电子商务类型的网络购物商业模式,B是BUSINESS的简称,C是CUSTOMER的简称,第一个B指的是商品或服务的供应商,第二个B指的是从事电子商务的企业,C则是表示消费者。
案例:京东商城、天猫商城
2. 元动力二奢-需求分析与系统设计
2.1 需求分析
网站前台:静态原型全流程演示,打开资料\静态原型\前台
,首页index.html
里面的功能可以依次点击搜索
商品详情页:
加入购物车:
下单页面:
选择支付页面:
微信支付页面:
支付成功页面:
这一整套购物流程。
还包括秒杀流程:
网站管理后台(商家所见页面):
静态原型演示,打开资料\静态原型\后台
,首页登录界面.html
商家所见页面:
其中有商品的上下架功能,报表统计功能。
我们的动力二奢平台采用的模式为:B2B2C 模式。
补充:企业开发流程
0 项目(产品)经理。UI 原型图 需求文档:登录页面 商品详情页
1项目经理让美工做出原型页面
2项目经理 拉着后端开发 前端开发工程师 讨论
3定义业务(业务)接口 post [http://ip](http://ip):port/goods/add {name:手表,price:200}
4前后端分离开发
5前后端联调 dev 服务器
6测试环境发布,测试部门测试
7预发布环境(业务部门老大 、技术部门老大)
8运维人员 正式上线 生产环境
9线上bug:快速修复,当天晚上11-12 部署上线。程序员回不了家原因。
2.2 系统设计
2.2.1 前后端分离
网站后台的部分采用前后端分离方式。
以前的JavaWeb项目大多数都是java程序员又当爹又当妈,又搞前端jsp,又搞后端。随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只管前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会,那么他毕竟什么都不精。
对于后端java工程师:
把精力放在设计模式,spring+springmvc,linux,mysql事务隔离与锁机制,mongodb,http/tcp,多线程,分布式架构,弹性计算架构,微服务架构,java性能优化,以及相关的项目管理等等。
对于前端工程师:
把精力放在html5,css3,vuejs,webpack,nodejs,Google V8引擎,javascript多线程,模块化,面向切面编程,设计模式,浏览器兼容性,性能优化等等。
我们在本课程中提供与项目课程配套的管理后台的前端代码,但是不讲解前端的内容。这样我们会将更多的精力放在后端代码的开发上!
以往:jsp页面
特点:包含后端代码,页面html等混杂。
缺点:不利于大型项目协作
现在:术业有专攻。
前端:各种js(angular.js vue.js react.js) ajax axios 异步请求。
后端:spring全家桶 中间件
交互格式:json
2.2.2 技术架构-面试
面试:你们上个项目中,技术架构,用到哪些技术呢?
2.2.3 系统架构图-面试
面试:你们上个项目中,业务架构?
3. 元动力二奢-框架搭建
3.1 环境准备
导入虚拟机
补充:虚拟机:复制了?选移动了?
IP:192.168.200.128
root 123456
ifconfig 显示ip
导入虚拟机:注意修改相关号段
1修改vmware vmnet8
子网IP:192.168.200.0
DHCP设置:起始结束号段 200
NAT设置:网关 192.168.200.2
2修改本机vmnet8 地址
右键网络打开共享中心
修改适配器设置
vnet8网卡 右键 属性
ipv4协议 修改
本机IP 192.168.200.1
网关 192.168.200.2
dns 192.168.200.2
3启动虚拟机
root 123456
4xshell crt finalshell 连接到虚拟机
作业:linux 相关命令复习 vim命令
3.2 项目结构说明
结构说明:
ydleses_gateway
网关模块,根据网站的规模和需要,可以将综合逻辑相关的服务用网关路由组合到一起。在这里还可以做鉴权和限流相关操作。
ydleses_service
微服务模块,该模块用于存放所有独立的微服务工程。
ydleses_service_api bean feign
对应工程的JavaBean、Feign、以及Hystrix配置,该工程主要对外提供依赖。
ydleses_transaction_fescar
分布式事务模块,将分布式事务抽取到该工程中,任何工程如需要使用分布式事务,只需依赖该工程即可。
ydleses_web
web服务工程,对应功能模块如需要调用多个微服务,可以将他们写入到该模块中,例如网站后台、网站前台等
面试问题:
1微服务好处:
2分布式事务:
3.3 父工程搭建
3.3.1 一级父工程搭建
创建java maven父工程 ydles_parent 注意:com.ydles
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<properties>
<skipTests>true</skipTests>
</properties>
<!--依赖包-->
<dependencies>
<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
删除src
3.3.2 二级父工程模块搭建
创建ydles_gateway、ydles_service、ydles_service_api、ydles_web工程,工程全部为pom工程,并将所有工程的src文件删除。
3.4 Eureka微服务搭建
(1)pom.xml依赖
创建模块ydles_eureka,pom.xml引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
(2) application.yml配置
创建配置文件application.yml
server:
port: 6868
eureka:
client:
register-with-eureka: false #是否将自己注册到eureka中
fetch-registry: false #是否从eureka中获取信息
service-url:
defaultZone: http://127.0.0.1:${server.port}/eureka/
(3)启动类配置
创建包com.ydles.eureka 包下创建启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class);
}
}
3.5 公共模块搭建
3.5.1 全局公共模块
(1)pom.xml依赖
创建公共子模块ydles_common,pom.xml引入依赖
<dependencies>
<!--web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis 使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
(2)常用对象
创建com.ydles.entity包 ,包下封装相关公共实体类。将’资源/common_entity’下的相关资源导入工程
package com.ydles.entity;
/**
* 返回结果实体类
*/
public class Result<T> {
private boolean flag;//是否成功
private Integer code;//返回码
private String message;//返回消息
private T data;//返回数据
public Result(boolean flag, Integer code, String message, Object data) {
this.flag = flag;
this.code = code;
this.message = message;
this.data = (T)data;
}
public Result(boolean flag, Integer code, String message) {
this.flag = flag;
this.code = code;
this.message = message;
}
public Result() {
this.flag = true;
this.code = StatusCode.OK;
this.message = "执行成功";
}
//getter and setter..
}
建立类用于承载分页的数据结果
/**
* 分页结果类
*/
public class PageResult<T> {
private Long total;//总记录数
private List<T> rows;//记录
public PageResult(Long total, List<T> rows) {
this.total = total;
this.rows = rows;
}
public PageResult() {
}
//getter and setter ......
}
创建返回状态码实体类
/**
* 返回码
*/
public class StatusCode {
public static final int OK=20000;//成功
public static final int ERROR =20001;//失败
public static final int LOGINERROR =20002;//用户名或密码错误
public static final int ACCESSERROR =20003;//权限不足
public static final int REMOTEERROR =20004;//远程调用失败
public static final int REPERROR =20005;//重复操作
}
3.5.2 数据访问公共模块搭建
这个公共模块是连接mysql数据库的公共微服务模块,所以需要连接mysql的微服务都继承自此工程。
创建公共模块ydles_common_db,pom文件引入依赖
<dependencies>
<dependency>
<groupId>com.ydles</groupId>
<artifactId>ydles_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--通用mapper起步依赖-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
3.6 商品微服务搭建
3.6.1 商品微服务API工程搭建
(1)ydles_service_api 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
(2)ydles_service_api 下创建ydles_service_goods_api子模块并添加common依赖
3.6.2 微服务工程搭建
项目结构中:
1ydles_service_api 微服务的feignClient,实体类。
2ydles_service 微服务:启动类 controller service dao。
ydles_service_api:微服务接口工程。
<dependencies>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
ydles_service_goods_api:商品微服务工程,实体类。
<dependencies>
<dependency>
<groupId>com.ydles</groupId>
<artifactId>ydles_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
ydles_service_goods工程
<dependencies>
<dependency>
<groupId>com.ydles</groupId>
<artifactId>ydles_common_db</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.ydles</groupId>
<artifactId>ydles_service_goods_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
application.yml
server:
port: 9011
spring:
application:
name: goods
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.128:3306/ydles_goods?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
com.ydles.goods
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = "com.ydles.goods.dao")
public class GoodsApplication {
public static void main(String[] args) {
SpringApplication.run(GoodsApplication.class,args);
}
}
注意:@MapperScan是tk.mybatis.spring.annotation包下的,用于扫描Mapper接口
4. 商品微服务-品牌增删改查
4.1 需求分析
创建商品微服务,实现对品牌表的增删改查功能。具体包括
(1)查询全部列表数据
(2)根据ID查询实体数据
(3)增加
(4)修改
(5)删除
(6)条件查询
(7)分页查询
(8)分页+条件查询
写一个测试controller
@RequestMapping("/demo")
@RestController //组合注解
public class DemoController {
@GetMapping("/test")
public String demo(){
return "demo mesage";
}
}
测试时先启动注册中心
4.2 表结构分析
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | 品牌id | INT | ||
name | 品牌名称 | VARCHAR | ||
img | 品牌图片地址 | VARCHAR | ||
letter | 品牌的首字母 | CHAR | ||
seq | 排序 | INT |
4.3 代码实现
注意:写代码要由底往上写。
定义接口:
GET ip:port/brand/findAll
返回
{
flag:true
code:20000
msg:"成功",
data:[]
}
4.3.1 品牌列表
(1)在ydles_service_goods_api创建com.ydles.goods.pojo包,包下创建Brand实体类
@Table(name="tb_brand")
public class Brand implements Serializable{
@Id
private Integer id;//品牌id
private String name;//品牌名称
private String image;//品牌图片地址
private String letter;//品牌的首字母
private Integer seq;//排序
// getter and setter .....(省略)
}
@Table和@Id都是JPA注解,@Table用于配置表与实体类的映射关系,@Id用于标识主键属性。
(2)Dao创建
在ydles_service_goods微服务下创建com.ydles.goods.dao.BrandMapper接口,代码如下:
public interface BrandMapper extends Mapper<Brand> {
}
继承了Mapper接口,就自动实现了增删改查的常用方法。
(3)业务层
创建com.ydles.goods.service.BrandService接口,代码如下:
public interface BrandService {
/***
* 查询所有品牌
* @return
*/
public List<Brand> findAll();
}
创建com.ydles.goods.service.impl包,包下创建服务实现类 BrandServiceImpl,代码如下:
@Service
public class BrandServiceImpl implements BrandService {
@Autowired
private BrandMapper brandMapper;
@Override
public List<Brand> findAll() {
return brandMapper.selectAll();
}
}
(3)控制层
控制层 com.ydles.goods包下创建controller包 ,包下创建类
@RestController
@RequestMapping("/brand")
public class BrandController {
@Autowired
private BrandService brandService;
@GetMapping
public Result findAll(){
List<Brand> brandList = brandService.findAll();
return new Result(true, StatusCode.OK,"查询成功",brandList) ;
}
}
最后测试 http://localhost:9011/brand
可用postman测试
4.3.2 根据ID查询品牌
GET ip:port/brand/102
{
flag:true
code:20000
msg:"成功",
data:{}
}
套路代码
(1) 业务层接口
修改com.ydles.goods.service.BrandService接口,添加根据ID查询品牌数据方法,代码如下:
/**
* 根据ID查询
* @param id
* @return
*/
public Brand findById(Integer id);
(2)业务层实现
修改com.ydles.goods.service.impl.BrandServiceImpl新增方法,代码如下:
/**
* 根据ID查询
* @param id
* @return
*/
@Override
public Brand findById(Integer id){
return brandMapper.selectByPrimaryKey(id);
}
(3) 控制层
BrandController新增方法
/***
* 根据ID查询品牌数据
* @param id
* @return
*/
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
Brand brand = brandService.findById(id);
return new Result(true,StatusCode.OK,"查询成功",brand);
}
测试http://localhost:9011/brand/102
4.3.3 新增品牌
需求:POST http://localhost:9011/brand/insert
json
{
"name": "Cartier",
"image": "123"
}
返回
{
flag:true
code:20000
msg:"成功"
}
(1)业务层接口 修改com.ydles.goods.service.BrandService,新增方法
/***
* 新增品牌
* @param brand
*/
public void add(Brand brand);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl,新增增加品牌方法代码如下:
/**
* 增加
* @param brand
*/
@Override
public void add(Brand brand){
brandMapper.insertSelective(brand);
}
(3)控制层 BrandController新增方法
/***
* 新增品牌数据
* @param brand
* @return
*/
@PostMapping
public Result add(@RequestBody Brand brand){
brandService.add(brand);
return new Result(true,StatusCode.OK,"添加成功");
}
4.3.4 修改品牌
需求:PUT http://localhost:9011/brand/update/102
{
"name": "卡地亚"
}
修改某一个品牌时,只修改传进来的数据。
(1) 业务层接口
需改com.ydles.goods.service.BrandService,添加修改品牌方法,代码如下:
/***
* 修改品牌数据
* @param brand
*/
public void update(Brand brand);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl,添加修改品牌方法,代码如下:
/**
* 修改
* @param brand
*/
@Override
public void update(Brand brand){
brandMapper.updateByPrimaryKeySelective(brand);
}
(3)控制层
BrandController新增方法
/***
* 修改品牌数据
* @param brand
* @param id
* @return
*/
@PutMapping(value="/{id}")
public Result update(@RequestBody Brand brand,@PathVariable Integer id){
brand.setId(id);
brandService.update(brand);
return new Result(true,StatusCode.OK,"修改成功");
}
4.3.5 删除品牌
需求:DELETE http://localhost:9011/brand/325414
(1)业务层接口
修改com.ydles.goods.service.BrandService,添加删除品牌方法,代码如下:
/***
* 删除品牌
* @param id
*/
public void delete(Integer id);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl,新增删除品牌方法,代码如下:
/**
* 删除
* @param id
*/
@Override
public void delete(Integer id){
brandMapper.deleteByPrimaryKey(id);
}
(3)控制层 BrandController新增方法
/***
* 根据ID删除品牌数据
* @param id
* @return
*/
@DeleteMapping(value = "/{id}" )
public Result delete(@PathVariable Integer id){
brandService.delete(id);
return new Result(true,StatusCode.OK,"删除成功");
}
代码优化:
1代码里的魔法值 ——》常量或者枚举
2resful风格 :
前后端交互中,json传递数据。
数据的增删改查,使用http的不同方法。
查询 GET
新增 POST
修改 PUT
删除 DELETE
4.3.6 品牌列表条件查询
需求:Get http://localhost:9011/brand/search?name=奈儿&letter=C
sql:select * from tb_brand where name like ‘%奈儿%’ and letter=C
(1) 业务层接口
修改com.ydles.goods.service.BrandService,增加根据条件搜索品牌方法,代码如下:
/***
* 多条件搜索品牌方法
* @param searchMap
* @return
*/
public List<Brand> findList(Map<String, Object> searchMap);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl,添加根据多条件搜索品牌方法的实现,代码如下:
//缺啥补啥
@Override
public List<Brand> search(Map<String, String> searchMap) {
//sql: select * from tb_brand where letter=C and name like "%奈儿%"
Example example=new Example(Brand.class);
//criteria 放搜索条件
Example.Criteria criteria = example.createCriteria();
if(searchMap!=null){
if(StringUtils.isNotBlank(searchMap.get("name"))){
//and name like "%奈儿%"
criteria.andLike("name","%"+searchMap.get("name")+"%");
}
if(StringUtils.isNotBlank(searchMap.get("letter"))){
//and name like "%奈儿%"
criteria.andEqualTo("letter",searchMap.get("letter"));
}
}
List<Brand> brandList = brandMapper.selectByExample(example);
return brandList;
}
(3) 控制层 BrandController新增方法
/***
* 多条件搜索品牌数据
* @param searchMap
* @return
*/
@GetMapping(value = "/search" )
public Result findList(@RequestParam Map searchMap){
List<Brand> list = brandService.findList(searchMap);
return new Result(true,StatusCode.OK,"查询成功",list);
}
postman测试 http://localhost:9011/brand/search?name=
4.3.7 品牌列表分页查询
需求 GET http://localhost:9011/brand/search/3/5
sql:select * from tb_brand limit 20,10
(1) 业务层接口 修改com.ydles.goods.service.BrandService添加分页方法,代码如下:
/***
* 分页查询
* @param page
* @param size
* @return
*/
public Page<Brand> findPage(int page, int size);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl添加分页方法实现,代码如下:
/**
* 分页查询
* @param pagejava
* @param size
* @return
*/
@Override
public Page<Brand> findPage(int page, int size){
PageHelper.startPage(page,size);
return (Page<Brand>)brandMapper.selectAll();
}
(3) 控制层 BrandController新增方法
/***
* 分页搜索实现
* @param page
* @param size
* @return
*/
@GetMapping(value = "/search/{page}/{size}" )
public Result findPage(@PathVariable int page, @PathVariable int size){
Page<Brand> pageList = brandService.findPage(page, size);
PageResult pageResult=new PageResult(pageList.getTotal(),pageList.getResult());
return new Result(true,StatusCode.OK,"查询成功",pageResult);
}
4.3.8 品牌列表条件+分页查询
需求: GET http://localhost:9011/brand/searchPage/2/5?lettrt=I&name="奈儿“
(1) 业务层接口 修改com.ydles.goods.service.BrandService,增加多条件分页查询方法,代码如下:
/***
* 多条件分页查询
* @param searchMap
* @param page
* @param size
* @return
*/
Page<Brand> findPage(Map<String, Object> searchMap, int page, int size);
(2)业务层实现 修改com.ydles.goods.service.impl.BrandServiceImpl,添加多条件分页查询方法代码如下:
/**
* 条件+分页查询
* @param searchMap 查询条件
* @param page 页码
* @param size 页大小
* @return 分页结果
*/
@Override
public Page<Brand> findPage(Map<String,Object> searchMap, int page, int size){
PageHelper.startPage(page,size);
Example example=new Example(Brand.class);
Example.Criteria criteria = example.createCriteria();
if(searchMap!=null){
// 品牌名称
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"));
}
}
return (Page<Brand>)brandMapper.selectByExample(example);
}
(3)控制层 BrandController新增方法
/***
* 分页搜索实现
* @param searchMap
* @param page
* @param size
* @return
*/
@GetMapping(value = "/search/{page}/{size}" )
public Result findPage(@RequestParam Map searchMap, @PathVariable int page, @PathVariable int size){
Page<Brand> pageList = brandService.findPage(searchMap, page, size);
PageResult pageResult=new PageResult(pageList.getTotal(),pageList.getResult());
return new Result(true,StatusCode.OK,"查询成功",pageResult);
}
5. 公共异常处理
为了使我们的代码更容易维护,我们创建一个类集中处理异常
在com.ydles.goods.handler包下创建公共异常处理类BaseExceptionHandler
/**
* 统一异常处理类
*/
@ControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result error( Exception e) {
e.printStackTrace();
return new Result(false, StatusCode.ERROR, e.getMessage());
}
}
拓展:java 异常错误分类
总结:
1走进电商 元动力二奢 -面试
电商是什么 有技术特点
二奢市场地位
2元动力二奢 需求
技术架构
业务架构
3搭建项目
导入虚拟机
代码架构
父工程 二级父工程 注册中心
商品微服务
4品牌的crud
查询:tk.mybatis
条件查询:example
分页:pageHelper
5公共异常处理
异常分类
第2章 分布式文件存储
角色:商品 后端开发
学习目标
目标1:能够CORS解决跨域问题
目标2:理解规格参数模板与商品分类表结构
目标3:掌握通用mapper自定义方法的使用
目标4:能够使用分布式文件存储FastDFS上传文件
1. 跨域解决方案CORS-面试
1.1 什么是跨域
面试:
1什么是跨域问题:浏览器的同源策略,导致不能向其他域名发送异步请求。
2同源策略:具有相同的协议(protocol),主机(host)和端口号(port)
页面:[http://192.168.1.1:8080/search](http://192.168.1.1:8080/search) 商品搜索页面
https://192.168.1.1:8080/search?name=手表 ----->no
[http://192.168.1.2:8080/search](http://192.168.1.2:8080/search) ------------->no
[http://192.168.1.1:8081/goods/add](http://192.168.1.1:8081/goods/add) ------------->no
[http://192.168.1.1:8080/goods/add](http://192.168.1.1:8080/goods/add) -------------> YES
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
跨域问题:浏览器的同源策略限制。会报错。
如果跨域调用,会出现如下错误:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:9100‘ is therefore not allowed access. The response had HTTP status code 400.
由于我们采用的是前后端分离的编程方式,前端和后端必定存在跨域问题。解决跨域问题可以采用CORS
1.2 CORS简介
CORS:跨域资源共享
条件:IE10以上
本质:请求头增加一个参数,开启跨域请求。
CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
请求过程如下图:
Preflight Request:
然后服务器端给我们返回一个PreflightResponse
我们怎么做:那么具体如何实现呢?springMVC的版本在4.2或以上版本,可以使用注解实现跨域。 我们只需要在Controller类上添加注解@CrossOrigin
就可以了。
2. 导入基础架构项目(了解)
需求:刚进一家新公司,你会拿到是代码呢?几十个人写了几年。
解压项目基础架构包 即可。
使用idea打开,观察项目架构。
2.1 导入项目
在idea
下点击file
菜单中的open
项, 找到ydles_parent位置打开
3.规格参数与分类管理(后台)-重点
我们本小节只需同学们理解管理后台规格参数与分类管理的需求和表结构的设计,没有代码实现,因为管理后台所需的代码在基础工程中已经帮我们生成了,无需自己编写。
打开day01静态页面-后台首页。查看商品-规格参数。
分类:分类 衣物—-》裙子—-》连衣裙、百褶裙、婚纱
模板:裙子模板———》大小:s m l 裙子颜色:黑色、红色 胸围:100 腰围:100 臀围:100
手机模板----》手机颜色:黑色、红色 内存:4G 6G 存储:128G 256G
参数:量变 胸围:110 腰围:100 臀围:100 内存:4G 6G 存储:128G 256G
规格:质变 大小:s m l 手机颜色:黑色、红色
分类:手机 电脑 电视
品牌:华为 小米 苹果
表:
3.1 规格参数管理
3.1.1 需求分析
规格参数模板是用于管理规格参数的单元。
前端交互方式见管理后台的静态原型
3.1.2 表结构分析
规格参数模板相关的表有3个
tb_template表(模板表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | ID | INT | ||
name | 模板名称 | VARCHAR | ||
spec_num | 规格数量 | INT | ||
para_num | 参数数量 | INT |
tb_spec表(规格表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | ID | INT | ||
name | 名称 | VARCHAR | ||
options | 规格选项 | VARCHAR | ||
seq | 排序 | INT | ||
template_id | 模板ID | INT |
tb_para表(参数表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | id | INT | ||
name | 名称 | VARCHAR | ||
options | 选项 | VARCHAR | ||
seq | 排序 | INT | ||
template_id | 模板ID | INT |
模板与规格是一对多关系 ,模板与参数是一对多关系
3.2 分类管理
3.2.1 需求分析
商品分类一共分三级管理,主要作用是在网站首页中显示商品导航,以及在管理后台管理商品时使用。
3.2.2 表结构分析
tb_category 表 (商品分类)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | 分类ID | INT | ||
name | 分类名称 | VARCHAR | ||
goods_num | 商品数量 | INT | ||
is_show | 是否显示 | CHAR | 0 不显示 1显示 | |
is_menu | 是否导航 | CHAR | 0 不时导航 1 为导航 | |
seq | 排序 | INT | ||
parent_id | 上级ID | INT | ||
template_id | 模板ID | INT |
商品分类与模板是多对一关系
4.通用mapper自定义方法-重点掌握
4.1 根据商品分类名称查询品牌列表
需求:根据商品分类名称查询品牌列表
http://localhost:9001/brand/category/女士包袋
4.1.1 表结构分析
基于上述的讲解已知分类与品牌之间的关系属于多对多关系,这里通过tb_category_brand表来建立关联关系。
分类-品牌中间表
列名 | 类型 | 说明 |
---|---|---|
category_id | int(11) | 分类ID |
brand_id | int(11) | 品牌ID |
4.1.2 代码实现
写出sql
SELECT NAME,image FROM tb_brand WHERE id IN ( SELECT brand_id FROM tb_category_brand WHERE category_id IN ( SELECT id FROM tb_category WHERE NAME = '女士包袋' ) )
(1)修改BrandMapper,新增方法定义
/**
* 根据分类名称查询品牌列表
* @param categoryName
* @return
*/
@Select("SELECT name,img FROM tb_brand WHERE id IN (SELECT brand_id FROM tb_category_brand WHERE category_id IN (SELECT id FROM tb_category WHERE NAME=#{name}) )order by seq")
public List<Map> findListByCategoryName(@Param("name") String categoryName);
(2)修改BrandService,新增方法定义
/**
* 根据商品分类名称查询品牌列表
* @param categoryName
* @return
*/
public List<Map> findListByCategoryName(String categoryName);
(3)BrandServiceImpl实现方法
@Override
public List<Map> findListByCategoryName(String categoryName) {
return brandMapper.findListByCategoryName(categoryName);
}
(4)BrandController新增方法
/**
* 根据分类名称查询品牌列表
* @param category
* @return
*/
@GetMapping("/category/{category}")
public Result findListByCategoryName(@PathVariable String category){
System.out.println(category);
List<Map> brandList = brandService.findListByCategoryName(category);
return new Result(true,StatusCode.OK,"查询成功",brandList);
}
测试: http://localhost:9001/brand/category/女士包袋
4.2 根据商品分类名称查询规格列表
http://localhost:9001/spec/category/女士包袋
4.2.1 表结构分析
我们这里会用到规格表、模板表、分类表 注意:以下表结构已省略无关字段
tb_template 表(模板表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | ID | INT | ||
name | 模板名称 | VARCHAR |
tb_spec 表( 规格表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | ID | INT | ||
name | 名称 | VARCHAR | ||
options | 规格选项 | VARCHAR | ||
seq | 排序 | INT | ||
template_id | 模板ID | INT |
tb_category 表 (商品分类)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | 分类ID | INT | ||
name | 分类名称 | VARCHAR | ||
parent_id | 上级ID | INT | ||
template_id | 模板ID | INT |
4.2.2 代码实现
sql:
select name,options from tb_spec where template_id in (select template_id from tb_category where name = '女士包袋') order by seq
(1)SpecMapper新增方法定义
@Select("SELECT name,options FROM tb_spec WHERE template_id IN ( SELECT template_id FROM tb_category WHERE NAME=#{categoryName}) order by seq")
public List<Map> findListByCategoryName(@Param("categoryName") String categoryName);
(2)SpecService新增方法定义
/**
* 根据商品分类名称查询规格列表
* @param categoryName
* @return
*/
public List<Map> findListByCategoryName(String categoryName);
(3)SpecServiceImpl实现方法
@Override
public List<Map> findListByCategoryName(String categoryName) {
List<Map> specList = specMapper.findListByCategoryName(categoryName);
for(Map spec:specList){
String[] options = ((String) spec.get("options")).split(",");//规格选项列表
spec.put("options",options);
}
return specList;
}
(4)SpecController新增方法
/**
* 根据商品分类名称查询规格列表
* @param category
* @return
*/
@GetMapping("/category/{category}")
public Result findListByCategoryName(@PathVariable String category){
List<Map> specList = specService.findListByCategoryName(category);
return new Result(true,StatusCode.OK,"",specList);
}
测试: http://localhost:9001/spec/category/女士包袋
5. 分布式文件存储-FastDFS
FastDFS:Fast distribite file system 有小有多的小文件
HDFS:hadoop distribite file system 大文件
5.1 FastDFS简介
5.1.1 FastDFS体系结构
FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
作者:阿里 P8 200w年薪 余庆大神
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
tracker: 元数据信息,文件上传时间、地址、大小。
记录storage集群信息。
storage:存数据。
5.1.2 上传流程
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
fileId: group1/M00/00/00/asdfsadfsadfsadfsadfsadfsa.png
组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。
虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path对应。如果配置了
/data/00-ff/00-ff/——->/M00
/data2——>/M01
数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据
文件。
文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储
服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
5.2 FastDFS搭建-了解
已经在虚拟机中搭建完毕。
我们使用Docker搭建FastDFS的开发环境
拉取镜像
docker pull morunchang/fastdfs
运行tracker
docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
运行storage
docker run -d --name storage --net=host -e TRACKER_IP=<your tracker server address>:22122 -e GROUP_NAME=<group name> morunchang/fastdfs sh storage.sh
- 使用的网络模式是–net=host, 替换为你机器的Ip即可
- 是组名,即storage的组
- 如果想要增加新的storage服务器,再次运行该命令,注意更换 新组名
(4)修改nginx的配置
进入storage的容器内部,修改nginx.conf
docker exec -it storage /bin/bash
进入后
vi /data/nginx/conf/nginx.conf
添加以下内容
location /group1/M00 {
proxy_next_upstream http_502 http_504 error timeout invalid_header;
proxy_cache http-cache;
proxy_cache_valid 200 304 12h;
proxy_cache_key $uri$is_args$args;
proxy_pass http://fdfs_group1;
expires 30d;
}
(5)退出容器
exit
(6)重启storage容器
docker restart storage
5.3 文件存储微服务
创建文件管理微服务ydles_service_file,该工程主要用于实现文件上传以及文件删除等功能。
(1)修改pom.xml,引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>com.ydles</groupId>
<artifactId>ydles_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
(2)在resources文件夹下创建fasfDFS的配置文件fdfs_client.conf
connect_timeout = 60
network_timeout = 60
charset = UTF-8
http.tracker_http_port = 8080
tracker_server = 192.168.200.128:22122
connect_timeout:连接超时时间,单位为秒。
network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败
charset: 字符集
http.tracker_http_port :.tracker的http端口
tracker_server: tracker服务器IP和端口设置
(3)在resources文件夹下创建application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
server:
port: 9008
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
max-file-size是单个文件大小,max-request-size是设置总上传的数据大小
(4)创建com.ydles.file包,创建启动类FileApplication
@SpringBootApplication
@EnableEurekaClient
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class);
}
}
5.4 文件上传-重点
5.4.1 文件信息封装
文件上传一般都有文件的名字、文件的内容、文件的扩展名、文件的md5值、文件的作者等相关属性,我们可以创建一个对象封装这些属性,代码如下:
FastDFSFile:文件实体类
FastDFSClient:文件工具类,封装底层方法。
FileController:接口
创建com.ydles.file.pojo.FastDFSFile
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;
}
// getter and setter ...
}
5.4.2 文件操作
创建FastDFSClient类,放在com.ydles.file.util下在该类中实现FastDFS信息获取以及文件的相关操作,
代码如下:
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
*/
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;
}
}
5.4.3 文件上传
创建一个FileController,在该控制器中实现文件上传操作,代码如下:
@RestController
@RequestMapping("/file")
public class FileController {
@PostMapping("/upload")
public Result uploadFile(MultipartFile file){
try{
//判断文件是否存在
if (file == null){
throw new RuntimeException("文件不存在");
}
//获取文件的完整名称
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)){
throw new RuntimeException("文件不存在");
}
//获取文件的扩展名称 abc.jpg jpg
String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
//获取文件内容
byte[] content = file.getBytes();
//创建文件上传的封装实体类
FastDFSFile fastDFSFile = new FastDFSFile(originalFilename,content,extName);
//基于工具类进行文件上传,并接受返回参数 String[]
String[] uploadResult = FastDFSClient.upload(fastDFSFile);
//封装返回结果
String url = FastDFSClient.getTrackerUrl()+uploadResult[0]+"/"+uploadResult[1];
return new Result(true,StatusCode.OK,"文件上传成功",url);
}catch (Exception e){
e.printStackTrace();
}
return new Result(false, StatusCode.ERROR,"文件上传失败");
}
}
5.5 Postman测试文件上传
步骤:
1、选择post请求方式,输入请求地址 http://localhost:9008/upload
2、填写Headers
Key:Content-Type
Value:multipart/form-data
3、填写body
选择form-data 然后选择文件file 点击添加文件,最后发送即可。
总结:
1跨域-面试
什么是跨域:由于浏览器的同源策略,导致不能向其他域发送异步请求。
什么是同源策略:协议,域名,端口号
CORS @CrossOrigin
2回看其他同事写的代码
3规格参数模板分类品牌 关系
4自定义mapper
复杂sql需求,自定义mapper方法 sql书写
5fastDFS
上传写完
作业:删除 下载图片 fileID
第3章 微服务网关限流&鉴权
角色:网关组一员。
课程目标
- 掌握微服务网关Gateway的系统搭建。
- 掌握网关限流的实现
- 能够使用BCrypt实现对密码的加密与验证
- 了解加密算法
- 能够使用JWT实现微服务鉴权
1.微服务网关Gateway 过滤+路由
1.1 微服务网关概述-面试
不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 认证复杂,每个服务都需要独立认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
以上这些问题可以借助网关解决。
网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
画图
优点如下:
- 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
- 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
- 易于统一认证授权。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数
总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。
实现微服务网关的技术有很多,
- nginx Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
- zuul ,Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。 不维护了,弃用。
- spring-cloud-gateway, 是spring 出品的 基于spring 的网关项目,集成断路器,路径重写,性能比Zuul好。
我们使用gateway这个网关技术,无缝衔接到基于spring cloud的微服务开发中来。
gateway官网:
https://spring.io/projects/spring-cloud-gateway
1.2 微服务网关微服务搭建
由于我们开发的系统 有包括前台系统和后台系统,后台的系统给管理员使用。那么也需要调用各种微服务,所以我们针对管理后台搭建一个网关微服务。分析如下:
搭建步骤:
创建项目 ydles_gateway_system
1依赖。二级父工程导入了,所以也可以不导。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2启动类:com.ydles.system.GatewayApplication
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3配置文件:application.yml
spring:
application:
name: sysgateway
cloud:
gateway:
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
redis:
host: 192.168.200.128
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
参考官方手册:
测试:
直接访问goods服务:http://localhost:9001/brand
通过网关访问:http://localhost:9101/goods/brand
localhost:9001/brand
路由功能:
1网关访问:http://localhost:9101/goods/brand/category/手机
2uri: /goods/brand/category/女士包袋
3商品微服务:http://localhost:9001/brand/category/女士包袋
1.3 微服务网关跨域
修改application.yml ,在spring.cloud.gateway节点添加配置,
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
最终配置文件如下:
spring:
application:
name: sysgateway
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
1.4 微服务网关过滤器
我们可以通过网关过滤器,实现一些逻辑的处理,比如ip黑白名单拦截、特定地址的拦截等。下面的代码中做了两个过滤器,并且设定的先后顺序,只演示过滤器与运行效果。(具体逻辑处理部分学员实现)
需求:
1 ip拦截。pdd ip是淘宝,不让你百亿补贴。
2 危险操作日志打印:/admin/delete 记录
(1)ydles_gateway_system创建IpFilter
需求:ip黑名单拦截
IpFilter com.ydles.system.filter包下
@Component
public class IpFilter implements GlobalFilter, Ordered {
//具体业务逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取客户端的访问ip
System.out.println("经过了第一个过滤器");
ServerHttpRequest request = exchange.getRequest();
InetSocketAddress remoteAddress = request.getRemoteAddress();
System.out.println("ip:"+remoteAddress.getHostName());
//放行
return chain.filter(exchange);
}
//过滤器的执行优先级,返回值越小,执行优先级越高
@Override
public int getOrder() {
return 1;
}
}
(1)ydles_gateway_system创建UrlFilter
需求:特定地址的拦截 /admin/delete log
@Component
public class UrlFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("经过了第二个过滤器");
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
System.out.println("path:"+path);
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 2;
}
}
访问测试: http://localhost:9101/goods/brand/category/手机
2 网关限流
概念:两个概念
限流:当我们的系统被频繁的请求的时候,就有可能将系统压垮,需要在每一个微服务中做限流操作
网关限流:在网关系统做限流 goods 1w/s search 5w/s
2.1 思路分析
2.2 令牌桶算法
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
如下图:
这个算法的实现,有很多技术,Guava(读音: 瓜哇)是其中之一,redis客户端也有其实现。
2.3 网关限流代码实现
需求:每个ip地址1秒内只能发送1次请求goods,多出来的请求返回429错误。
代码实现:
(1)spring cloud gateway 默认使用redis的RateLimter限流算法来实现。所以我们要使用首先需要引入redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
(2)定义KeyResolver
在GatewayApplicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。
@Bean
public KeyResolver ipKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
};
}
(3)配置文件配置redis及限流策略。某个服务的filters下,加入如下代码。
spring:
application:
name: sysgateway
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
# 配置Redis 127.0.0.1可以省略配置
redis:
host: 192.168.200.128
port: 6379
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
解释:
- burstCapacity:令牌桶总容量。
- replenishRate:令牌桶每秒填充平均速率。
- key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
通过在replenishRate
和中设置相同的值来实现稳定的速率burstCapacity
。设置burstCapacity
高于时,可以允许临时突发replenishRate
。在这种情况下,需要在突发之间允许速率限制器一段时间(根据replenishRate
),因为2次连续突发将导致请求被丢弃(HTTP 429 - Too Many Requests
)
key-resolver: “#{@userKeyResolver}” 用于通过SPEL表达式来指定使用哪一个KeyResolver.
如上配置:
表示 一秒内,允许 一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。
最大突发状况 也只允许 一秒内有一次请求,可以根据业务来调整 。
(4)测试
启动redis
启动注册中心
启动商品微服务
启动gateway网关
打开浏览器 http://localhost:9101/goods/brand
快速刷新,当1秒内发送多次请求,就会返回429错误。
http 响应码
2 没问题
200
3 没问题,需要浏览器做操作
302 303 转发
4 客户端有问题
404 not found 429
5服务端
501 系统繁忙,请稍后再试
3. BCrypt密码加密
3.1 BCrypt快速入门
在用户模块,对于用户密码的保护,通常都会进行加密。我们通常对密码进行加密,然后存放在数据库中,在用户进行登录的时候,将其输入的密码进行加密然后与数据库中存放的密文进行比较,以验证用户密码是否正确。 目前,MD5和BCrypt比较流行。相对来说,BCrypt比MD5更安全。因为其内部引入的加盐机制
BCrypt 官网http://www.mindrot.org/projects/jBCrypt/
(1)新建测试类,main方法中编写代码,实现对密码的加密
public class TestBcrypt {
public static void main(String[] args) {
/**
* 得到盐
* 盐是一个随机生成的含有29个字符的字符串,并且会与密码一起合并进行最终的密文生成
* 并且每一次生成的盐的值都是不同的
*/
for(int i=0;i<10;i++){
String gensalt = BCrypt.gensalt();
System.out.println("salt:"+gensalt);
String saltPassword = BCrypt.hashpw("123456", gensalt);
System.out.println("本次生成的密码:"+saltPassword);
}
}
}
(2)main方法中编写代码,实现对密码的校验。BCrypt不支持反运算,只支持密码校验。
//校验密码
boolean checkpw = BCrypt.checkpw("123456", saltPassword);
System.out.println("密码校验结果:"+checkpw);
结果:每次盐不一样,加密后不一样,但都能验证。
3.2 新增管理员密码加密-掌握
3.2.1 需求与表结构分析
ydles_system下tb_admin新增管理员,使用BCrypt进行密码加密
id | int | 主键id |
---|---|---|
login_name | varchar | 登录名 |
password | varchar | 密码 |
status | char | 状态 |
3.2.2 代码实现
(1)修改ydles_service_system项目的AdminServiceImpl
@Override
public void add(Admin admin){
//获取盐
String gensalt = BCrypt.gensalt();
//对用户的密码进行加密
String hashpw = BCrypt.hashpw(admin.getPassword(), gensalt);
admin.setPassword(hashpw);
adminMapper.insert(admin);
}
测试:postman http://localhost:9101/system/admin
{
"loginName":"test1",
"password":"123456",
"status":"1"
}
3.3 管理员登录密码验证
3.3.1 需求分析
系统管理用户需要管理后台,需要先输入用户名和密码进行登录,才能进入管理后台。
思路:
用户发送请求,输入用户名和密码
后台管理微服务controller接收参数,验证用户名和密码是否正确,如果正确则返回用户登录成功结果
3.3.2 代码实现
(1)AdminService新增方法定义
/**
* 登录验证密码
* @param admin
* @return
*/
boolean login(Admin admin);
(2)AdminServiceImpl实现此方法
@Override
public boolean login(Admin admin) {
//根据登录名查询管理员
Admin admin1=new Admin();
admin1.setLoginName(admin.getLoginName());
admin1.setStatus("1");
Admin admin2 = adminMapper.selectOne(admin1);//数据库查询出的对象
if(admin2==null){
return false;
}else{
//验证密码, Bcrypt为spring的包, 第一个参数为明文密码, 第二个参数为密文密码
return BCrypt.checkpw(admin.getPassword(),admin2.getPassword());
}
}
(3)AdminController新增方法
/**
* 登录
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
boolean login = adminService.login(admin);
if(login){
return new Result();
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错误");
}
}
测试:postman http://localhost:9101/system/admin/login
{
"loginName":"test1",
"password":"1234561"
}
4.加密算法(了解)
由于在学习JWT的时候会涉及使用很多加密算法, 所以在这里做下扫盲, 简单了解就可以
加密算法种类有:
4.1.可逆加密算法
解释: 加密后, 密文可以反向解密得到密码原文.
4.1.1. 对称加密
秘钥:2(每位向后推2个)
abc——cde———->abc
【文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥】
解释: 在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。
优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点: 没有非对称加密安全.
用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。
常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
4.1.2. 非对称加密
【两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密】
解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.
加密与解密:
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
签名:
- 私钥签名, 持有公钥进行验证是否被篡改过.
优点: 非对称加密与对称加密相比,其安全性更好;
缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。 用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.
常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)
拓展:linux 免密登陆。
4.2.不可逆加密算法
解释: 一旦加密就不能反向解密得到密码原文.
种类: Hash加密算法, 散列算法, 摘要算法等
用途:一般用于效验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。
常见的不可逆加密算法有: MD5、SHA、HMAC
4.3.Base64编码
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。Base64编码可用于在HTTP环境下传递较长的标识信息。采用Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。注意:Base64只是一种编码方式,不算加密方法。
在线编码工具:
http://www.jsons.cn/img2base64/
拓展:字符编码相关知识。
5. JWT 实现微服务鉴权
5.1 什么是微服务鉴权
我们之前已经搭建过了网关,使用网关在系统中比较适合进行权限校验。
单点登录的特点是:
1、认证系统为独立的系统。
2、各子系统通过Http或其它协议与认证系统通信,完成用户认证。
3、用户身份信息存储在Redis集群。
Java中有很多用户认证的框架都可以实现单点登录:
1、Apache Shiro.
2、CAS
3、Spring security CAS
5.2 JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(playload)
载荷就是存放有效信息的地方。
定义一个payload:
{"sub":"1234567890","name":"itlils","admin":true,"age":18}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
hs256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==",secret)
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
5.3 JJWT签发与验证token
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:
5.3.1 创建token
(1)新建项目jwtTest中的pom.xml中添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2) 创建测试类,代码如下
public static void main(String[] args) {
//获取系统的当前时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
//生成jwt令牌
JwtBuilder jwtBuilder = Jwts.builder()
.setId("66")//设置jwt编码
.setSubject("元动力二奢")//设置jwt主题
.setIssuedAt(new Date())//设置jwt签发日期
//.setExpiration(date)//设置jwt的过期时间
.signWith(SignatureAlgorithm.HS256, "ydlershe");
//生成jwt
String jwtToken = jwtBuilder.compact();
System.out.println(jwtToken);
}
运行打印结果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyNyIsInN1YiI6InlkbGVyc2hlc2hhbmdjaGVuZyIsImlhdCI6MTYzNDYzMjA0N30.ZTrtNXaAQzFIr1MipUTMaprB2xYcvz5_XbNlvN4Adeoxxxxxxxxxx eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyNyIsInN1YiI6InlkbGVyc2hlc2hhbmdjaGVuZyIsImlhdCI6MTYzNDYzMjA0N30.ZTrtNXaAQzFIr1MipUTMaprB2xYcvz5_XbNlvN4Adeoey
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
验证base64 https://tool.oschina.net/encrypt?type=3
5.3.2 解析token
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyNyIsInN1YiI6InlkbGVyc2hlc2hhbmdjaGVuZyIsImlhdCI6MTYzNDYzMjMyNn0.orvxPJcAsOquRjcjj947vMmD11QoZn8BuHKSeCEbV8g";
Claims claims = Jwts.parser().setSigningKey("ydlershe").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
运行打印效果:
{jti=27, sub=ydlersheshangcheng, iat=1634632326}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.
注意:设置签名key必须和生成时一致。
5.3.3 设置过期时间
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
(1)创建token 并设置过期时间
//当前时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("小白")//设置主题 可以是JSON数据
.setIssuedAt(new Date())//设置签发日期
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256,"ydlershe");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
解释:
.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
运行,打印效果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI
(2)解析TOKEN
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";
Claims claims = Jwts.parser().setSigningKey("ydlershe").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
打印效果:
当前时间超过过期时间,则会报错。
5.3.4 自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
创建token:
@Test
public void createJWT(){
//当前时间
long currentTimeMillis = System.currentTimeMillis();
currentTimeMillis+=1000000L;
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("小白")//设置主题 可以是JSON数据
.setIssuedAt(new Date())//设置签发日期
.setExpiration(date)//设置过期时间
.claim("roles","admin")//设置角色
.signWith(SignatureAlgorithm.HS256,"ydlershe");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
}
运行打印效果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI
解析TOKEN:
//解析
@Test
public void parseJWT(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI";
Claims claims = Jwts.parser().setSigningKey("ydlershe").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
}
运行效果:
5.4 元动力二奢微服务鉴权代码实现-重点
5.4.1 思路分析-重点理解
1.1 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
1.2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
2.1 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
2.2 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
一、登陆:
1用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进 行登录
2用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
二、访问资源:
1用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
2网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
5.4.2 系统微服务签发token
(1)在ydles_service_system添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)在ydles_service_system中粘贴JwtUtil工具类。看懂即可。
package com.ydles.system.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "ydlershe";
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("admin") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);// 设置过期时间
return builder.compact();
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}
(3)修改AdminController的login方法, 用户登录成功则签发TOKEN
/**
* 登录
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
boolean login = adminService.login(admin);
if(login){ //如果验证成功
Map<String,String> info = new HashMap<>();
info.put("username",admin.getLoginName());
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
info.put("token",token);
return new Result(true, StatusCode.OK,"登录成功",info);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错误");
}
}
使用postman 测试 http://localhost:9101/system/admin/login
{"loginName":"itlilaoshi","password":"123456"}
5.4.3 网关过滤器验证token
(1)在ydles_gateway_system网关系统添加依赖
<!--鉴权-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)导入jwt工具类
package com.ydles.gateway.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* jwt校验工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "ydlershe";
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
(3)创建过滤器,用于token验证
/**
* 鉴权过滤器 验证token
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 获取请求
ServerHttpRequest request = exchange.getRequest();
//2. 则获取响应
ServerHttpResponse response = exchange.getResponse();
//3. 如果是登录请求则放行
if (request.getURI().getPath().contains("/admin/login")) {
return chain.filter(exchange);
}
//4. 获取请求头
HttpHeaders headers = request.getHeaders();
//5. 请求头中获取令牌
String token = headers.getFirst(AUTHORIZE_TOKEN);
//6. 判断请求头中是否有令牌
if (StringUtils.isEmpty(token)) {
//7. 响应中放入返回的状态吗, 没有权限访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//8. 返回
return response.setComplete();
}
//9. 如果请求头中有令牌则解析令牌
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//10. 解析jwt令牌出错, 说明令牌过期或者伪造等不合法情况出现
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//11. 返回
return response.setComplete();
}
//12. 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
(4)测试:
注意: 数据库中管理员账户为 : itlilaoshi, 密码为 : 123456
如果不携带token直接访问,则返回401错误
如果携带正确的token,则返回查询结果
1首先登陆拿到令牌
http://localhost:9101/system/admin/login
2访问某个资源
http://localhost:9101/system/admin
返回401
3header中带上token
可以是正常访问
总结:
1网关 过滤+路由
2网关限流
令牌桶
代码实现 reyresolver 配置流速,桶大小
3密码加密
密码 存成密文
md5+ 加盐
BCrypt
4加密算法
可逆
对称加密
非对称
不可逆
5jwt实现网关鉴权
jwt base64( head).base64(payload).hs256(前两部分,secret)
jjwt 生成jwt 解析
图:网关集成鉴权逻辑
第4章 商品管理
角色:商品组,后端开发。
学习目标
- 能够使用开源算法snowflake生成分布式id
- 完成新增和修改商品功能
- 完成商品审核和上下架功能
- 完成删除与还原商品功能
1. 分布式ID生成解决方案-面试
为什么生成唯一ID
背景:互联网多模块微服务情况下,高并发。
程序:goods order
数据库:goods order
order微服务 100 id 1 id 1
采用分库分表。order1\order2等。
所以主键的生成成为问题。
每个模块存入数据库时,需要生成唯一的ID。
1.1 分布式ID生成解决方案
1.1.1 UUID
常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。
gateway服务下测试uuid生成
代码例子:a8fb5e44-f841-49ff-b10b-01d3afb2b238
36*8=288bit
优点:
1)简单,代码方便。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
缺点:
1)没有排序,无法保证趋势递增。
2)UUID往往是使用字符串存储,查询的效率比较低。 id int 快!
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。32string*8=256bit
4)传输数据量大
5)不可读。
1.1.2 Redis
gateway服务下测试redis递增
long 1=incr(id”)
long 2=incr(“id”)
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
优点:
1)不依赖于数据库,灵活方便,且性能优于数据库。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。 id long
缺点:
1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2)需要编码和配置的工作量比较大。
3)网络传输造成性能下降。
1.1.3 开源算法snowflake
64位bit数字。详情见拓展资料。
1000000000000000000000000000000000000000000000000000000000000001
0 1010101010000000 0000100001 0000000001
0 1010101010000000 0000100001 0000000010
0 1010101010000011 0000100001 0000000001 4096
同一毫秒下同一机器 4096
同一机器1秒 4096000
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。 32机房 32台goods
缺点: 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
我们在《元动力二奢》系统中采用的就是开源算法snowflake
1.2 snowflake快速入门-掌握
1.2.1 快速入门
(1)common下导入IdWorker工具类,方便其他模块调用。(2)编写代码
然后goods工程下,创建测试类。
观察特性:唯一性、增长性
public static void main(String[] args) {
IdWorker idWorker = new IdWorker(1,1);
for(int i=0;i<1000;i++){
long id = idWorker.nextId();
System.out.println(id);
}
}
1.2.2 配置分布式ID生成器
(1)IdWorker.java拷贝到ydles_common工程com.ydles.util包中
(2)ydles_service_goods的application.yml添加配置
workerId: 0
datacenterId: 0
(3)修改GoodsApplication,增加代码
@Value("${workerId}")
private Integer workerId;
@Value("${datacenterId}")
private Integer datacenterId;
@Bean
public IdWorker idWorker(){
return new IdWorker(workerId,datacenterId);
}
2 新增和修改商品
2.1 概念与表结构分析
2.1.1 SPU与SKU概念
SPU = Standard Product Unit (标准产品单位)
概念 : SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
通俗点讲,属性值、特性相同的货品就可以称为一个 SPU
例如:华为P50 就是一个 SPU
https://item.jd.com/100024533316.html
SKU=stock keeping unit( 库存量单位)
SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
在服装、鞋类商品中使用最多最普遍。
例如:华为P50 红色 64G 就是一个 SKU
华为P50 红色 8+64G 3000 就是一个 SKU。我们真实能买到的具有具体数据的一个物品。
2.1.2 表结构分析
tb_spu 表 (SPU表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | 主键 | VARCHAR | ||
sn | 货号 | VARCHAR | ||
name | SPU名 | VARCHAR | ||
caption | 副标题 | VARCHAR | ||
brand_id | 品牌ID | INT | ||
category1_id | 一级分类 | INT | ||
category2_id | 二级分类 | INT | ||
category3_id | 三级分类 | INT | ||
template_id | 模板ID | INT | ||
freight_id | 运费模板id | INT | ||
img | 图片 | VARCHAR | ||
imgs | 图片列表 | VARCHAR | ||
sale_service | 售后服务 | VARCHAR | ||
introduction | 介绍 | TEXT | ||
spec_items | 规格列表 | VARCHAR | ||
para_items | 参数列表 | VARCHAR | ||
sale_num | 销量 | INT | ||
comment_num | 评论数 | INT | ||
is_marketable | 是否上架 | CHAR | ||
is_enable_spec | 是否启用规格 | CHAR | ||
is_delete | 是否删除 | CHAR | ||
status | 审核状态 | CHAR |
tb_sku 表(SKU商品表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | 商品id | VARCHAR | ||
sn | 商品条码 | VARCHAR | ||
name | SKU名称 | VARCHAR | ||
price | 价格(分) | INT | ||
num | 库存数量 | INT | ||
alert_num | 库存预警数量 | INT | ||
img | 商品图片 | VARCHAR | ||
imgs | 商品图片列表 | VARCHAR | ||
weight | 重量(克) | INT | ||
create_time | 创建时间 | DATETIME | ||
update_time | 更新时间 | DATETIME | ||
spu_id | SPUID | BIGINT | ||
category_id | 类目ID | INT | ||
category_name | 类目名称 | VARCHAR | ||
brand_name | 品牌名称 | VARCHAR | ||
spec | 规格 | VARCHAR | ||
sale_num | 销量 | INT | ||
comment_num | 评论数 | INT | ||
status | 商品状态 1-正常,2-下架,3-删除 | CHAR |
2.2 实现思路
前端传递给后端的数据格式 是一个spu对象和sku列表组成的对象
{
"spu": {
"name": "PRADA包包2020秋季限量版",
"caption": "PRADA最新包包",
"brandId": 103,
"category1Id": 903,
"category2Id": 945,
"category3Id": 946,
"freightId": 10,
"image": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"images": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"introduction": "PRADA包包2020秋季限量版,抢完为止",
"paraItems": "{"出厂年份":"2020","赠品":"袋子"}",
"saleService": "八天包退,闪电退货",
"sn": "020102331",
"specItems": "{"颜色":["黑","蓝"],"二手程度":["崭新出厂","略有磨损","久经沙场","破损不堪","战痕累累"]}",
"templateId": 42
},
"skuList": [
{
"sn": "10192010293",
"num": 100,
"alertNum": 20,
"price": 10000,
"spec": "{"颜色":"黑","二手程度":"崭新出厂"}",
"image": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"images": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"status": "1",
"weight": 530
},{
"sn": "10192010292",
"num": 100,
"alertNum": 20,
"price": 8000,
"spec": "{"颜色":"蓝色","二手程度":"略有磨损"}",
"image": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"images": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"status": "1",
"weight": 530
},{
"sn": "10192010292",
"num": 100,
"alertNum": 20,
"price": 4000,
"spec": "{"颜色":"红色","二手程度":"久经沙场"}",
"image": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"images": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"status": "1",
"weight": 530
}
]
}
2.3 代码实现-日常工作
2.3.1 SPU与SKU列表的保存
代码实现:
(1)ydles_service_goods_api工程创建组合实体类Goods
/**
* 商品组合实体类
*/
public class Goods implements Serializable {
private Spu spu;
private List<Sku> skuList;
public Spu getSpu() {
return spu;
}
public void setSpu(Spu spu) {
this.spu = spu;
}
public List<Sku> getSkuList() {
return skuList;
}
public void setSkuList(List<Sku> skuList) {
this.skuList = skuList;
}
}
(2)ydles_service_goods工程SpuService新增方法add(Goods goods)
/***
* 新增
* @param goods
*/
void add(Goods goods);
(3)ydles_service_goods工程SpuServiceImpl实现此方法
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private SkuMapper skuMapper;
@Autowired
private BrandMapper brandMapper;
@Autowired
private IdWorker idWorker;
/**
* 保存商品 SPU+SKU列表
* @param goods 商品组合实体类
*/
@Transactional
@Override
public void add(Goods goods) {
Spu spu = goods.getSpu();
long spuId = idWorker.nextId();
spu.setId(String.valueOf(spuId));
spu.setIsDelete("0");
spu.setIsMarketable("0");
spu.setStatus("0");
spuMapper.insertSelective(spu);
//保存sku集合数据到数据库
saveSkuList(goods);
}
/**
* 保存sku列表
* @param goods
*/
private void saveSkuList(Goods goods){
//获取spu对象
Spu spu = goods.getSpu();
//当前日期
Date date = new Date();
//获取品牌对象
Brand brand = brandMapper.selectByPrimaryKey(spu.getBrandId());
//获取分类对象
Category category = categoryMapper.selectByPrimaryKey(spu.getCategory3Id());
//获取sku集合对象
List<Sku> skuList = goods.getSkuList();
if (skuList != null) {
for (Sku sku : skuList) {
//设置sku主键ID
sku.setId(String.valueOf(idWorker.nextId()));
//设置sku规格
if (sku.getSpec() == null || "".equals(sku.getSpec())) {
sku.setSpec("{}");
}
//设置sku名称(商品名称 + 规格)
String name = spu.getName();
//将规格json字符串转换成Map
Map<String, String> specMap = JSON.parseObject(sku.getSpec(), Map.class);
if (specMap != null && specMap.size() > 0) {
for(String value : specMap.values()){
name += " "+ value;
}
}
sku.setName(name);//名称
sku.setSpuId(spu.getId());//设置spu的ID
sku.setCreateTime(date);//创建日期
sku.setUpdateTime(date);//修改日期
sku.setCategoryId(category.getId());//商品分类ID
sku.setCategoryName(category.getName());//商品分类名称
sku.setBrandName(brand.getName());//品牌名称
skuMapper.insertSelective(sku);//插入sku表数据
}
}
}
(3)修改SpuController的add方法
/***
* 新增数据
* @param goods
* @return
*/
@PostMapping
public Result add(@RequestBody Goods goods){
spuService.add(goods);
return new Result(true,StatusCode.OK,"添加成功");
}
2.3.2 品牌与分类关联
实现思路:
将分类ID与SPU的品牌ID 一起插入到tb_category_brand表中
(1)创建实体类
@Table(name="tb_category_brand")
public class CategoryBrand implements Serializable {
@Id
private Integer categoryId;
@Id
private Integer brandId;
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public Integer getBrandId() {
return brandId;
}
public void setBrandId(Integer brandId) {
this.brandId = brandId;
}
}
这个表是联合主键,所以templateId和brandId都有@Id注解
(2)新建数据访问接口
public interface CategoryBrandMapper extends Mapper<CategoryBrand> {
}
(3)SpuServiceImpl引入
@Autowired
private CategoryBrandMapper categoryBrandMapper;
(4)修改SpuServiceImpl的saveSkuList方法,添加分类与品牌之间的关联, 修改后代码如下:
private void saveSkuList(Goods goods) {
//获取spu对象
Spu spu = goods.getSpu();
//当前日期
Date date = new Date();
//获取品牌对象
Brand brand = brandMapper.selectByPrimaryKey(spu.getBrandId());
//获取分类对象
Category category = categoryMapper.selectByPrimaryKey(spu.getCategory3Id());
/**
* 添加分类与品牌之间的关联
*/
CategoryBrand categoryBrand = new CategoryBrand();
categoryBrand.setBrandId(spu.getBrandId());
categoryBrand.setCategoryId(spu.getCategory3Id());
int count = categoryBrandMapper.selectCount(categoryBrand);
//判断是否有这个品牌和分类的关系数据
if(count == 0) {
//如果没有关系数据则添加品牌和分类关系数据
categoryBrandMapper.insert(categoryBrand);
}
//获取sku集合对象
List<Sku> skuList = goods.getSkuList();
if (skuList != null) {
for (Sku sku : skuList) {
//设置sku主键ID
sku.setId(String.valueOf(idWorker.nextId()));
//设置sku规格
if (sku.getSpec() == null || "".equals(sku.getSpec())) {
sku.setSpec("{}");
}
//设置sku名称(商品名称 + 规格)
String name = spu.getName();
//将规格json字符串转换成Map
Map<String, String> specMap = JSON.parseObject(sku.getSpec(), Map.class);
if (specMap != null && specMap.size() > 0) {
for(String value : specMap.values()){
name += " "+ value;
}
}
sku.setName(name);//名称
sku.setSpuId(spu.getId());//设置spu的ID
sku.setCreateTime(date);//创建日期
sku.setUpdateTime(date);//修改日期
sku.setCategoryId(category.getId());//商品分类ID
sku.setCategoryName(category.getName());//商品分类名称
sku.setBrandName(brand.getName());//品牌名称
skuMapper.insertSelective(sku);//插入sku表数据
}
}
}
2.3.3 根据ID查询商品
需求:根据id 查询SPU和SKU列表 ,显示效果如下:
{
"spu": {
"id":"12312312312"
"name": "PRADA包包2021秋季限量版",
"caption": "PRADA最新包包",
"brandId": 103,
"category1Id": 903,
"category2Id": 945,
"category3Id": 946,
"freightId": 10,
"img": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"imgs": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"introduction": "商品介绍",
"paraItems": "{"出厂年份":"2020","赠品":"袋子"}",
"saleService": "七天包退,闪电退货",
"sn": "020102331",
"specItems": "{"颜色":["黑","蓝"],"二手程度":["崭新出厂","略有磨损","久经沙场","破损不堪","战痕累累"]}",
"templateId": 42
},
"skuList": [
{
"sn": "10192010293",
"num": 100,
"alertNum": 20,
"price": 10000,
"spec": "{"颜色":"黑","二手程度":"崭新出厂"}",
"img": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"imgs": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"status": "1",
"weight": 530
},
{
"sn": "10192010292",
"num": 100,
"alertNum": 20,
"price": 5000,
"spec": "{"颜色":"黑","二手程度":"战痕累累"}",
"img": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg",
"imgs": "https://img14.360buyimg.com/n1/jfs/t1/181065/5/3216/48663/6098c03fEad0ea4e5/659d59d79f8d0043.jpg,//img14.360buyimg.com/n1/jfs/t1/198898/26/12864/27744/61655645E38886d5d/a27cd7700b92b5ca.jpg,https://img14.360buyimg.com/n1/jfs/t1/191847/24/2416/44441/6098c03fEfe505a38/b00b22b4e091f1c3.jpg",
"status": "1",
"weight": 530
},
]
}
代码实现:
(1)ydles_service_goods工程SpuService新增方法定义
/**
* 根据ID查询商品
* @param id
* @return
*/
public Goods findGoodsById(String id);
(2)ydles_service_goods工程SpuServiceImpl实现此方法
/**
* 根据ID查询商品
* @param id
* @return
*/
public Goods findGoodsById(String id){
//查询spu
Spu spu = spuMapper.selectByPrimaryKey(id);
//查询SKU 列表
Example example=new Example(Sku.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("spuId",id);
List<Sku> skuList = skuMapper.selectByExample(example);
//封装,返回
Goods goods=new Goods();
goods.setSpu(spu);
goods.setSkuList(skuList);
return goods;
}
(3)修改SpuController的findById方法
@GetMapping("/{id}")
public Result findById(@PathVariable String id){
Goods goods = spuService.findGoodsById(id);
return new Result(true,StatusCode.OK,"查询成功",goods);
}
2.3.4 保存修改
(1)ydles_service_goods工程SpuService新增方法定义
/***
* 修改数据
* @param spu
*/
void update(Goods goods);
(2)ydles_service_goods工程SpuServiceImpl实现此方法
@Override
public void update(Goods goods ) {
//取出spu部分
Spu spu = goods.getSpu();
spuMapper.updateByPrimaryKey(spu);
//删除原sku列表
Example example=new Example(Sku.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("spuId",spu.getId());
skuMapper.deleteByExample(example);
saveSkuList(goods);//保存sku列表
}
(3)修改SpuController的update方法
/***
* 修改数据
* @param goods
* @param id
* @return
*/
@PutMapping(value="/{id}")
public Result update(@RequestBody Goods goods,@PathVariable String id){
spuService.update(goods);
return new Result(true,StatusCode.OK,"修改成功");
}
3 商品审核与上下架-日常工作
3.1 需求分析
商品指spu
1商品新增后,审核状态为0(未审核),默认为下架状态。
2审核商品,需要校验是否是被删除的商品,如果未删除则修改审核状态为1,并自动上架。
3下架商品,需要校验是否是被删除的商品,如果未删除则修改上架状态为0
4上架商品,需要审核状态为1,如果为1,则更改上下架状态为1
3.2 实现思路
(1)按照ID查询SPU信息
(2)判断修改审核、上架下架状态
(3)保存SPU
3.3 代码实现
3.3.1 商品审核
需求:校验是否是被删除的商品,如果未删除则修改审核状态为1,并自动上架
/audit/{id}
回顾,updateByPrimaryKey与updateByPrimaryKeySelective区别。
(1)SpuService新增方法
/**
* 审核
* @param id
*/
public void audit(String id);
(2)SpuServiceImpl实现方法
@Transactional
public void audit(String id) {
//查询spu对象
Spu spu = spuMapper.selectByPrimaryKey(id);
if (spu == null){
throw new RuntimeException("当前商品不存在");
}
//判断当前spu是否处于删除状态
if ("1".equals(spu.getIsDelete())){
throw new RuntimeException("当前商品处于删除状态");
}
//不处于删除状态,修改审核状态为1,上下架状态为1
spu.setStatus("1");
spu.setIsMarketable("1");
//执行修改操作
spuMapper.updateByPrimaryKeySelective(spu);
}
(2)SpuController新增方法
/**
* 审核
* @param id
* @return
*/
@PutMapping("/audit/{id}")
public Result audit(@PathVariable String id){
spuService.audit(id);
return new Result();
}
测试
3.3.2 下架商品
需求:校验是否是被删除的商品,如果未删除则修改上架状态为0
/pull/{id}
(1)SpuService新增方法
/**
* 下架商品
* @param id
*/
public void pull(String id);
(2)SpuServiceImpl实现方法
@Transactional
public void pull(String id) {
//查询spu
Spu spu = spuMapper.selectByPrimaryKey(id);
if (spu == null){
throw new RuntimeException("当前商品不存在");
}
//判断当前商品是否处于删除状态
if ("1".equals(spu.getIsDelete())){
throw new RuntimeException("当前商品处于删除状态");
}
//商品处于未删除状态的话,则修改上下架状态为已下架(0)
spu.setIsMarketable("0");
spuMapper.updateByPrimaryKeySelective(spu);
}
(2)SpuController新增方法
/**
* 下架
* @param id
* @return
*/
@PutMapping("/pull/{id}")
public Result pull(@PathVariable String id){
spuService.pull(id);
return new Result();
}
测试
3.3.3 上架商品
需求:必须是通过审核的商品才能上架
/put/{id}
(1)SpuService新增方法
/**
* 上架商品
* @param id
*/
public void put(String id);
(2)SpuServiceImpl 实现此方法
/**
* 上架商品
* @param id
*/
@Override
public void put(String id) {
Spu spu = spuMapper.selectByPrimaryKey(id);
if(!spu.getStatus().equals("1")){
throw new RuntimeException("未通过审核的商品不能上架!");
}
spu.setIsMarketable("1");//上架状态
spuMapper.updateByPrimaryKeySelective(spu);
}
(3)SpuController新增方法
/**
* 上架
* @param id
* @return
*/
@PutMapping("/put/{id}")
public Result put(@PathVariable String id){
spuService.put(id);
return new Result();
}
测试
4 删除与还原商品-日常工作
4.1 需求分析
商品列表中的删除商品功能,并非真正的删除(物理删除),而是采用逻辑删除将删除标记的字段设置为1.
在回收站中有还原商品的功能,将删除标记的字段设置为0
在回收站中有删除商品的功能,是真正的物理删除,将数据从数据库中删除掉。
4.2 实现思路
商品列表中的删除商品,执行逻辑删除,修改spu表is_delete字段为1
商品回收站中的还原商品,修改spu表is_delete字段为0
商品回收站中的删除商品,执行delete操作,进行物理删除
4.3 代码实现
4.3.1 逻辑删除商品
需求:商品列表中的删除商品,执行逻辑删除,判断其已下架,修改spu表is_delete字段为1。
SpuController
@DeleteMapping(value = "/{id}")
public Result delete(@PathVariable String id) {
spuService.delete(id);
return new Result(true, StatusCode.OK, "删除成功");}
SpuService
void delete(String id);
SpuServiceImpl
@Override
@Transactional
public void delete(String id) {
//逻辑删除
//查询spu
Spu spu = spuMapper.selectByPrimaryKey(id);
//判断是否处于下架
if(!spu.getIsMarketable().equals("0")){
throw new RuntimeException("下架状态才能删除商品");
}
//是下架状态 经行软删除
spu.setIsDelete("1");
spu.setStatus("0");
//存入数据库
spuMapper.updateByPrimaryKeySelective(spu);
}
测试
4.3.2 还原被删除的商品
回收站显示数据:select * from spu where is_delete=1
需求:恢复已删除状态的商品,设置其已删除属性为0,未审核。
/restore/{id}
(1)SpuService新增方法
/**
* 恢复数据
* @param id
*/
public void restore(String id);
(2)SpuServiceImpl实现此方法
/**
* 恢复数据
* @param id
*/
@Override
public void restore(String id) {
Spu spu = spuMapper.selectByPrimaryKey(id);
//检查是否删除的商品
if(!spu.getIsDelete().equals("1")){
throw new RuntimeException("此商品未删除!");
}
spu.setIsDelete("0");//未删除
spu.setStatus("0");//未审核
spuMapper.updateByPrimaryKeySelective(spu);
}
(3)SpuController新增方法
/**
* 恢复数据
* @param id
* @return
*/
@PutMapping("/restore/{id}")
public Result restore(@PathVariable String id){
spuService.restore(id);
return new Result();
}
4.3.3 物理删除商品
需求:判断必须逻辑删除商品才能物理删除
(1)SpuService 新增方法
/**
* 物理删除
* @param id
*/
public void realDelete(String id);
(2)SpuServiceImpl 实现方法
@Override
public void realDelete(String id) {
Spu spu = spuMapper.selectByPrimaryKey(id);
//检查是否删除的商品
if(!spu.getIsDelete().equals("1")){
throw new RuntimeException("此商品未删除!");
}
spuMapper.deleteByPrimaryKey(id);
}
(3)SpuController新增方法
/**
* 物理删除
* @param id
* @return
*/
@DeleteMapping("/realDelete/{id}")
public Result realDelete(@PathVariable String id){
spuService.realDelete(id);
return new Result();
}
测试
总结:
1分布式id
为什么:同一个模块,分布式部署,需要不重复的id
怎么做:uuid redis 雪花算法
二奢 idworker
2crud
spu—->sku(spuId)
iphone 13——->8+128 5000,8+512 8000
增:goods(spu,skuList) 品牌分类关联
删: 逻辑 还原 物理删除
改:原来sku关联删除掉,新增
查:id
第5章 网站首页高可用 nginx+lua
角色:运维。linux简单的命令,vim基本操作。
学习目标
- 了解Lua语言的基本语法
- 使用nginx+Lua+redis实现广告缓存-重点
- 掌握nginx限流的基本使用方法
1 Lua介绍
1.1 lua是什么
Lua 是一个小巧的脚本语言。 html js python 弱类型语言
简单来说:
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
lua 语言具有以下特性
- 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
- 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
- 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
- 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。
应用场景
- 游戏开发
- 独立应用脚本
- Web 应用脚本
- 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
- 安全系统,如入侵检测系统
- redis中嵌套调用实现类似事务的功能。 redis 注入 lua脚本,redis增强。 incr(“id”)
- web容器中应用处理一些过滤 缓存等等的逻辑,例如nginx。
1.2 lua的安装
虚拟机中已安装。建议对照pdf自己安装。
有linux版本的安装也有mac版本的安装。。我们采用linux版本的安装,首先我们准备一个linux虚拟机。
安装步骤,在linux系统中执行下面的命令。
yum install -y gcc
yum install libtermcap-devel ncurses-devel libevent-devel readline-devel
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar -zxf lua-5.3.5.tar.gz
cd lua-5.3.5
make linux test
make install
1.3 快速入门
创建hello.lua文件,内容为
print("hello");
保存。执行命令
lua helloworld.lua
输出为:
Hello
1.4 LUA的基本语法
lua有交互式编程和脚本式编程。
交互式编程就是直接输入语法,就能执行。
脚本式编程需要编写脚本文件,然后再执行。
一般采用脚本式编程。(例如:编写一个hello.lua的文件,输入文件内容,并执行lua hell.lua即可)
拓展:编写lua脚本。回忆linux基本操作。vim基本操作。参见拓展资料。
linux:https://www.runoob.com/linux/linux-command-manual.html
vim:https://blog.csdn.net/weixin_37657720/article/details/80645991
1.4.1 注释
单行注释:两个减号是单行注释:
--
多行注释:
--[[
多行注释
多行注释
--]]
1.4.2 关键字
关键字就好比java中的 break if else等等一样的效果。lua的关键字如下:
and | break | do | else |
elseif | end | false | for |
function | if | in | local |
nil | not | or | repeat |
return | then | true | until |
while |
1.4.3 定义变量
全局变量,默认的情况下,定义一个变量都是全局变量,
如果要用局部变量 需要声明为local.例如:
-- 全局变量赋值
a=1
-- 局部变量赋值
local b=2
如果变量没有初始化:则 它的值为nil 这和java中的null不同。
1.4.4 Lua中的数据类型
Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
数据类型 | 描述 |
---|---|
nil | 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。 |
boolean | 包含两个值:false和true。 |
number | 表示双精度类型的实浮点数 |
string | 字符串由一对双引号或单引号来表示 |
function | 由 C 或 Lua 编写的函数 |
userdata | 表示任意存储在变量中的C数据结构 |
thread | 表示执行的独立线路,用于执行协同程序 |
table | Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。 |
1.4.5 流程控制
如下:类似于if else
--[ 0 为 true ]
if(0) then
print("0 为 true")
else
print("0 不为true")
end
执行:
lua hello.lua
结果:
0为true
1.4.6 函数
lua中也可以定义函数,类似于java中的方法。例如:
--[[ 函数返回两个值的最大值 --]]
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end
-- 调用函数
print("两值比较最大值为 ",max(10,4))
print("两值比较最大值为 ",max(5,6))
执行:
lua hello.lua
结果:
10
1.4.7 require 函数
require 用于 引入其他的模块,类似于java中的类要引用别的类的效果。
用法:
require "<模块名>"
2.nginx+lua+redis实现广告缓存-重点
面试:
1ngxin有什么作用?
1.1负载均衡
1.2反向代理
1.3静态网页服务器
2tomcat与nginx,网页服务器,他们的区别是什么呢?
网页服务器:tomcat与nginx。
1从应用方面:
tomcat一般是做动态解析才会用得到,支持jsp(html+java)的解析,需要配置JDK支持。
nginx,则一般是做静态html ,1负载均衡、2反向代理、3http服务器。
2性能方面 :
tomcat并发500
nginx 上万 tps
需要在页面上显示广告的信息。
2.2 OpenResty
2.2.1 OpenResty介绍
罗永浩 锤子发布会 今晚票钱 68w 捐给OpenResty 基金会
OpenResty(又称:ngx_openresty) 是一个基于 NGINX 的可伸缩的 Web 平台,由中国人章亦春发起,提供了很多高质量的第三方模块。
OpenResty 是一个强大的 Web 应用服务器,Web 开发人员可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,更主要的是在性能方面,OpenResty可以 快速构造出足以胜任 10K 乃至1000K以上并发连接响应的超高性能 Web 应用系统。
360,UPYUN,阿里云,新浪,腾讯网,去哪儿网,酷狗音乐等都是 OpenResty 的深度用户。
OpenResty 简单理解,就相当于封装了nginx,并且集成了LUA脚本,开发人员只需要简单的其提供了模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
2.2.2 OpenResty安装
虚拟机中已安装。
linux安装openresty:
1.添加仓库执行命令
yum install yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
2.执行安装
yum install openresty
3.安装成功后 会在默认的目录如下:
/usr/local/openresty
2.2.3 安装nginx
默认已经安装好了nginx,在目录:/usr/local/openresty/nginx 下。
修改/usr/local/openresty/nginx/conf/nginx.conf ,将配置文件使用的根设置为root,目的就是将来要使用lua脚本的时候 ,直接可以加载在root下的lua脚本。
#user nobody; 配置文件第一行原来为这样, 现改为下面的配置
user root root;
2.3 实现思路
2.3.1 表结构分析
banner
查看数据库:ydles_business库下tb_ad表,分析表结构。
广告位置:不同位置价位不同。查看jd.com
tb_ad (广告表)
字段名称 | 字段含义 | 字段类型 | 字段长度 | 备注 |
---|---|---|---|---|
id | ID | INT | ||
name | 广告名称 | VARCHAR | ||
position | 广告位置 | VARCHAR | 系统定义 | |
start_time | 开始时间 | DATETIME | ||
end_time | 到期时间 | DATETIME | ||
status | 状态 | CHAR | 0:无效 1:有效 | |
image | 图片地址 | VARCHAR | ||
url | URL | VARCHAR | ||
remarks | 备注 | VARCHAR | ||
web_index_lb | 首页轮播图 | |||
web_index_amusing | 有趣区 | |||
web_index_ea_lb | 家用电器楼层轮播图 | |||
web_index_ea | 家用电器楼层广告 | |||
web_index_mobile_lb | 手机通讯楼层轮播图 | |||
web_index_mobile | 手机通讯楼层广告 |
2.3.2 缓存预热与二级缓存查询
缓存预热与二-级缓存查询
1缓存预热:编写lua脚本实现缓存预热(将mysql里的数据查询出来存入redis)
2一级缓存查询:
3 二级缓存查询
2.4 代码实现
2.4.1 缓存预热
运维人员,脚本,每天凌晨2点,nginx发一个请求
( http://192.168.200.128/ad_update?position=web_index_lb),让nginx来做的。
实现思路:
定义请求:用于查询数据库中的数据更新到redis中。
(1)连接mysql ,按照广告分类ID读取广告列表,转换为json字符串。
(2)连接redis,将广告列表json字符串存入redis 。
定义请求:
请求:
/ad_update
参数:
position --指定广告位置
返回值:
json
1编写/root/lua/ad_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"]
local db = mysql:new()
db:set_timeout(1000)
local props = {
host = "192.168.200.128",
port = 3306,
database = "ydles_business",
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()
local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
local ip ="192.168.200.128"
local port = 6379
red:connect(ip,port)
red:set("ad_"..position,cjson.encode(res))
red:close()
ngx.say("{\"flag\":true,\"position\":\""..position.."\"}")
redis key:ad_web_index_lb value:”{url:http://asdfasdf,image:asdfasdf}”
2 使nginx执行lua脚本
vim /usr/local/openresty/nginx/conf/nginx.conf
在server 80 localhost中添加转发
#添加广告
location /ad_update {
content_by_lua_file /root/lua/ad_update.lua;
}
测试:重启nginx
cd /usr/local/openresty/nginx/sbin
./nginx -s reload
访问:运维 页面 http://192.168.200.128/ad_update?position=web_index_lb
查看redis中有没有数据。
2.4.2 广告缓存读取
用户访问首页,钩子方法里,请求(http://192.168.200.128/ad_read?position=web_index_lb),显示在页面上。
实现思路:
通过lua脚本直接从redis中获取数据即可。
定义请求:
请求:/ad_read
参数:position
返回值:json
创建文件 vim /root/lua/ad_read.lua。看懂即可。
ngx.header.content_type="application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(2000)
local ok, err = red:connect("192.168.200.128", 6379)
local rescontent=red:get("ad_"..position)
ngx.say(rescontent)
red:close()
修改:nginx读取lua脚本
vim /usr/local/openresty/nginx/conf/nginx.conf
在server 80 localhost中添加转发
#读取广告
location /ad_read {
content_by_lua_file /root/lua/ad_read.lua;
}
重启nginx
cd /usr/local/openresty/nginx/sbin
./nginx -s reload
测试访问: 用户登录首页 http://192.168.200.128/ad_read?position=web_index_lb
2.4.3 二级缓存-加入openresty本地缓存
问题:如上的方式没有问题,但是如果请求都到redis,redis压力也很大,所以我们一般采用多 级缓存的方式来减少下游系统的服务压力。
解决方案:先查询openresty本地缓存 如果没有再查询redis中的数据
操作: 看懂内容即可。
- 修改/root/lua目录下ad_read文件, 内容如下:
--设置响应头类型
ngx.header.content_type="application/json;charset=utf8"
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
--获取本地缓存
local cache_ngx = ngx.shared.dis_cache;
--根据ID 获取本地缓存数据
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.200.128", 6379)
--获取key的值
local rescontent=red:get("ad_"..position)
--输出到返回响应中
ngx.say(rescontent)
--关闭连接
red:close()
--将redis中获取到的数据存入nginx本地缓存
cache_ngx:set('ad_cache_'..position, rescontent, 10*60);
else
--nginx本地缓存中获取到数据直接输出
ngx.say(adCache)
end
- 修改nginx配置文件vi /usr/local/openresty/nginx/conf/nginx.conf ,http节点下添加配置:
#包含redis初始化模块
lua_shared_dict dis_cache 5m; #共享内存开启
2.4.4 前端页面实现(了解)
(1)修改index.html,编写脚本
<script>
new Vue({
el:"#app",
data:{
ad:{
web_index_lb:[]
}
},
methods:{
adRead:function(position){
axios.get('ad_read?position='+position).then(response=>{
this.ad[position]=response.data
})
}
},
created(){
this.adRead('web_index_lb')
this.adRead('web_index_lb')
}
})
</script>
在页面上添加...
(2)修改index.html,渲染广告轮播图
<div id="myCarousel" data-ride="carousel" data-interval="4000" class="sui-carousel slide">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active" v-for="item in ad.web_index_lb"></li>
</ol>
<div class="carousel-inner" id="lbt">
<div class="item" v-for="item in contentList">
<a :href="item.url">
<img :src="item.pic" />
</a>
</div>
</div>
<a href="#myCarousel" data-slide="prev" class="carousel-control left">‹</a>
<a href="#myCarousel" data-slide="next" class="carousel-control right">›</a>
</div>
(3)上传至服务器并测试
更改
# 加载首页
location / {
root html;
index index.html index.htm;
}
域名访问:C:\Windows\System32\drivers\etc\hosts
添加 192.168.200.128 www.ydles.com
3 nginx限流
回顾网关限流:令牌桶。
一般情况下,首页的并发量是比较大的,即使有了多级缓存,如果有大量恶意的请求,也会对系统造成影响。而限流就是保护措施之一。
nginx提供两种限流的方式:
- 一是控制速率
- 二是控制并发连接数
3.1 控制速率
控制速率的方式之一就是采用漏桶算法。
3.1.1 漏桶算法实现控制速率限流
1漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
2与令牌桶区别:
令牌桶控制进的速度
漏桶算法控制出的速度
漏桶算法实现 nginx的配置
3配置示意图如下:
看懂即可,注意位置。
修改/usr/local/openresty/nginx/conf/nginx.conf:
# 设置限流配置 限速,每秒2个,内存区域10m,大概16万个session
limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=2r/s;
server {
listen 8081;
server_name localhost;
charset utf-8;
location / {
limit_req zone=myRateLimit;
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 方便测试。
测试:重新加载配置文件
cd /usr/local/openresty/nginx/sbin
./nginx -s reload
快速刷新: http://192.168.200.128:8081/
http 503: 由于临时的服务器维护或者过载,服务器当前无法处理请求。
3.1.2 处理突发流量
上面例子限制 2r/s,如果有时正常流量突然增大,超出的请求将被拒绝,无法处理突发流量,可以结合 burst 参数使用来解决该问题。
例如,如下配置表示:
limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=2r/s;
server {
listen 8081;
server_name localhost;
charset utf-8;
location / {
limit_req zone=myRateLimit burst=5 nodelay;
root html;
index index.html index.htm;
}
}
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 一起使用。
例如:如下配置:
server {
location / {
limit_req zone=myRateLimit burst=5 nodelay;
root html;
index index.html index.htm;
}
}
如上表示:
处理突发5个请求的时候,没有延迟,等到完成之后,按照正常的速率处理。
如上两种配置结合就达到了速率稳定,但突然流量也能正常处理的效果。配置代码如下:
#user nobody;
user root root;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# 设置限流配置
limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=2r/s;
server {
listen 8081;
server_name localhost;
charset utf-8;
location / {
limit_req zone=myRateLimit burst = 5 nodelay;
root html;
index index.html index.htm;
}
}
}
测试:如下图 在1秒钟之内可以刷新5次,正常处理。
但是超过之后,连续刷新5次,抛出异常。
总结: 运维
1lua脚本
js lua python go
2重点——-首页广告缓存
3nginx限流
漏桶算法:漏洞
burst=5 nodelay