系统描述

有12个微服务
eureka——注册中心,服务发现
抢单时,请求首先被发送到nginx(抗并发能力很强),使用lua脚本实现抢单,性能远超其他应用服务抢单。

day1

了解电商:B2C(商家直接面向用户) 淘宝是B2B2C

了解畅购架构:微服务架构,common封装了一些公共组件和工具类

商品微服务搭建,增删改查

day2

  • 理解FastDFS工作流程

    1. 分布式文件管理系统
    2. 文件上传
    3. 文件下载
    4. 文件删除
    5. 文件缓存控制
  • 搭建文件上传微服务

  • 相册管理(实战)
  • 规格参数管理(实战)
  • 商品分类管理(实战)

FastDFS:
开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。适合以文件为载体得在线服务
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过Tracker server 调度最终由 Storage(仓库) server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。

上传流程如下图:
image.png

项目中哪里用到了FastDFS
后台上传商品信息时,商品图片上传用得FastDFS

现在虚拟机上用docker把FastDFS安装好,然后在项目得application yml文件中把tracker和storage都配置好,配置FastDFS依赖,有现成得对象可以调用

day3

  • SPU与SKU概念理解

    1. SPU:某一款商品的公共属性
    2. SKU:某款商品的不同参数对应的商品信息[某个商品]
  • 删除商品

    1. 逻辑删除:修改了删除状态
    2. 物理删除:真实删除了数据
  • 找回商品

    1. 找回商品:一定是属于逻辑删除的商品

    day4 lua、Canal实现广告缓存

    首页门户系统需要展示各种各样的广告数据。
    这些广告数据为什么不能用mysql去查?原因在于:
    一般来说,首页的访问流量都是比较高的,这种高流量的访问不适合直接去查mysql,那样会给mysql带来比较大的压力,有两种方案

  1. 把数据做成静态页
  2. 用缓存

在我们的方案中使用缓存来展示首页广告,使用2级缓存

nginx+redis两级缓存实现广告读取

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

使用lua编写脚本,在nginx生成本地缓存。nginx方面具体使用的openresty
OpenResty(又称:ngx_openresty) 是一个基于 nginx的 Web 平台。OpenResty 简单理解成 就相当于封装了nginx,并且集成了LUA脚本,开发人员只需要简单的其提供了模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。

总体实现逻辑,请求时先找openresty缓存能不能命中,再查redis缓存能不能命中(如果redis中了,openresty没中,就把查询的内容同步到openresty缓存中),都不命中再去mysql查,查到后把广告内容同步到redis中。
更新时先来凝结mysql数据库查询,查到后同步到redis中

nginx中可以修改config文件,在某一路径下直接调用lua脚本处理请求。

通过编写update_content.lua,实现连接mysql查询,并将查询数据存储到redis中
通过编写read_content.lua,实现openresty、redis两级缓存查询。

nginx限流

nginx提供两种限流的方式:

  • 一是控制速率(某一个请求,一秒请求多少次)
    • 漏桶算法:请求进入漏桶,从漏桶出口以一定速度出水,水流过大时会直接溢出(返回503)
    • 在nginx配置中可以通过burst参数桶的大小。
      • burst 译为突发、爆发,表示在超过设定的处理速率后能额外处理的请求数,当 rate=10r/s 时,将1s拆成10份,即每100ms可处理1个请求。此处,burst=4 ,若同时有4个请求到达,Nginx 会处理第一个请求,剩余3个请求将放入队列,然后每隔100ms从队列中获取一个请求进行处理。若请求数大于4,将拒绝处理多余的请求,直接返回503.
  • 二是控制并发连接数(限制某一用户的ip连接数量控制流量)

canal同步广告(问到mysql往binlog转,顺便说canal)

canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。
canal原理:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)
  3. canal解析binary log对象(原始为byte流),以动态监控mysql数据库的变化

redis缓存击穿问题,可以使用多重缓存来解决,其中就可以使用canal来做信息同步

day5、day6 商品搜索es

