From 99e65afe564145c3f9d357f5cbcb9191ca61226e Mon Sep 17 00:00:00 2001 From: youthlql <1826692270@qq.com> Date: Thu, 21 Apr 2022 21:01:33 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85suibi=EF=BC=8C1=E5=B9=B4?= =?UTF-8?q?=E5=89=8D=E6=A0=A1=E6=8B=9B=E7=9A=84=E4=B8=80=E4=BA=9B=E8=80=81?= =?UTF-8?q?=E5=86=85=E5=AE=B9=EF=BC=8C=E6=9C=89=E5=85=B4=E8=B6=A3=E7=9A=84?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=9C=8B=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/suibi/我的校招-不完全知识点整理.md | 1832 ++++++++++++++++++++++- 1 file changed, 1831 insertions(+), 1 deletion(-) diff --git a/docs/suibi/我的校招-不完全知识点整理.md b/docs/suibi/我的校招-不完全知识点整理.md index 9bda003..d983bdb 100644 --- a/docs/suibi/我的校招-不完全知识点整理.md +++ b/docs/suibi/我的校招-不完全知识点整理.md @@ -7,7 +7,7 @@ categories: - 随笔 keywords: 校招,面试 description: 如题,一部分的整理【三万字】,具体的看文章 -cover: 'https://npm.elemecdn.com/lql_static@latest/bg/00178.webp' +cover: 'https://npm.elemecdn.com/lql_static@latest/bg/00009.webp' abbrlink: 5df2d017 date: 2021-04-22 14:21:58 --- @@ -2539,6 +2539,1836 @@ https://blog.csdn.net/j04110414/article/details/78914787 +# 电商项目面试点 + +## 说明 + +> 2019年5月开始做的电商,糅合了2020的几家电商项目,总结一下面试点。 +> +> 1. 关于redis,rocketmq的知识点。还需要看过专门的知识才能整理的比较全 + +**参考:** + +https://blog.csdn.net/qq_41618510/article/details/83280653 + +https://blog.51cto.com/13517854/2073947 + + +## 1.架构图 + +![架构图](https://npm.elemecdn.com/youthlql@1.0.12/image/001.png) + + + +## 2.OMS,PMS,UMS,WMS什么意思 + +OMS:订单系统,也就是订单相关的表 + +PMS:商品数据结构 + +UMS:用户数据结构 + +WMS:仓库,库存数据结构 + + + +## 3.SKU和SPU相关 + +> 参考:2019版-04 谷粒商品pms.docx + +### 1.什么是SPU、SKU? + +1. 简单的说: SPU就是一个iPhone6s, SKU就是银色iPhone6s、粉色iPhone6s + +2. 通俗点讲,属性值、特性相同的商品就可以称为一个SPU + +3. SKU是物理上不可分割的最小存货单元 + + + +### 2.为什么将SKU抽取出来? + +比如,咱们购买一台iPhoneX手机,iPhoneX手机就是一个SPU,但是你购买的时候,不可能是以iPhoneX手机为单位买的,商家也不可能以iPhoneX为单位记录库存。必须要以什么颜色什么版本的iPhoneX为单位。比如,你购买的是一台银色、128G内存的、支持联通网络的iPhoneX ,商家也会以这个单位来记录库存数。那这个更细致的单位就叫库存单元(SKU)。 + + + +### 3.SPU解决的是什么问题? + +1. 一般的电商系统你点击进去以后,都能看到这个商品关联了其他好几个类似的商品,而且这些商品很多的信息都是共用的,比如商品图片,海报、销售属性等。 +2. 那么系统是靠什么把这些sku识别为一组的呢,那是这些sku都有一个公用的spu信息。而它们公共的信息,都放在spu信息下,更方便管理。类似于面向对象的封装,把公共的信息封装起来。 + + + +## 4.PMS基本属性 + +> 参考:2019版-04 谷粒商品pms.docx + +### 1.什么是平台属性? + + + +![数据结构之平台属性](https://npm.elemecdn.com/youthlql@1.0.12/image/002.png) + +- 比如电脑整机的一级分类下,有笔记本、游戏本、台式机、一体机的二级分类。笔记本这个二级分类又包含了处理器、屏幕尺寸、内存容量、硬盘容量、显卡类别这些属性。那么针对联想某个型号的笔记本,它作为笔记本这种分类,每个分类属性都有对应的值,cpu(属性)是i7(属性值)的,内存(属性)是8G(属性值)的,屏幕尺寸(属性)是14寸(属性值)的。 + +- 所以注意一点,平台属性的外键是三级分类id,在使用平台属性功能之前必须选择三级分类。 + + + +### 2.什么是销售属性? + + + +![数据结构之销售属性](https://npm.elemecdn.com/youthlql@1.0.12/image/003.png) + + + +### 3.销售属性与平台属性的关系? + +![数据结构](https://npm.elemecdn.com/youthlql@1.0.12/image/004.png) + + + +## 5.申请对接支付宝的流程? + + + +### 1.打开支付宝网站开放平台(open.alipay.com),并点击下方的网络/移动应用列表 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/094.png) + +### 2.点击支付接入,并填写相关信息 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/095.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/096.png) + +### 3.接入电脑网站支付 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/097.png) + +### 4.进行签约 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/098.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/099.png) + +### 5.代码流程 + +①引入SDK + +②进行支付宝的相关配置 + +``` java +package com.atguigu.atcrowdfunding.app.config; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.atguigu.front.vo.pay.PayVo; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "alipay") +@Component +@Data +public class AlipayTemplate { + + //在支付宝创建的应用的id + private String app_id = "2016092200568607"; + + // 商户私钥,您的PKCS8格式RSA2私钥 + private String merchant_private_key = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCSgX/nTQ0lD+S8ObaM5LGZ1hiz18GXnNpqPLhJCym4xOpn35FNPHrPkDGEoMKrZ5LJeA4cZulckD8AtpvBCpeyIkrj/i1WVmSg10hVX67MlVets4UecCHZv2hKAN0/iId76kozdqrd7Csp/YgXPquN9Np0NFotggTrmiBANk+vcpTF9SCGrDq/isOoCvClfbvVJjApfLLOel3yECe5K/SZ8puiWILVm1NxEXAqJ8z0ipPZVGrXsT6Bo0pEyCPcEL0SqaC9WT0zdWQzdUknCzZV9W2wKjEXBJG9hqxay5kPaKm9leBatSkDAaDxH/N5g36HRfY7BmklwRZsp17lHinxAgMBAAECggEAfnnfck35WBKFc90a9D0F+Xlzr+ZGEV3uzKIIsb46UXFlrzC5HoVkvEWOCiJCjHiIpvbGr8xED43TZgk/IwLC/JxQLM0kVJGWo6fWoSVOIP2YSLNe620APBvaq3BdkFiMJfSYBB+g2J7mkIR39SE8Nvu3j3QWmYzSNJbE2spINnwTzNBL1OPaB5h3hSjyI07KaUcOjhTBF0EZl83NlBDsxmQvy0NmuOIWAcIXXvGoIbwkA774J3LhwL+VS4W2FpQj4FlxvDlPu24GeNWN7oO66T3Jp9bweO120ObhuKwZQosDGkJq0975zVSJX5QtUWHMM/QDPO8Pk24n2AoPcACQcQKBgQDS6kqD+sK8dDBpkmxYopA1gJJATnur0RHFZJb5webOhnEZnePhB1hhhGvKFcrdY2hcYeQiUZkHMsnWItNUe9E9ccp4++m6KKG0iV/BQda7zx1zMTTZUMvSbO282Q31YnQu7Yz6BSk4f/U5Qbu61AK53Tv1ejSAgQhXt1Pwq8KD7QKBgQCx0pkqW4+53tY2o4iPqFGjKYI2yk5bAH5etmOvW51OZ4Slsq/aUJKBVG6fOpRVKkiXulHhrp5csZH0/C7kaj4Hy7TjgUKSWvwlv7i7jgN0dq/bhVJz82y+N9pENWvy5J0I8Kt67XH+6JDEGWjlV58auifMRSx5mRJNn5pM6qrFlQKBgFyZWm/JV1fv1xVyoLjlXlTvBsbO7kMH/jpgqFwtAk1n/x3VEShJ1kayIbTOjotWSopMvCFJG9tqM+0cyxWLatkELXWifAIsNpqRuYWah1FbZD2fu+kxLNtM0a+YyCUUvZeg2cUnIOraWupxbp9e13eMpvdmWMiWXfhM18CRWEwdAoGAUwT0l076EhgUQJwm1JML0jY94eCfpmLbnNJgRe1qysEPr+B1s2IslA7cOqC5we0kyRmmwsuoibQpZYwbRG7JmRAk2pZtgzDRSbpxv7a0rDoBLmbXMOU0Hraqw2+Bf3v2SMc79/9FWnIvrC4EyBYZZPwGOpsNAZRSdEUQX9qrceUCgYB99OOtFFt1ixzyTCyUj3Fuiw7BsPhdI3nuMSoNTPIDNpzRBp/KFXyv/FNJ2CjTAsX3OR3D6KmEYihqUfrYeb0P5zoybcQLMxbXxK+ec6F2o6U2iqFIq0MKwHUqsb9X3pj4qE0ZHbFgRtIHnL2/QGV5PFJdmIZIBKZcvB8fW6ztDA=="; + // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 + private String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQQceVUChTJGtF/a8SXufhSxDTKporieTq9NO7yDZSpDlAX1zVPT/nf0KWAlxq1TYappWMIYtyrOABhJyn6flNP6vuSBiM5lYsepHvYrtRHqlFiJruEkiaCgEZBKL5aCfBHYj0oqgQn9MpNV/PEH4cBYAVaiI4+VX8CBUQfeEGjgN6OkpLULZ3X0JUkmSnVvCNJ1m3PD68IIlbOfEZXJUKCqmZhzprGR5VWswjxA+g87cMwvijL4gdkSy/daG62Bz5vApcmmMkuX1k1fMWP4ajZCASVw8HD+MSLRhd8We9F97gd8CW0TavzbdR+mTS5H4yEgO8F9HRAsbkhV9yu0yQIDAQAB"; + // 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 + // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息 + private String notify_url; + + // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 + //同步通知,支付成功,一般跳转到成功页 + private String return_url; + + // 签名方式 + private String sign_type = "RSA2"; + + // 字符编码格式 + private String charset = "utf-8"; + + // 支付宝网关; https://openapi.alipaydev.com/gateway.do + private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do"; + + public String pay(PayVo vo) throws AlipayApiException { + + //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type); + //1、根据支付宝的配置生成一个支付客户端 + AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, + app_id, merchant_private_key, "json", + charset, alipay_public_key, sign_type); + + //2、创建一个支付请求 //设置请求参数 + AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); + alipayRequest.setReturnUrl(return_url); + alipayRequest.setNotifyUrl(notify_url); + + //商户订单号,商户网站订单系统中唯一订单号,必填 + String out_trade_no = vo.getOut_trade_no(); + //付款金额,必填 + String total_amount = vo.getTotal_amount(); + //订单名称,必填 + String subject = vo.getSubject(); + //商品描述,可空 + String body = vo.getBody(); + + alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," + + "\"total_amount\":\""+ total_amount +"\"," + + "\"subject\":\""+ subject +"\"," + + "\"body\":\""+ body +"\"," + + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); + + String result = alipayClient.pageExecute(alipayRequest).getBody(); + + //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面 + System.out.println("支付宝的响应:"+result); + + return result; + + } +} + +``` + + + +## 6.用户下单到完成支付的执行流程? + +1.用户在购物车页点击**去结算**,然后后台跳转到结算页,此结算页用于给用户确认收货地址,联系人,商品等信息。 + +- 下面这个就是结算页,也可称作订单确认页 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/114.png) + +2.后台同时会生成一个交易码(也可称作原子令牌),放在结算页(订单确认页) + +3.用户点击提交订单,首先会验证交易码(原子令牌) + +- 如果验证不成功(说明用户通过浏览器回滚等原因重复提交),则直接提示错误。 +- 如果验证成功,后台创建订单项,填充订单信息(收货地址,金额等) + +4.接着进行**验价操作**,从数据查出购买商品的实际价格与页面提交的价格进行对比。如果失败,则提示商品价格出现变化,请重新确认,重定向到结算页(订单确认页)。 + +5.如果验价通过则保存订单到数据库,并远程锁定库存。(锁库存这里会用到基于MQ的最终一致性分布式事务,细问的话看**40.RokcetMQ解决订单-库存场景的分布式事务**) + +- 如下图所示,**TODO 4.远程锁库存**。如果锁定成功,就往RocketMQ里发送一条延时消息(假定的是40min的**延时消息A**,在后面解锁库存会用到) +- 接着如果订单创建成功,库存也锁成功了,之后也没有发生异常。则往RocketMQ里也发送一条延时消息(假定的是30min的**延时消息B**,在后面关闭订单的时候会用到) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/089.png) + +6.一切操作成功的话,就跳转到支付页。(支付更细的流程看**42.支付相关问题**) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/100.png) + +7.点击支付宝,进行一系类的支付信息的处理,跳转到支付宝支付页面。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/102.png) + +8.如果支付成功,则会通过支付宝的异步回调接口,保存交易流水,修改订单支付状态等数据。接着就可以等物流了。注意我们的库存在用户没有确认收货的时候只是处于锁定状态,用户确认收货的时候才是真正的减去。 + +![1589973740556](https://npm.elemecdn.com/youthlql@1.0.12/image/109.png) + +9.如果中间出现任何的问题支付不成功,比如30min未支付,或者库存等各种异常。就会根据延迟之前的MQ延迟消息进行相应的分布式事务控制。延迟消息B先关闭订单,延迟消息A再解锁库存。(更多场景见**40-42**)。 + +## 7.用户下单成功但是没有接受到信息怎么处理?(未总结) + +## 8.订单生成的过程? + +下面图只是个参考,具体文字逻辑看**6.用户下单到完成支付的执行流程?** + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/070.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/071.png) + + + +## 9.添加购物车的过程? + +1.传递参数(商品skuid,添加数量) + +2.根据skuid调用skuService查询商品的详细信息 + +3.将商品详细信息封装成购物车信息 + +4.判断用户是否登录 + +5.根据用户登录决定走cookie的分支还是db + + - 用户没登录 + + - 若cookie为空,则直接加入购物车列表,然后添加cookie。 + + - 若cookie不为空,则判断此商品是否添加过。如果添加过则更新数量,没添加过则直接更新。最后更新Cookie + +- 用户登录了,则根据用户ID和商品spuid,检查此用户购物车是否有此商品 + - 若有此商品,则更新数据库数量。最后更新redis缓存 + - 若无此商品,则直接添加到数据库。最后更新redis缓存 + +## 10.Item商品详情页 + +> 参考:2019版-05 谷粒商品详情页.docx,2019版-day07-Item商品详情.docx + +### 1.从外部点进这个商品详情页后 + +**问题**: + +1. 查出该商品的spu的所有销售属性和属性值 + +2. 标识出本商品(当前sku)对应的销售属性,也就是标红 + +**解决办法:** + +1. 双重for循环,效率很低 + - 循环当前skuInfo对象的销售属性值和对应spu的销售属性值,如果相等就把返回的spu销售属性值对应的isCheck字段置为1,否则设为0。前端页面展示的就是is_check为1,框就变红。 + - 首先双重for循环,效率就低。其次,通过skuInfo对象还要再到数据库里查出此sku的所有销售属性值,多了一次数据库操作,很费时。还有一次数据库操作就是查出spu的所有属性和属性值 + +2. 巧妙的用sql,**算是一次代码优化** + - 通过当前sku对应spu的id,查出所有该spu的销售属性和属性值,并关联某skuid如果能关联上is_check设为1,否则设为0。前端页面展示的就是is_check为1,框就变红。 **这一步的sql有点复杂,但是只需要需要查一次数据库** + - 这样的话通过一次sql,在查出spu的所有属性和属性值的同时关联skuid。 + +```mysql + SELECT sa.id ,sa.spu_id, sa.sale_attr_name,sa.sale_attr_id, + sv.id sale_attr_value_id, + sv.sale_attr_value_name, + skv.sku_id, + IF(skv.sku_id IS NOT NULL,1,0) is_check + FROM spu_sale_attr sa + INNER JOIN spu_sale_attr_value sv ON sa.spu_id=sv.spu_id AND sa.sale_attr_id=sv.sale_attr_id + LEFT JOIN sku_sale_attr_value skv ON skv.sale_attr_id= sa.sale_attr_id AND skv.sale_attr_value_id=sv.id AND skv.sku_id=10 + WHERE sa.spu_id=24 +ORDER BY sv.sale_attr_id,sv.id +``` + + + +### 2.sku根据销售属性的动态切换 + +**要解决的问题:**根据销售属性切换一个sku的其他兄弟姐妹 + +**1.传统的步骤:** + +1. 页面根据销售属性的选择的组合,定位到关联的sku。具体就是通过页面被选择属性值id,得到skuId。**这一步要查一次数据库** +2. 根据skuId查询到sku对象返回到页面。 **这一步要查一次数据库** + +- 缺点:每换一次属性值,就要去数据库查skuid,很费时。 + + + +**2.一个小优化:** + +1. 在用户进入某一个spu领域后,将该spu所包含的sku们和这些sku对应的销售属性值,生成一个k是销售属性值组合,v是skuId的hash表格,放到页面上。 + + + + | key(就是对应的销售属性值) | value(类似239\|243这种组合对应的skuid) | + | :-----------------------: | :------------------------------------: | + | k:239\|243 | v:106 | + | k:239\|244 | v:107 | + | k:240\|245 | v:108 | + +**流程:**也就是在一进入任意一个sku的item页时,就把这个sku对应的spu属性和属性值全查出来。同时也利用查出来的spuid,查出此spu下的所有skuinfo,制作上述hash表。 + +**优点:**每换一个属性,不用再去数据库查skuid。直接在页面上根据hash的key找出skuid,,节省了很多次查询数据库操作。 + +## 11.Item详情页的性能优化 + +> 参考:2019版-05 谷粒商品详情页.docx,2019版-day07-Item商品详情.docx + +### 1.为什么Item详情页要进行性能优化? + +虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能必须进行尽可能的优化。 + +- 一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。 + +- 一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。 + +- 提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。 + +- 重点要讲的是另外一个层面:尽量避免直接查询数据库。解决办法就是:**缓存**。缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。 + + + +### 2.解决方案(redis) + +- 由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。 + +- 企业中最常用的方式就是:object:id:filed,比如:sku:1314:info。 + + + +## 12.压测和性能优化 + +> 参考:尚硅谷官方-2020谷粒电商 + +### 1. Jmeter性能压测 + +下面你这个是百度的,这个是Jmeter大致的界面 + +![这个测的是百度的](https://npm.elemecdn.com/youthlql@1.0.12/image/006.png) + + + +### 2.Jconsole和jvisualvm性能监控 + +#### jvisualvm截图 + +![jvisualvm](https://npm.elemecdn.com/youthlql@1.0.12/image/007.png) + + + +#### GC可视化: + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/008.png) + + + +### 3.性能指标 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/009.png) + + + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/010.png) + +### 4.简单压测结果 + +2020谷粒电商-视频147的压测结果。具体的硬件还不清楚 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/011.png) + +### 5.静态资源优化 + +1. 前端用的tymleaf。之前的静态资源加载都是交给了tomcat,可以用nginx进行动静分离。将静态文件交给nginx,提高微服务的性能。 + + + +## 13.首页优化获取三级分类数据 + +> 参考:尚硅谷官方-2020谷粒电商 + +### 1.优化前的业务代码 + +之前的3级分类数据获取,都是在循环里不断的查数据库。查了N多次数据库,导致性能,吞吐量很差(具体数据可以看**12.压测和性能优化-4.简单压测结果**) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/012.png) + + + +### 2.如何优化 + +将N次数据库查询,改为一次把所有分类查出来,然后再在这个基础上区分1,2,3级分类。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/013.png) + +### 3.优化后结果 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/014.png) + + + +## 14.在你的电商系统中,什么数据适合放入缓存? + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/015.png) + +具体点的就比如: + +- 首页目录的一级,二级,三级分类:**属于**即时性和数据一致性要求不高。 +- 商品列表:**属于**访问量大,且卖家更新商品的频率不高,可以放入缓存。 +- 商品详情:**属于**访问量大,且卖家更新商品的频率不高,可以放入缓存。 + + + +## 15.说说你缓存是怎么考虑的?本地缓存和分布式缓存 + +### 1.本地缓存 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/016.png) + +最简单的本地缓存就是一个Map,单体架构下使用本地缓存没有问题。但是如果是分布式的项目,就会有很多问题。比如我们的商品服务,可能会部署10台服务器。 + +- 第一个问题就是:如果某次请求经过负载均衡进入了1号服务器,这时候一号服务器本地有缓存了。但是其他9个服务器里的服务是没有缓存的,下次经过负载均衡的请求如果到了其他9个服务上,还是要查一遍数据库。 +- 第二个问题就是:本地缓存在分布式环境下的缓存不一致很严重,道理很简单,某个更新操作只能更新这一次所请求的服务上的本地缓存,其他服务上还是旧的缓存。造成严重的数据不一致问题。 + +### 2.分布式缓存 + +常见的就是用redis做分布式缓存。 + + + +## 16.使用缓存优化目录的三级分类业务 + +### 1.代码 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/017.png) + + + +### 2.优化之后压测产生异常 + +#### 出现的问题及原因 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/018.png) + +#### 解决办法 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/019.png) + +### 3.性能比较 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/020.png) + + + +## 17.缓存出现的穿透,雪崩,击穿等问题 + +> 怎么解决看总结的redis部分 + + + +## 18.分布式锁前言问答 + +### 1.锁的时序问题 + +刚开始的时候,把放入缓存这行代码,写在了synchronized代码块之外。导致可能会有:锁释放了,但是缓存还没添加完。其它线程确认了一遍缓存还没有,就继续查数据库了。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/021.png) + + + +解决办法就是把**结果放入缓存**这一步也锁在synchronized代码块里 + +![1589102152078](https://npm.elemecdn.com/youthlql@1.0.12/image/022.png) + +### 2.为什么不能用本地锁? + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/023.png) + +本地锁的synchronized(this)只能锁住当前服务,而在分布式环境下,同一种功能肯定是部署了多个服务,所以出现了锁不住的问题。 + + + + + +## 19.原生redis的分布式锁 + +### 1.分布式锁演化阶段 + +#### 阶段一 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/024.png) + + + +#### 阶段二 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/025.png) + + + +#### 阶段三 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/026.png) + + + +#### 阶段四 + +这个可能有点不太好理解。如果没有lua脚本提供原子性的**判断+删除**,假设我们设置的锁过期时间是10s。 + +1. a线程9.5s的时候我们redis发请求获取到锁的随机值,假设网络传输了0.3s,9.8s时redis收到请求,redis把值传到我们的服务假设网络传输用了0.5s。 +2. 锁的随机值到我们的服务时,redis里的a线程的锁已经释放了。此时b线程(其它线程)加上了锁,我们对比成功之后,会把b线程(其它线程)的锁给删了。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/027.png) + + + +#### 阶段五 + +主要意思就是 原子加锁(set nx ex命令)和原子解锁(lua脚本) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/028.png) + + + +### 2.实际分布式锁代码 + +```Java +@Override +public PmsSkuInfo getSkuById(String skuId,String ip) { + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"进入的商品详情的请求"); + PmsSkuInfo pmsSkuInfo = new PmsSkuInfo(); + // 链接缓存 + Jedis jedis = redisUtil.getJedis(); + // 查询缓存 + String skuKey = "sku:"+skuId+":info"; + String skuJson = jedis.get(skuKey); + + if(StringUtils.isNotBlank(skuJson)){//if(skuJson!=null&&!skuJson.equals("")) + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"从缓存中获取商品详情"); + + pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); + }else{ + // 如果缓存中没有,查询mysql + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"发现缓存中没有,申请缓存的分布式锁:"+"sku:" + skuId + ":lock"); + + // 设置分布式锁 + String token = UUID.randomUUID().toString(); + String OK = jedis.set("sku:" + skuId + ":lock", token, "nx", "px", 10*1000);// 拿到锁的线程有10秒的过期时间 + if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){ + // 设置成功,有权在10秒的过期时间内访问数据库 + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"有权在10秒的过期时间内访问数据库:"+"sku:" + skuId + ":lock"); + + pmsSkuInfo = getSkuByIdFromDb(skuId); + + if(pmsSkuInfo!=null){ + // mysql查询结果存入redis + jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo)); + }else{ + // 数据库中不存在该sku + // 为了防止缓存穿透将,null或者空字符串值设置给redis + jedis.setex("sku:"+skuId+":info",60*3,JSON.toJSONString("")); + } + + // 在访问mysql后,将mysql的分布锁释放 + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"使用完毕,将锁归还:"+"sku:" + skuId + ":lock"); + String lockToken = jedis.get("sku:" + skuId + ":lock"); + if(StringUtils.isNotBlank(lockToken)&&lockToken.equals(token)){ + //jedis.eval("lua");可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生 + jedis.del("sku:" + skuId + ":lock");// 用token确认删除的是自己的sku的锁 + } + }else{ + // 设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法) + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"没有拿到锁,开始自旋"); + + return getSkuById(skuId,ip); + } + } + jedis.close(); + return pmsSkuInfo; +} +``` + + + +> - 上面的分布式锁代码是2019版的谷粒电商代码,lua脚本没有写。 +> - 下面补充了lua脚本你的方式,来自2020官方谷粒电商 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/029.png) + + + +### 3.redission框架分布式锁最终代码 + +```java +private Map> getCatalogJsonFromDbWithRedissonLock() { + + // 1.锁的名字。锁的粒度,越细越快 + // 锁的粒度:具体缓存的是某个数据,11-号商品:product-11-lock + RLock lock = redisson.getLock("CatalogJson-lock"); + lock.lock(); + Map> dataFromDb; + try { + dataFromDb = getDataFromDb(); + } finally { + lock.unlock(); + } + return dataFromDb; +} +``` + + + +## 20.redission分布式锁框架 + +### 1.为什么要使用redission? + +1. Java的JUC包,有各种类型的锁,可重入锁,读写锁,Semaphore/CountDownLatch等很多种特性的锁。但是这些统统属于本地锁,在分布式环境下基本用不了。那我们想用他们的这些特性怎么办呢?redission提供了这些各种各样的分布式同步机制,分布式同步锁。 +2. redission锁有异常自动释放,以及业务时间过长自动续期的功能。 + + + +### 2.常用的redission锁 + +#### 普通的Lock可重入锁 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/030.png) + + + +> 上面是视频截图,下面是github上别人敲的源码。 + +```Java +@ResponseBody + @GetMapping("/hello") + public String hello() { + // 1. 获取一把锁,只要锁的名字一样,就是同一把锁 + RLock lock = redisson.getLock("my-lock"); + + // 2.加锁 + lock.lock(); // 不指定时间,默认30s过期,如果代码业务没有执行完会自动续期,如果执行过程中系统崩溃,则30s后锁在redis中自动被删除 +// lock.lock(10, TimeUnit.SECONDS); // 指定时间,10s之后自动解锁。解锁时间必须大于业务时间,因为不会自动续期,否则会出现系统异常。 + // 1.如果传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间。 + // 2.如果我们未指定锁的超时时间,就使用30 * 1000【lockWatchdogTimeout看门狗的默认时间】 + // 2-1.只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】 + // internalLockLeaseTime【看门狗时间】 / 3 -> 10s续一次期; + + // 最佳实战: + // 1). lock.lock(10, TimeUnit.SECONDS)最好使用指定时间的方法,没有续期的操作,时间设置大一点。 + try { + System.out.println("加锁成功,执行业务" + Thread.currentThread().getId()); + Thread.sleep(30000); + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("释放锁......." + Thread.currentThread().getId()); + lock.unlock(); + } + return "hello"; + } +``` + +常规的分布式lock锁是以hash的数据结构实现的 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/032.png) + +#### 读写锁 + +```java +@ResponseBody +@GetMapping("/write") +public String writeValue() { + String s = ""; + RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); + RLock rLock = readWriteLock.writeLock(); + try { + rLock.lock(); + s = UUID.randomUUID().toString(); + Thread.sleep(30000); + redisTemplate.opsForValue().set("writeValue", s); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + rLock.unlock(); + } + return s; +} + +// 保证一定能读到最新的数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁 +// 写锁没释放读就必须等待 +// 写 + 读:等待写锁释放 +// 写 + 写:阻塞方式 +// 读 + 写:有读锁,写锁也会等待读锁释放后进行。 +// 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都在同时加锁成功。 +@ResponseBody +@GetMapping("/read") +public String readValue() { + String s = ""; + RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); + RLock rLock = readWriteLock.readLock(); + rLock.lock(); + try { + s = redisTemplate.opsForValue().get("writeValue"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + rLock.unlock(); + } + return s; +} +``` + +读写锁,可以看到是以hash数据结构实现的 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/031.png) + + + +#### CountDownLatch + +```java +/** + * 放假锁门 + * 5个班的全部走完,才可以锁大门 + */ +@GetMapping("/lockDoor") +@ResponseBody +public String lockDoor() throws InterruptedException { + RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); + countDownLatch.trySetCount(5); + countDownLatch.await(); + return "ok"; +} + +@GetMapping("/gogogo/{id}") +@ResponseBody +public String gogogo(@PathVariable("id") Integer id) { + RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); + countDownLatch.countDown(); + return id + "班的人都走了!"; +} +``` + + + + + +## 21.你的电商项目是怎样解决缓存数据一致性? + +### 1.一致性的两个模式以及会出现的问题? + + + + + +### 2.解决缓存一致性问题 + + + + + +## 22.缓存层SpringCache + +缺点很突出,分布式环境下不建议使用。老老实实使用原生的redis操作 + +```java +* 4. Spring-Cache的不足: +* 1). 读模式: +* 缓存穿透:查询一个null的数据。解决: cache-null-values = true +* 缓存击穿: 大量并发进来同事查询一个正好过期的数据。解决: 加锁?默认是无锁的. @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)加一个本地锁,而且是读的本地锁 +* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间. spring.cache.redis.time-to-live=xxx ms +* 2). 写模式: (缓存与数据库一致) +* 1). 读写加锁(适用于读多写少的情况) +* 2). 引入canal, 感知到mysql的更新去更新数据库 +* 3). 读多写多,直接查询数据库 +* 总结: +* 常规数据(读多写少, 即时性,一致性要求不高的数据): 完全可以使用spring-Cache +* 特殊数据: 特殊设计 +``` + + + +## 23.项目亮点:线程池与CompletableFuture异步编排 + +### 1.为什么要使用异步编排 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/035.png) + +- 异步的意思是可以用多个线程来完成这六步操作 +- 然而4,5,6的执行都依赖于1的数据。也就是说1执行完,拿到sku的基本信息,才能获得4,5,6的数据。我们在异步的同时需要保证多个线程的顺序。简称**编排**,就是把顺序编排一下。 + + + +## 24.Item商品详情页优化 + +### 1.异步编排优化 + +> 参考:官方2020谷粒电商 + +#### 没有优化前的代码 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/036.png) + + + +#### 优化后的代码 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/037.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/038.png) + +这样写之后,会大大提升item详情页的速度。 + + + +### 2.商品详情页放入redis缓存 + +> 参考:微服务谷粒电商_2019 + +**代码**: + +```java +@Override +public PmsSkuInfo getSkuById(String skuId,String ip) { + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"进入的商品详情的请求"); + PmsSkuInfo pmsSkuInfo = new PmsSkuInfo(); + // 链接缓存 + Jedis jedis = redisUtil.getJedis(); + // 查询缓存 + String skuKey = "sku:"+skuId+":info"; + String skuJson = jedis.get(skuKey); + + if(StringUtils.isNotBlank(skuJson)){//if(skuJson!=null&&!skuJson.equals("")) + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"从缓存中获取商品详情"); + + pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); + }else{ + // 如果缓存中没有,查询mysql + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"发现缓存中没有,申请缓存的分布式锁:"+"sku:" + skuId + ":lock"); + + // 设置分布式锁 + String token = UUID.randomUUID().toString(); + String OK = jedis.set("sku:" + skuId + ":lock", token, "nx", "px", 10*1000);// 拿到锁的线程有10秒的过期时间 + if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){ + // 设置成功,有权在10秒的过期时间内访问数据库 + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"有权在10秒的过期时间内访问数据库:"+"sku:" + skuId + ":lock"); + + pmsSkuInfo = getSkuByIdFromDb(skuId); + + if(pmsSkuInfo!=null){ + // mysql查询结果存入redis + jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo)); + }else{ + // 数据库中不存在该sku + // 为了防止缓存穿透将,null或者空字符串值设置给redis + jedis.setex("sku:"+skuId+":info",60*3,JSON.toJSONString("")); + } + + // 在访问mysql后,将mysql的分布锁释放 + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"使用完毕,将锁归还:"+"sku:" + skuId + ":lock"); + String lockToken = jedis.get("sku:" + skuId + ":lock"); + if(StringUtils.isNotBlank(lockToken)&&lockToken.equals(token)){ + //jedis.eval("lua");可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生 + jedis.del("sku:" + skuId + ":lock");// 用token确认删除的是自己的sku的锁 + } + }else{ + // 设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法) + System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"没有拿到锁,开始自旋"); + + return getSkuById(skuId,ip); + } + } + jedis.close(); + return pmsSkuInfo; +} +``` + +商品详情页属于用户访问量大,且卖家更新少,即时性要求不高的数据 + + + +## 25.缓存总结 + +### 1.在你的电商系统中实际哪些数据放入到了缓存中? + +三级分类目录,商品详情页。商品列表(可说可不说,能圆话就说,觉得圆不了就不说) + + + +### 2.分布式锁其它的实现方式?(待完善) + +我们用的是redis,还有zookeeper也可以。 + + + + + +##+.注意点(待完善) + +- 分布式锁是为了锁数据库,是为了防止数据库被击穿。 + + + +## 26.分布式session + +### 1.有什么问题? + + + +- Session不同步:是指同一个服务的多个实例(部署在多个服务器上的多个实例)之间,session无法同步 + +- Session不共享:是指不同服务之间,域名不同,cookie存的JsessionID无法共享。 + + - 注意一个问题:即使cookie的domain都搞成了相同的域名。用户A在会员服务登录了,session也只会存在会员服务的服务器上(具体就是存在tomcat里)。订单服务拿到了cookie里的jsessionID,由于订单服务的服务器上没有存A的session,订单服务拿到了jsessionID也没用。 + + - 所以要先解决同步问题,也就是保证所有服务都能访问到已存在的session(不管你是存在哪里)。然后再来解决cookie跨域问题。 + + + +### 2.常见的解决办法 + + + +#### 服务器之间Session复制 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/040.png) + + + +#### Session存到客户端 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/041.png) + + + +#### Session进行hash + +大致意思就是,根据用户的ip或者某个特殊字段,通过nginx将其hash到固定的服务器上。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/042.png) + + + +#### Session统一存储 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/043.png) + + + +## 27.SpringSession+redis解决分布式session + +> 这个方法采用的就是Session统一存储 + +- 首先将Session全部存到redis,这样所有的服务都可以访问到,解决了session同步的问题 + +- 其次用SpringSession设置domain,解决session跨域问题。 + +### 1.用法 + +#### 主要配置 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/044.png) + +domain的域名设置成了最大的域名gulimall.com。这样可以看到下面的效果,在auth.gulimall.com域名下登录后,cookie的domain是.gulimall.com。 + +#### 效果 + +![1589355305838](https://npm.elemecdn.com/youthlql@1.0.12/image/045.png) + +其实代码什么都必须要改。我们配置了一些东西,直接调用session.setAttribute(),就会自动把session存到redis + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/046.png) + + + +### 2.原理 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/047.png) + + + +## 28.单点登录 + +> baidu.com是一级域名, xxx.baidu.com格式的都是二级域名;xxx.xxx.baidu.com格式的都是三级域名 + +### + + + +### 1.Spring Session+redis实现的跨二级域名的单点登录 + +> - 分布式session只能解决类似a.jd.com,b.jd.com这样二级域名的分布式系统登录。domain不能设置为.com +> +> - 或者video.qq.com,news.qq.com这种的跨二级域名 + +步骤和原理在上面讲了 + + + +### 2.基于Cookie接入的Web端跨一级域名的单点登录 + +> 以新浪公司为例。新浪公司旗下的**微博**域名是weibo.com,新浪官方网站域名是sina.com.cn。经过测试如果在weibo.com登录之后,sina.com.cn就自动登录。单点登录可以多系统跨域名登录,把认证中心单独成一个系统。 + +#### xxl-sso基于cookie的原理 + +client1访问 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/048.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/049.png) + +client2客户端登录 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/050.png) + + + +#### 我们的sso-demo代码流程 + + + + + +#### 使用jwt优化一个东西 + +> 具体代码见雷丰阳的-2019电商单点登录部分 + +![1589442017455](https://npm.elemecdn.com/youthlql@1.0.12/image/052.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/053.png) + +如果使用这样的优化,倒不如全部都用jwt来实现 + +### 3.基于Token接入的多端跨一级域名的单点登录 + +#### 大致架构和流程图 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/054.png) + + + +下面的两张流程图来自:[使用JWT实现完全跨域](https://blog.csdn.net/weixin_42873937/article/details/82460997) + + + + + + + + + +#### xxl-sso的基于token的文档说明 + +```java +正常情况下,登录流程如下: +1、获取用户输入的账号密码后,请求SSO Server的登录接口,获取用户 sso sessionid ;(参考代码:TokenClientTest.loginTest) +2、登陆成功后,获取到 sso sessionid ,前端(浏览器)需要主动存储,后续请求时需要设置在 Header参数 中 +3、此时,使用 sso sessionid 访问受保护的 "Client01应用" 和 "Client02应用" 提供的接口,接口均正常返回(参考代码:TokenClientTest.clientApiRequestTest) +``` + +## 29.我们的电商系统采用的就是第三种基于token的方式 + +### 1.我们的流程图 + +上面的流程图说的很完善,和我们的电商略有区别。我们没有实现跨一级域名,我们只实现了跨二级域名。 + +- 把认证中心独立成一个服务,只负责token的颁发和校验 +- 每个客户端加一个拦截器,用来校验请求头中是否有jwt,以及jwt的合法性。 + +![1589444375916](https://npm.elemecdn.com/youthlql@1.0.12/image/055.png) + +## 30.认证相关的问题 + +### 1.由认证中心签发的token如何保存? + +1.保存到浏览器的cookie或localstorage中,其它客户端通过iframe+postMeassgae的方式跨域获取 + +2.sso-server认证完成之后,将Token以url参数的形式返回给对应的客户端,并让其储存在本域名下。 + + + +### 2.如何校验JWT + +- 拦截器统一实现,我们的电商系统因为包名都是com.atguigu.gmall。所以我们只在web-util的模块下写了拦截器,但其实也可以在所有客户端都加拦截器。如**28-3的图** +- 通过密钥和签名算法 + + + +### 3.为什么在order.gmall.com域名下登录,其它二级域名(cart.gmall.com)也有cookie? + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/056.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/057.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/058.png) + +从代码中可以看出,我们刻意把domain设置成了gmall.com这种格式。所以我们的单点登录只是实现了跨二级域名的单点登录,并没有跨一级域名。 + + + +### 4.如何判断哪些操作需要不需要登录? + +自定义注解。 + + + +### 5.jwt生成的token能跨域是什么意思? + +> https://blog.csdn.net/weixin_42873937/article/details/82460997 + +- 这是客户端存取JWT的跨域问题只能这样解决 + +1、前端页面将JWT令牌从response响应头中取出,然后存入localstorage或cookie中。但是遇到跨域场景,处理起来就会比较复杂因为一旦在浏览器中跨域将获取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是获取不到的,所以我选择了一种页面跨域的方式进行处理,使用iframe+H5的postMessage。 + +2、或者sso-server认证完成之后,将Token以url参数的形式返回给对应的客户端,并让其储存在本域名a.com下。并且ssoserver.com域名下也存一个 + +- 如果在Authorization header中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。 + +``` +在基于 Token 进行身份验证的的应用程序中,服务器通过Payload、Header和一个密钥(secret)创建令牌(Token)并将 Token发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:Authorization: Bearer Token。 +``` + +**关于cookie的问题** + +1.比如一个cookie(假设我们称为cookieA)的domain为a.weibo.com,那么所有客户端访问a.weibo.com的网页时,都会带上cookieA。但是如果访问b.weibo.com,那么cookie就带不过去。因为domain不一样 + +2.比如一个cookie(假设我们称为cookieA)的domain为weibo.com,那么所有客户端访问a.weibo.com的网页时,都会带上cookieA。访问b.weibo.com,也会带上cookieA。因为domain是最上层的cookie,但是最多只能到weibo.com这种一级域名。 weibo.com和sina.comc.n这种无论如何都共享不了。 + +3.上面的第二个问题可以用:把jwt放在请求头中带过去,这个才是token能跨域的意思。 + + + +## 31.购物车相关的其它问题 + +### 1.存到redis的购物车的结构? + +- 存储的是购物车集合 + +- 购物车缓存中的某一个购物车数据的更新 + + - 如果用set kv 取出json,转化成集合,从集合中取出对象,修改对象,放回集合,集合放回缓存。很麻烦 + - 所以直接使用hash进行存储(方便查询和修改用户购物车集合中的某一个单独的购物车对象) +- 企业中最常用的方式就是:object:id:filed + + +**redis的hash结构:** + +hashkey + +​ Key value + +​ Key value + + **我们存的:** + +"user:"+memberId+":cart" + +​ skuId cart + + ​ skuId cart + +```java +// 同步到redis缓存中 +Jedis jedis = redisUtil.getJedis(); + +Map map = new HashMap<>(); +for (OmsCartItem cartItem : omsCartItems) { + cartItem.setTotalPrice(cartItem.getPrice().multiply(cartItem.getQuantity())); + map.put(cartItem.getProductSkuId(), JSON.toJSONString(cartItem)); +} + +jedis.del("user:"+memberId+":cart"); +jedis.hmset("user:"+memberId+":cart",map); +``` + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/059.png) + +jedis直接存个map就行 + + + +```java +@RequestMapping("cartList") +@LoginRequired(loginSuccess = false) +public String cartList(HttpServletRequest request, HttpServletResponse response, HttpSession session, ModelMap modelMap) { + + List omsCartItems = new ArrayList<>(); + String memberId = (String)request.getAttribute("memberId"); + String nickname = (String)request.getAttribute("nickname"); + + if(StringUtils.isNotBlank(memberId)){ + // 已经登录查询db + omsCartItems = cartService.cartList(memberId); + }else{ + // 没有登录查询cookie + String cartListCookie = CookieUtil.getCookieValue(request, "cartListCookie", true); + if(StringUtils.isNotBlank(cartListCookie)){ + omsCartItems = JSON.parseArray(cartListCookie,OmsCartItem.class); + } + } + + for (OmsCartItem omsCartItem : omsCartItems) { + omsCartItem.setTotalPrice(omsCartItem.getPrice().multiply(omsCartItem.getQuantity())); + } + + modelMap.put("cartList",omsCartItems); + // 被勾选商品的总额 + BigDecimal totalAmount =getTotalAmount(omsCartItems); + modelMap.put("totalAmount",totalAmount); + return "cartList"; +} +``` + + + +### 2.查询购物车列表有没有什么事情需要注意? + +1.如果redis中没有要从数据库中查询,要连带把最新的价格也取出来,默认要显示最新价格而不是当时放入购物车的价格,如果考虑用户体验可以把两者的差价提示给用户。 + +2.如果用户突然登录了,还要注意合并cookie的购物车数据 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/060.png) + + + +### 3.购物车的另一种实现思路 + +拦截器+ThreadLocal实现用户身份鉴别 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/062.png) + + + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/061.png) + +添加购物车操作 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/063.png) + +```java +@Override +public Cart getCart() throws ExecutionException, InterruptedException { + Cart cart = new Cart(); + + UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); + if (userInfoTo.getUserId() != null) { + // 1.已经登录 + String cartKey = CART_PREFIX + userInfoTo.getUserId(); + // 2. 如果临时购物车的数据还没有进行合并【合并购物车】 + String tempCartKey = CART_PREFIX + userInfoTo.getUserKey(); + List tempCartItems = getCartItems(tempCartKey); + if (tempCartItems != null) { + // 临时购物车有数据,需要合并 + for (CartItem tempCartItem : tempCartItems) { + addToCart(tempCartItem.getSkuId(), tempCartItem.getCount()); + } + // 清除临时购物车的数据 + clearCart(tempCartKey); + } + // 3. 获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登陆后的购物车的数据】 + List cartItems = getCartItems(cartKey); + cart.setItems(cartItems); + } else { + // 未登录 + String cartKey = CART_PREFIX + userInfoTo.getUserKey(); + List cartItems = getCartItems(cartKey); + cart.setItems(cartItems); + } + return cart; +} +``` + +## 32.订单的相关问题 + +### 1.同一个电商网站的用户账号,可以不可以在不同的机器上登录 + +可以,web网站的账号时可以在不同的客户端同时登录的 + + + +### 2.在我们点击结算按钮时,后台的购物车数据结构是否被删除,订单数据结构是否生成 + +没有生成,结算按钮不调用后台的service数据库服务,结算页面只是用来用户确认送货清单和选择收获地址信息的页面 + + + +### 3.点击提交订单按钮时, 后台的购物车数据结构是否被删除,订单数据结构是否生成 + +生成了,购物车数据转化为订单数据,购物车表删除数据,订单表新增数据 + +提交订单时,是对服务器的写操作,一般不用表单提交,而是直接从缓存或者数据库中查询用户所要购买的商品,转化成订单 + + + +### 4.如何保证订单不被重复提交 + +**①场景:** + +如何防止用户通过页面回退的方式重复提交同一个订单 + +**②从购物车到下订单有两个主要步骤** + +1.购物车页点击去结算,会生成一个结算页。这个结算页时供用户看的,后台生成订单,并不完全按照上面的来。(比如突然有东西涨价了,后台是按实际情况价格来的。不过可以做友好提示,如果价格变了,给用户提示) + +2.用户确认完结算页后,点击生成订单。自此结束 + +**③如何防止重复提交** + +1.在点击结算的时候,根据memberId生成随机交易码,并存在redis中,还要把交易码(或者说令牌)传给前端。 + +```java +public String genTradeCode(String memberId) { + + Jedis jedis = redisUtil.getJedis(); + + String tradeKey = "user:"+memberId+":tradeCode"; + + String tradeCode = UUID.randomUUID().toString(); + + jedis.setex(tradeKey,60*15,tradeCode); + + jedis.close(); + + return tradeCode; +} +``` + +2.在真正点击**生成订单**时,前端不仅要传用户id等相关信息,还要把交易码传过来。首先检验交易码是否正确,如果正确那么直接删除redis中的交易码。这样用户通过浏览器回退结算页的时候,即使再次点击生成订单,因为交易码检验不过,无法生成订单。 + + + +### 5.删除交易码时会出现什么问题? + +这样的代码如果并发很高,有可能会造成交易码校验全部通过if语句,造成交易码重复使用的问题。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/064.png) + + + +所以删除交易码的时候采用lua脚本删除,就是“原子性的对比+删除”。 + +``` +public String checkTradeCode(String memberId, String tradeCode) { + Jedis jedis = null ; + + try { + jedis = redisUtil.getJedis(); + String tradeKey = "user:" + memberId + ":tradeCode"; + + //String tradeCodeFromCache = jedis.get(tradeKey);// 使用lua脚本在发现key的同时将key删除,防止并发订单攻击 + //对比防重删令牌 + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + Long eval = (Long) jedis.eval(script, Collections.singletonList(tradeKey), Collections.singletonList(tradeCode)); + + if (eval!=null&&eval!=0) { + jedis.del(tradeKey); + return "success"; + } else { + return "fail"; + } + }finally { + jedis.close(); + } +} +``` + + + +## 33.你在实际使用Feign的时候,有没有遇到什么问题,怎么解决的? + +### 1.Feign远程调用丢失请求头问题 + +1.问题的原因就是,Feign远程调用的时候会创建一个新的request。而Feign调用的时候,会经历一系列拦截器。所以我们只需要实现某个拦截器,手动的加入请求头即可。 + +2.大致流程,就是先构造新的request,然后经历一系列拦截器,最后发送请求。需要注意的是下面代码的RequestContextHolder是原请求的上下文环境,而不是新的request的上下文。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/066.png) + + + +**解决办法:** + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/065.png) + + + +### 2.Feign异步调用丢失上下文问题 + +RequestContextHolder内部实现就是用的ThreadLocal。现在主线程和异步线程不是用一个线程,拦截器里的RequestContextHolder当然拿不到主线程的上下文。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/067.png) + +所以需要我们手动在Service方法里给每个异步线程添加主线程的上下文Attributes。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/068.png) + + + +## 34.哪些业务是需要保证幂等性的? + +- 库存,不可能让同一个操作,库存扣减多次 + +- 订单,不可能让同一个操作,生成多个订单。上面的订单交易码,就是为了保证幂等性。 + +- 支付,支付就更不用说了 + +- 更多场景见**2020谷粒电商视频274** + + + +**大致的解决方案** + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/069.png) + +- 刚开始有点疑惑数据库悲观锁(行锁,表锁,gap锁,next-key锁等)和分布式锁为什么能防止幂等性?下面讲一下 + +1.1号订单和2号订单失败了,库存需要被解锁(可能是把数量改回去)。此时库存有三台机器部署了三个一样的服务,A,B,C服务。 + +2.后台写了一个定时异步线程,自动去解锁库存。这时候A,B,C三个服务都同时启动了定时任务,如果没有锁的话就会同时解锁,打破了幂等性。如果有悲观锁或者分布式锁的话,配合(表里加一个是否解锁字段,或者版本啥的)。保证数据的正确性(也就是保证幂等性)。 + +3.这些幂等性方案是需要配合来使用的,并不是只用一个就可以的。并发(**锁来保证**)和保证幂等性(**唯一约束啥的来保证**)是同时存在的。 + +4.如果你没有锁,只有唯一约束。并发量高的情况下,就有可能同时存在两个主键相同的的记录(打破了唯一约束)。 + +> 高并发场景下使用锁来防止唯一约束被打破: +> +> https://blog.csdn.net/ruixing222/article/details/103034392 +> +> https://blog.csdn.net/qq_28018283/article/details/80241090 + + + +## 35.如何锁定库存(未总结) + +这块等github上那位大哥的代码,再总结。没代码不好看具体逻辑 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/072.png) + + + +## 36.库存出现的事务问题 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/075.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/073.png) + +```java +@Transactional + @Override + public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) { + confirmVoThreadLocal.set(vo); + SubmitOrderResponseVo response = new SubmitOrderResponseVo(); + MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get(); + response.setCode(0); + // 1. 验证令牌【令牌的对比和删除必须保证原子性】 + // 0令牌失败 1 删除成功 + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS) else return 0 end"; + String orderToken = vo.getOrderToken(); + // 原子验证令牌和删除令牌 + Long result = (Long) redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken); + if (result == 0L) { + // 令牌验证失败 + response.setCode(1); + return response; + } else { + // 验证成功 + // 下单,创建订单,验令牌,验价格,锁库存........ + OrderCreateTo order = createOrder(); + //2. 验价 + BigDecimal payAmount = order.getOrder().getPayAmount(); + BigDecimal payPrice = vo.getPayPrice(); + if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) { + // 验价通过 + // 3.保存订单 + saveOrder(order); + // 4. 库存锁定,只要有异常回滚订单数据 + // 订单号,所有订单项(skuId, skuName, num) + WareSkuLockVo lockVo = new WareSkuLockVo(); + lockVo.setOrderSn(order.getOrder().getOrderSn()); + List locks = order.getOrderItems().stream().map(item -> { + OrderItemVo itemVo = new OrderItemVo(); + itemVo.setSkuId(item.getSkuId()); + itemVo.setCount(item.getSkuQuantity()); + itemVo.setTitle(item.getSkuName()); + return itemVo; + }).collect(Collectors.toList()); + lockVo.setLocks(locks); + R r = wmsFeignService.orderLockStock(lockVo); + if (r.getCode() == 0) { + // 锁成功了 + response.setOrder(order.getOrder()); + rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder()); + return response; + } else { + // 锁失败了 +// throw new NoStockException(1L); + response.setCode(3); + return response; + } + } else { + // 验价没通过 + } + } + + return null; + } +``` + +### 1.本地事务会出现的问题 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/074.png) + +### 2.CAP定理 + +> https://www.zhihu.com/question/54105974 + +#### 为什么CAP只能三选二? + +> 尚硅谷2020谷粒电商-285、商城业务-分布式事务-分布式CAP&Raft原理-从8min开始 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/115.png) + +###为什么P[分区容错]总是成立? + +* 分区容错表明当消息从一个节点向另一个节点发送消息的过程中,消息可能会丢失。以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 一致性 和 可用性 之间做出选择。**所以如果不保证分区容错性的话,所有消息都无法发送的话,分布式系统的各个节点将无法同步消息。** +* 分区容错无法避免,因此可以认为 CAP定理 的 分区容错性 总是成立。 CAP 定理告诉我们,剩下的 一致性 和 可用性 无法同时做到。通常我们为了分区容错,我们的系统必须保证能够在任意网络分区下正常运行。 + + + +### 3.了解RAFT算法吗? + +raft是保证一致性的算法。 + +**领导选举:** + +- 关键点是两个超时时间, + + - 一个是选举超时。follower节点(从节点)--candidate(候选者的)自旋超时时间,一般是150ms-300ms。超过这个时间。没有收到之前的领导命令,就开始自己成为候选者。给其他节点发信息,进行领导选举。 + + - 一个是消息发送的心跳超时时间 + +**保证一致性的过程:**日志复制 + +1.客户端给leader节点发送一个保存值为5的操作 set 5 + +2.leader节点首先把set 5保存在自己的节点日志里,此时并未把这个日志提交到leader节点里。leader节点首先把这个日志随着心跳发送给各个从节点,命令他们保存这个日志。大部分从节点只要响应日志写好了,leader节点就会commit了。leader节点commit之后,通知其他从节点提交5。然后给客户端响应保存成功。 + + + +### 4.当出现网络分区中断的时候,raft算法如何保证一致性(了解,应该不会问) + +> 尚硅谷2020谷粒电商---285、商城业务-分布式事务-分布式CAP&Raft原理 从30min开始讲的 + +有个点可能会有点误解。 + +- 下面的两个节点选不出leader节点,因为得不到3个以上(大部分节点)的同意。所以如果有客户端请求过来,就会告诉下面的两个节点组成的分区不可用。这就是所谓的牺牲了可用性来保证一致性。 +- 但是视频里讲的是下面的2个节点由于本来就有一个leader节点,所以还可以接受set 数据。但是由于得不到大多数节点的确认,还是保存不了数据,所以依旧给客户端返回是不可用的。但是这里我有个疑问,那如果只是读下面两个节点的数据呢?是否就可以读到呢?如果读到了岂不是就不可用了。所以视频讲的可能有一点点问题。以上面的第一个为准。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/076.png) + + + + + +## 37.Base理论 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/077.png) + +实际的分布式系统都是想保证AP,一致性采用最终一致性 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/078.png) + + + +## 38.分布式事务解决方案 + +> https://gitee.com/youthlql/advanced-java/blob/master/docs/distributed-system/distributed-transaction.md + +https://www.cnblogs.com/jiangyu666/p/8522547.html + +https://blog.csdn.net/john1337/article/details/97551499 + +## 39.Seata分布式事务(未总结完) + +Seata分布式事务不适合高并发场景,中间用了全局锁之类的东西,并发能力不高。可以用于并发不高的场景,比如电商的后台管理系统。 + +http://seata.io/zh-cn/docs/overview/what-is-seata.html + + + +## 40.RokcetMQ解决订单-库存场景的分布式事务 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/086.png) + +### 1.为什么不用定时任务? + +我们采用的方式是隔一段时间后,检查库存,同时根据各种逻辑判断是否需要解锁库存。按理来说定时任务也是可以的,但是存在时效性问题,以及下方说的轮询数据库带来压力。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/085.png) + +### 2.Rocketmq延时队列 + +> - 由于rabbitmq不支持定时发送消息,所以需要较为麻烦的方法来实现延时队列,可以看到视频中讲的比较麻烦。 +> +> - 而RocektMQ天生支持延迟消息,也就天生支持延时队列。 + +下面的这种属于补偿性的 + +1.订单发起远程调用锁定库存 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/081.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/083.png) + +2.库存锁定成功的话,发送延时消息(假设延迟消息是40min)。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/082.png) + + + +3.40min中后,MQ检测到消息。并根据情况决定解锁库存或不解锁 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/079.png) + + + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/080.png) + +解锁的sql + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/084.png) + +### 3.延迟队列关闭订单 + +创建订单之后30min之内,若用户没支付,则需要将订单关闭,也就是将订单的状态改为已取消。40min后,则需要执行上面的延时队列解锁库存的操作。 + +**代码流程** + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/089.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/090.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/091.png) + + + +## 41.上面的关单和解锁库存会出现什么问题? + +### 1.问题 + +如果发送订单创建成功的那个消息,因为网络,机器卡顿等原因发送慢了。导致解锁库存的消息先被消费,结果发现订单是**新建状态**,于是是库存就不解锁了,消息也被消费完了。那么一旦上面的那个订单未被支付,库存将永远无法被解锁。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/087.png) + +### 2.解决办法 + +关闭订单后,给库存队列发送一个订单已关闭的消息。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/088.png) + +- 下面代码的第一个监听是为了解决锁定库存远程调用网络故障(实际已锁,但是网络原因返回错误,导致库存锁了,订单回滚了等这些问题)。同时也可以解决正常情况下用户30min未支付,关单并解锁库存的逻辑 + +- 第二个方法的监听就是为了防止上面说的不正常情况。 + +![1589944066987](https://npm.elemecdn.com/youthlql@1.0.12/image/092.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/093.png) + + + +## 42.支付相关问题 + +### 1.你是用什么测试的支付? + +支付宝官方提供的沙箱环境 + + + +### 2.支付的大致代码和页面流程 + +#### ①订单页点击支付宝进行付款 + +页面图片: + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/100.png) + +代码: + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/101.png) + +#### ②跳转到支付宝支付页面 + +上面点击**支付宝**就会向这个Controller发请求,带上必要的数据之后,直接跳转到支付页面 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/103.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/102.png) + +#### ③跳转到我的订单页(同步回调页面) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/106.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/104.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/105.png) + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/107.png) + + + +### 3.异步结果通知,来修改订单支付状态 + +支付宝的异步通知,也算一种分布式事务,属于最大努力通知的分布式事务。就是支付宝通过异步地址不断的给我们发送消息,告诉我们支付成功了。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/108.png) + +![1589973740556](https://npm.elemecdn.com/youthlql@1.0.12/image/109.png) + +![1589973769235](https://npm.elemecdn.com/youthlql@1.0.12/image/110.png) + + + +### 4.支付的最后一步:收单 + +如果我们的订单过期了(根据代码逻辑,订单过期的时候,库存也会马上解锁),支付宝才支付。就有可能造成通过异步回调将订单状态改为已支付,但是库存已经解锁。用户白支付了。所以我们要在支付宝上加上付款时间倒计时。 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/111.png) + +只需要加个参数就可以调用支付宝的自动收单 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/112.png) + +加了1min支付宝自动收单后的效果 + +![](https://npm.elemecdn.com/youthlql@1.0.12/image/113.png) + + + + + +## 电商模式 + +谷粒电商采用的是B2C模式 + + + + + +## 为什么要进行服务拆分,即为什么要用分布式系统(未总结) + +https://gitee.com/youthlql/Java-Interview-Advanced/blob/master/docs/distributed-system/why-dubbo.md + + + +## 什么是RPC + + + + + + + + + + + + + + + + + + + +## #.遇到的难题 + +### 1.sku根据销售属性的动态切换 + +记录一下:视频里当时说的是可以用for循环,也可以通过一个sql语句来写。sql语句的写法在 "2019版-05 谷粒商品详情页.docx" + + + +## 2.Feign调用被认证中心拦截器拦截?没明白为什么 + +> 视频296 + + + + + +## 3.分布式系统为什么要拆分 + +![image-20200913085803325](https://npm.elemecdn.com/youthlql@1.0.12/image/image-20200913085803325.png) +