Elasticsearch功能:

  1. 商品搜索页实现关键字搜索
  2. 海量商品数据的品牌统计(页面最上方)
  3. 海量商品规格统计信息(页面最上方)
  4. 多条件搜索(品牌、规格、价格等条件筛选)
  5. 搜索排序

IK分词器作用:实现分词搜索内容分词

kibana:
Elasticsearch默认分词,将所有内容都拆开
例如:
elasticsearch默认的分词:http://localhost:9200/userinfo/_analyze?analyzer=standard&pretty=true&text=我是中国人 (或者不写analyzer=standard)
分词之后是:“我”“是”“中”“国”“人“,会将每一个词都拆开。
加上ik分词器之后会根据需求进行智能化拆分
例如:
使用ik对中文分词 http://localhost:9200/userinfo/_analyze?analyzer=ik&pretty=true&text=我是中国人
分词之后是:“我”“中国人”“中国”“国人”

数据导入es:使用Spring Data ElasticSearch,实现项目与ElasticSearch的交互

1557563491839.png
实现过程:

  1. 创建一个javabean,在javabean(skuinfo)中添加索引库映射配置(可以配置analyzer,type等)
  2. 创建feign,实现查询所有的sku集合
  3. 在搜索微服务中调用feign,查询所有的sku集合,并将sku集合转换为skuinfo集合
  4. 调用dao(继承ElasticsearchRepository),实现数据导入到ES中

关键词搜索:使用es搜索时和mybatis-plus差不多,都是先创建分组条件对象,再通过分组条件对象实现条件查询

day7 Thymeleaf

当系统审核完成商品,需要将商品详情页进行展示,那么采用静态页面生成的方式生成。
生成的静态页都防止在nginx的指定目录下

Thymeleaf的介绍:模板引擎,可用于Web与非Web环境中的应用开发。弄一个界面出来,后端查出来的数据往页面上填写,通过feign接口调用用search微服务用es查找数据,再将查找到的数据添加再

Thymeleaf在本项目中的作用:

  1. 搜索结果静态页展示
    1. 搜索数据填充
    2. 搜索数据展现和搜索条件回显(search微服务将搜索条件和搜索数据都打包成map的格式),Thymeleaf使用each循环展示
  2. 商品详情页展示

条件搜索实现:用户每次点击搜索的时候,其实在上次搜索的基础之上加上了新的搜索条件,也就是在上一次请求的URL后面追加了新的搜索条件,我们可以在后台每次拼接组装出上次搜索的URL,然后每次将URL存入到Model中,页面每次点击不同条件的时候,从Model中取出上次请求的URL,然后再加上新点击的条件参数实现跳转即可。

商品详情页使用静态页,需要canal始终监听goods数据库,goods有变化了需要重新生成静态页

day8 微服务网关

微服务网关是什么:
不同微服务一般有不同的网络地址,而外部客户端可能需要调用多个服务接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,可能会有以下问题:

  1. 客户端多次请求不同的微服务,增加了客户端的复杂性
  2. 存在跨域请求,一定场景下处理相对复杂
  3. 认证比较复杂,每个服务都需要独立认证
  4. 难以重构,如果每个微服务都面向客户端,如果以后有将一个服务拆分成几个服务或者将几个服务合成一个服务的需求的时候,重构将会比较困难
  5. 某些微服务如果用了防火墙/浏览器不友好的协议,直接访问会有困难。

微服务网关的优点:

  1. 安全,只有网关系统对外暴露,微服务隐藏在内网,通过防火墙保护
  2. 易于监控,可以在网关收集监控数据并将其推送到外部系统进行分析
  3. 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  4. 减少了客户端与各个微服务之间的交互次数
  5. 易于统一授权。

微服务网关使用技术:springboot-gateway

路由过滤:
比如用户请求www.alibaba.com:8001/api/goods 只要以这个开头的,全部路由到goods微服务去
同时也可以配置负载均衡,比如lb://goods 其中goods是微服务在erueka里面的别名

项目中微服务网关都做了什么:

  1. 路由过滤,做到对外只暴露网关微服务一个端口号
  2. 网关限流:因为所有请求都要先通过网关才能到微服务中,所以在网关限流就可以实现在微服务限流
    1. 项目中共有两层限流
      1. nginx网关,主要是针对大量请求的第一层抵御,有限流,但是限制的流量属于总流量比较大,这个经过限流的总流量如果都冲击到某个微服务还是比较大的。
      2. 第二层网关是用springcloud gateway实现的,是对于各个微服务请求的限流,这回限制的流量比较小,用来保护微服务,防止雪崩效应
    2. 限流算法
      1. 令牌桶算法:所有请求处理之前都需要拿到一个可用的令牌才能被处理,用令牌桶添加令牌的速度来限流 项目中做的是将令牌生成后存放到redis中,请求需要先获得redis的令牌才行(通过添加依赖,有固定的实现方式)
  3. 权限校验:

    1. 使用JWT来进行校验
      1. JWT用于微服务之间传递用户信息的一段加密字符串,该字符串是一个json格式,各个微服务可以根据该json字符串识别用户的身份信息。(JWT由头部、载荷与签名组成)
        1. 头部:关于jwt的最基本信息,类型和加密算法(签名算法)等
        2. 载荷:存放有效信息的地方,包括三种信息
          1. 标准中注册的声明
          2. 公共的声明
          3. 私有的声明
        3. 签证:用来验证数据有没有被篡改
          1. header中规定的加密算法(based64(header) + based64(payload) + 放置在服务器的密钥)
      2. jwt保存的内容经过based64反编译就可以得到了,所以不能放敏感信息,但说jwt可靠就是因为它的签名机制能保证服务器可以识别出来这个jwt包含的信息有没有被修改过
    2. 鉴权是怎么做的
      1. 用户想要完成功能要访问微服务,这是要携带头文件信息
      2. 用户访问先到网关,网关查看用户访问路径看用不用拦截
        1. 需要拦截,查看url + header中的(参数信息、头信息、cookie信息),看看有没有jwt
          1. 有jwt,判断jwt是否被篡改过
            1. 被篡改过不放行
            2. 没有篡改过,放行
          2. 没有jwt,判断用户还没登录,跳转到登录界面
            1. 判断用户登录成功,签发jwt,后续放在cookie或者请求头当中
            2. 用户登录失败,提醒账号或者密码错误

              day9、day10 Oauth2

              常用授权方式:
  4. 授权码模式

    1. 申请授权码
    2. 用授权码授权令牌 申请令牌时需要http basic认证(Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码) 用户名是clientid
  5. 密码模式
    1. 使用用户名密码即可获得认证

基于oauth2的资源服务授权流程——基于公钥私钥改善
1562479275302.png
传统方式客户端在访问资源服务器比较麻烦,性能低下:

  1. 客户端向授权服务器申请令牌
  2. 客户端授权成功收到令牌
  3. 客户端携带令牌访问资源服务器
  4. 资源服务器找授权服务器验证令牌合法性
  5. 授权服务器返回校验结果
    1. 校验成功资源服务器返回资源
    2. 校验失败资源服务器拒绝服务

1562406193809.png
使用公钥私钥进行改善
授权服务器保存私钥,私钥生成令牌
资源服务器保存公钥,公钥可以校验令牌是否合法

总结:
用户首先访问网关微服务

  • 网关微服务查看用户请求有没有携带认证微服务生成的令牌(检查cookie、header、请求体)
    • 用户携带令牌了,放行,将令牌放到header中
      • 用户请求来到资源微服务进行令牌认证
        • 公钥认证令牌通过,可以访问
          • 公钥认证令牌时,使用公钥验证签名是否正确
        • 公钥认证令牌不通过,不允许访问
    • 用户没有携带令牌,不放行,前端提醒用户去登录
      • 用户到oauth认证微服务登录,用户名密码登录
        • 登录成功,生成由私钥加密的token令牌,放到cookie里面
        • 登录失败,不放行

公钥私钥非对称加密使用rsa算法
rsa算法安全性依赖于大数分解,计算两个大素数得乘积很容易,但反过来由该成积分解成两个素数相乘非常难。
ps:一些小贴士

  1. oauth2是如何认证用户的,通过UserDetailsServiceImpl 继承了UserDetailsService并重写了loadUserByName实现登录的用户名密码验证,这里远程调用了userFeign,调取了user数据库中的信息
  2. 用户权限怎么开启和设置(用户微服务有个ResourceServerConfig配置类,上面有EnableGlobalMethodSecurity注解,开启后就打开了用户权限,可以通过注解设置每个方法允许的权限)详情请见第九天的md文档
    1. 资源服务器配置好资源授权配置(ResourceServerConfig),里面有如下配置以实现利用公钥解密得到解密后的jwt
      1. tokenStore()返回解密后的jwt
      2. jwtAccessTokenConverter()获取公钥和经过私钥加密的jwt进行解密
      3. getPubKey()读取公钥
      4. configure配置认证拦截,认证不通过可以访问哪些路径

这样用户只有携带正确的令牌(oauth生成的令牌)才能访问资源了

  1. 网关微服务如何做到检查令牌和将令牌放置到header里面的?请求到时候会先到网关微服务,所以网关微服务配置一个全局过滤器(AuthorizeFilter),作用是得到oauth2传过来的token令牌,再将其封装到header里面
  2. oauth2登录流程,oauth2是为了获取token的(类比buc)

使用账号密码登录方式,要点

  1. clientId和clientSecret放在resource里面读取 (这个clientid 和clientsecret要在oauth2的数据库里面有,这个数据表结构是固定的不能自己更改,你知道这个id和secret就说明你是被认证的客户端)
  2. 使用username password (由UserDetailsServiceImpl 配置后,连接了user表) clientid和clientsecret
  3. 使用restTemplate对象模拟前端发送请求获取token

day11订单结算

用户登录后的购物车,用户将商品加入购物车的时候,直接将要加入购物车的详情存入到Redis即可。每次查看购物车的时候直接从Redis中获取。
redis中使用hashmap形式保存购物车信息(key:username vlaue{ key:spuId,value: orderItemBean})
为什么要用redis存储购物车数据:因为购物车数据操作相对商品等信息更频繁,属于当前情况下的热点数据

用户下单流程:
用户从购物车点击结算,跳转到结算页

  1. 结算页加载用户对应的收件地址,购物车列表
  2. 用户点击结算的时候会立刻创建订单,更新订单表和订单明细表
    1. 解决异价问题(生成订单时查询当前价格)
    2. 解决并发库存不够问题(数据库操作行锁)
  3. 调用商品微服务,减少商品库存(注意要判断商品库存是否足够的问题)
  4. 用户支付(项目中使用微信扫码支付,每次生成的码都不一样)
    1. 生成订单后,调用微信支付api
    2. 获取微信支付返回的交易链接
    3. 根据交易链接生成二维码图片
    4. 将二维码展示给用户
    5. 用户扫码提交扫描链接
    6. 微信服务器验证扫描有效性
    7. 验证扫描有效返回支付授权
    8. 用户确认支付,输入密码
    9. 验证密码正确,完成交易
      1. 提示用户交易完成
      2. 通知我们这个服务支付结果(如果没有收到畅购服务的回应,就多发几次)
    10. 畅购也可以主动找微信服务器查询支付结果
    11. 确认结果后该发货该退单自己选1558490059984.png

用户下单后生成订单操作:

  1. 读取redis中该用户对应的购物车信息
  2. 循环查询当前购物车中的商品,构建订单和订单详情信息(该步骤中远程调用goods微服务获取最新的商品价格)
  3. 用户增加积分
  4. 删除redis中的该用户对应的购物车信息
  5. 发送信息给延时队列,消息是订单号,到过期时间还没支付就要回滚库存,删除订单

延时队列怎么实现的:
创建两个队列,其中一个队列设置过期时间,其中的消息一旦存活时间超过这个过期时间就变成死信,死信会自动转发到另一个队列中。
订单微服务始终监听死信队列,并根据队列里面的信息找到对应的订单查看订单支付状态,如果状态是未支付,就回滚库存。

image-20220211190657162.png
支付结果处理流程:

  1. 微信支付微服务在获取到支付结果后就将该结果发送到rabbitmq上(以json字符串形式)
  2. 订单微服务时中监听支付微服务发送消息的rabbitmq,根据监听到的数据修改订单状态(该逻辑在OrderMessageListener中)@RabbitListener注解可以绑定rabbitmq队列
    1. 如果支付成功就把支付流水号更新到订单中(并设置状态为已支付),
    2. 否则就删除订单,回滚库存

13、14day 秒杀流程

  1. 首先进行秒杀商品的入库审核,审核通过存入mysql
  2. 秒杀服务定时将秒杀商品转入redis(定时载入因为中途可能有商品上架,所以不能一次性转入)
  • 使用@Scheduled注解(cron = “0/5 ?”) 实现反复执行,里面cron表达式表示从第0秒开始,每5秒执行一次,将数据从mysql里面查询出来存入redis缓存当中
    • 存入的商品,符合以下几个条件
      • 判断审核状态为通过,
      • 参与秒杀时间在计算出的秒杀时间段里面
      • 原来不在redis中
      • 库存个数>0
  • 有5个时间段,只载入参与秒杀时间在这5个时间段的数据,载入的形式为hash形式,命名空间为参与秒杀开始时间,key为秒杀商品id,value为秒杀商品信息
  1. 客户开始抢购,用户的请求经过openResty(Nginx)到redis中获取秒杀商品(到达秒杀列表页)
  2. 用户如果想抢某个商品,也需要经过openResty到达秒杀详情页(从redis获取数据生成秒杀商品详情页)
  3. 用户进行秒杀抢购,但抢购之前需要先登录,登陆前请求还需要先到达openresty(判断当前秒杀商品还有没有库存,如果没有库存就直接拦截了,也不用调用后面的微服务了减轻压力)
  4. openResty通过之后判断用户是否登录(没登录就先去oauth2.0进行登录)
  5. 登录成功访问秒杀微服务,但还不能直接抢单(因为抢单要检查的内容很多,很费时,高并发的时候就都堵在那里了)。
  6. 用户排队抢单(记录用户id和商品id,将这些内容存储在redis中),提示用户当前正在排队,用户可以调用状态查询服务获取订单状态

  7. 排队用户进行多线程抢单
    • 获取排队信息
    • 多线程下单(检查下单时需要校验的内容),库存递减
    • 订单信息存储在redis中(为了提升下单速度)

    • 下单后更新更新排队状态,提示用户当前抢单成功进行支付
  8. 抢单成功用户进入支付微服务,如果支付成功,会删除订单信息以及排队抢单信息(并同步mysql中)
    • 抢单成功后同时发送延时队列(msg:订单id、用户id、秒杀商品id)以处理未支付订单
    • 监听服务监听延时队列,到时间后会监听到信息,利用该信息到redis中查找订单,如果没有就是已经支付好了,如果还有就是未支付
    • 未支付订单要关闭订单号对应的微信支付,库存回滚,删除抢单排队信息

下单需要检查的内容

  • 账号是否异常
  • 24小时内是否购买过该商品
  • 是否存在未支付的秒杀订单
  • 该秒杀商品是否还有库存
  • 秒杀商品抢购人数是否到达上限
  • 该用户是否恶意抢单(抢到不付款或者抢了就退)

秒杀一系列操作:

  1. 录入秒杀商品(应该有秒杀开始时间和秒杀结束时间字段)
  2. 秒杀频道列出秒杀商品(按照时段区分)
  3. 点击立即抢购实现秒杀下单,下单时扣减库存,库存为0或者不在活动期范围无法秒杀

秒杀核心思想:运用缓存减少数据库瞬间访问压力,当用户点击抢购时减少缓存中的库存数量,库存数为0或活动期结束时,同步到数据库。 产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,用户付款成功后再写入数据库。