|
@@ -215,7 +215,7 @@
|
|
|
- 认证切面类中:拿着要求前端提交设备标识+IP所属行政区域,如果发现设备变更,或者异地登录将Token删除,响应208业务状态码,前端引导用户再次登录。
|
|
|
- 将Token过期时间设置适当短(Token过期后问题不符存在)
|
|
|
|
|
|
-#### 3. 采用ElasticSearch进行站内多条件专辑检索、首页置顶分类热门专辑、关键字自动补全、竞价排名等功能.
|
|
|
+#### 3. 采用ElasticSearch实现站内多条件专辑检索、首页置顶分类热门专辑、关键字自动补全、竞价排名等功能
|
|
|
|
|
|
背景:
|
|
|
|
|
@@ -232,20 +232,20 @@
|
|
|
- 三、排序字段-进行排序
|
|
|
- 四、辅助字段
|
|
|
|
|
|
- - 2.专辑索引数据同步-利用Kakfa消息队列
|
|
|
+ - 2.专辑索引数据同步-利用RabbitMQ消息队列
|
|
|
|
|
|
- - 专辑服务中经过内容安全审核,发送消息到上架话题(存放专辑ID)
|
|
|
- - 搜索服务监听话题中消息,根据消息中专辑ID采用异步任务**CompletableFuture**+**线程池** 封装索引库文档对象,将专辑文档存入ES索引库
|
|
|
- - 开启并行线程分别OpenFeign调用专辑服务查询专辑信息(包含标签列表)、查询专辑统计信息
|
|
|
+ - 专辑服务中经过内容安全审核,发送消息到专辑交换机-路由到上架队列(存放专辑ID)
|
|
|
+ - 搜索服务监听上架队列中消息,根据消息中专辑ID采用异步任务**CompletableFuture**+**线程池** 封装索引库文档对象,将专辑文档存入ES索引库
|
|
|
+ - 开启并行线程分别发起OpenFeign调用专辑服务查询专辑信息(包含标签列表)、查询专辑统计信息
|
|
|
- 专辑信息异步任务执行完毕后,开启两个子线程查询专辑分类及专辑主播信息
|
|
|
- - 将上架专辑ID存入分布式布隆过滤器(解决缓存穿透)
|
|
|
- - 将上架专辑 名称 存入提词索引库(用于自动联想/补全功能)
|
|
|
+ - 将上架专辑ID存入分布式布隆过滤器(解决缓存穿透问题)
|
|
|
+ - 将上架专辑/主播名称 存入提词索引库(用于自动联想/补全功能)
|
|
|
|
|
|
- 2.站内搜索
|
|
|
|
|
|
> 封装检索DSL语句:
|
|
|
>
|
|
|
- > 4.设置查询条件 条件1:关键字 条件2:分类ID 条件3:标签 三个大条件采用and
|
|
|
+ > 设置查询条件 条件1:关键字 条件2:分类ID 条件3:标签 三个大条件 采用and
|
|
|
> 4.1 模拟用户录入“古典”关键字进行查询 需求匹配查询专辑标题或简介,或精确查询主播名称。关键字查询大条件包含三个子条件,三个子条件满足其一即可。考虑到用户录入内容千奇百怪,故选择**must**合适,会有相关性算分。
|
|
|
>
|
|
|
> 4.2 模拟用户选择“有声书=2”1级 分类进行过滤数据,分类数据是系统限制用户选择,适合于进行缓存,相同分类数据进行缓存,缓存命中率比较高。故采用**filter**
|
|
@@ -262,7 +262,7 @@
|
|
|
|
|
|
- 置顶分类热门专辑
|
|
|
|
|
|
- - 业务:在门户前端/app/小程序首页展示每个1级分类下置顶三级分类,同时还需要展示置顶分类下包含热门专辑,为了页面排版,前端展示7个置顶分类+全部、某个置顶分类下展示6个热度较高专辑。点击首页专辑,也支持根据任意级分类进行检索
|
|
|
+ - 业务:在门户前端/app/小程序首页展示每个1级分类下置顶三级分类,同时还需要展示置顶分类下包含热门专辑,为了页面排版,前端展示7个置顶分类+全部、某个置顶分类下展示6个热度较高专辑。点击首页分类,也支持根据任意级分类进行检索
|
|
|
|
|
|
- 实现
|
|
|
|
|
@@ -336,7 +336,7 @@
|
|
|
|
|
|
解决方案:
|
|
|
|
|
|
-应对高并发场景:**1.缓存** **2.线程池+异步** **3.限流** **4.SQL优化** **5.搭建集群**
|
|
|
+服务器端应对高并发手段:**1.缓存** **2.线程池+异步** **3.限流** **4.SQL优化** **5.搭建集群**
|
|
|
|
|
|
创建线程方式:
|
|
|
|
|
@@ -353,7 +353,7 @@
|
|
|
|
|
|
- 统一管理JVM用户线程,提供监控功能
|
|
|
|
|
|
- > 工具类Executors创建线程池(线程数或阻塞队列长度Integer最大值,容易出现栈溢出,堆内存移除)、自定义JDK线程池**ThreadPoolExecutor**,项目集成Zipkin链路追踪组件,发现问题:当多个远程调用串行调用,链路追踪完整(OpenFeign远程调用会自动将链路ID传递下游服务),改为多线程异步后,远程调用由开启子线程调用,子线程无法自动获取主线程中链路ID,导致链路不完整。解决办法:**采用Spring线程池类ThreadPoolTaskExecutor**,包含JDK线程池所有功能,提供装饰器setTaskDecorator编写主线程跟子线程之间传递参数(链路ID)解决问题。
|
|
|
+ > 工具类Executors创建线程池(线程数或阻塞队列长度Integer最大值,容易出现栈溢出,堆内存移除)、自定义JDK线程池**ThreadPoolExecutor**,项目**集成Zipkin链路追踪**组件,发现**问题**:当多个远程调用串行调用,链路追踪完整(OpenFeign远程调用会自动将链路ID传递下游服务),改为多线程异步后,远程调用由开启子线程调用,子线程无法自动获取主线程中链路ID,导致链路不完整。解决办法:**采用Spring线程池类ThreadPoolTaskExecutor**,包含JDK线程池所有功能,提供装饰器setTaskDecorator编写主线程跟子线程之间传递参数(链路ID)解决问题。
|
|
|
>
|
|
|
> 准备好:线程池作用原理、线程池7个参数、线程池拒绝策略、线程池核心数如何设置:2N(核心数)+1阻塞队列一般设置为300~500左右
|
|
|
>
|
|
@@ -372,21 +372,32 @@
|
|
|
- 组合异步任务:CompletableFuture.allOf(CompletableFuture<?>... cfs).join()所有任务执行完毕
|
|
|
- 只需要某个任务执行结束 .CompletableFuture.anyOf(CompletableFuture<?>... cfs)
|
|
|
|
|
|
- - QPS计算方式
|
|
|
+ - QPS平均耗时优差衡量
|
|
|
|
|
|
+ - 传统项目接口耗时小于1s就OK
|
|
|
+ - 互联网项目
|
|
|
+ - 一级接口:一级页面,如:首页或商品详情页
|
|
|
+ - 20并发小于150ms,通过率 >=99.9%
|
|
|
+ - 二级接口:二级页面,如:课程列表、订单列表
|
|
|
+ - 2、二级接口10并发小于200ms,通过率 >=99.9%
|
|
|
+ - 三级接口:三级页面,如:订单详情页、课程详情页
|
|
|
+ - 三级接口5并大小于300ms,通过率 >=99.9%
|
|
|
+
|
|
|
+ - QPS计算方式
|
|
|
+
|
|
|
> 通常情况下平均接口响应时间小于200ms,普通接口小于500ms,特殊接口要求小于1s
|
|
|
>
|
|
|
> ![image-20240604163917920](assets/image-20240604163917920.png)
|
|
|
-
|
|
|
+
|
|
|
- 优化结果:压测工具(ApacheJmeter,ab,ApiPost/ApiFox-在线接口文档及压测一体工具)
|
|
|
-
|
|
|
+
|
|
|
> 开发环境:压测并发100持续1分钟 专辑信息接口-SQL优化(至少达到Range级别)-80ms左右
|
|
|
>
|
|
|
- > 压测并发100持续1分钟 专辑信息接口-SQL优化(至少达到Range级别)-50ms左右
|
|
|
+ > 压测并发100持续1分钟 专辑信息接口-SQL优化+缓存(至少达到Range级别)-50ms左右
|
|
|
>
|
|
|
- > 总计请求数:2w+
|
|
|
+ > 总计请求数:2w+
|
|
|
>
|
|
|
- > 成功比例:99.98% 不能大于百分之1
|
|
|
+ > 成功比例:99.98% 不能大于百分之0.01
|
|
|
>
|
|
|
> 平均接口响应时间:400ms--->260ms
|
|
|
|
|
@@ -453,11 +464,11 @@
|
|
|
|
|
|
- 加锁 通过lua脚本操作hash结构的"锁"
|
|
|
|
|
|
- > 1. 判断锁是非被占用 0 说明锁空闲 or 判断持有锁的线程是否为当前线程
|
|
|
+ > 1. 判断锁是否存在0 说明锁空闲 or 判断持有锁的线程是否为当前线程
|
|
|
>
|
|
|
> 2. 满足第1步加锁条件,当前线程可以加锁
|
|
|
>
|
|
|
- > 2.1 对Hash锁中重入次数+1
|
|
|
+ > 2.1 对Hash中Value重入次数+1
|
|
|
>
|
|
|
> 2.2 将hash锁过期时间设置为30s
|
|
|
>
|
|
@@ -468,20 +479,22 @@
|
|
|
- 续期 通过lua脚本操作hash结构的"锁"
|
|
|
|
|
|
> 持有锁的线程开启锁续期,开启看门狗续期的条件:**锁释放时间**必须等于**-1**,持有锁的线程加锁成功,开启固定延迟时间为10s续期任务,每次续期锁为30s
|
|
|
- >
|
|
|
- > 1. 判断持有锁的线程是否为当前线程 条件成立:说明线程没有主动调用unlock释放锁(业务还未执行结束)
|
|
|
- > 2. 再次对锁的过期时间设置为30s
|
|
|
- > 3. 返回1续期成功
|
|
|
- > 4. 反之:当前线程的锁被线程释放掉,锁过期.返回0
|
|
|
- - 解锁 通过lua脚本操作hash结构的"锁"
|
|
|
-
|
|
|
- > 1. 判断持有锁的线程是否为当前线程
|
|
|
- > 1. 当前线程已经不再持有锁,锁自动过期 返回null
|
|
|
- > 2. 当前线程是持有锁的线程,对重入次数进行-1 返回重入次数
|
|
|
- > 1. 重入次数-1后重入次数大于0,再次对锁的过期时间设置为30s 确保上一次加锁任务逻辑有时间执行,返回false
|
|
|
- > 2. 重入次数-1后,重入次数小于等于0
|
|
|
- > 1. 删除锁
|
|
|
- > 2. 利用Redis发布订阅 命令publish通知阻塞的客户端可以抢锁了
|
|
|
+
|
|
|
+>
|
|
|
+ > 1. 判断持有锁的线程是否为当前线程 条件成立:说明线程没有主动调用unlock释放锁(业务还未执行结束)
|
|
|
+ > 2. 再次对锁的过期时间设置为30s
|
|
|
+ > 3. 返回1续期成功
|
|
|
+ > 4. 反之:当前线程的锁被线程释放掉,锁过期.返回0
|
|
|
+
|
|
|
+解锁 通过lua脚本操作hash结构的"锁"
|
|
|
+
|
|
|
+ > 1. 判断持有锁的线程是否为当前线程
|
|
|
+> 1. 当前线程已经不再持有锁,锁自动过期 返回null
|
|
|
+ > 2. 当前线程是持有锁的线程,对重入次数进行-1 返回重入次数
|
|
|
+ > 1. 重入次数-1后重入次数大于0,再次对锁的过期时间设置为30s 确保上一次加锁任务逻辑有时间执行,返回false
|
|
|
+ > 2. 重入次数-1后,重入次数小于等于0
|
|
|
+ > 1. 删除锁
|
|
|
+ > 2. 利用Redis发布订阅模式publish命令通知阻塞的客户端可以抢锁了
|
|
|
|
|
|
#### 6. 采用自定义缓存注解优化去除冗余缓存及分布式锁业务代码、提高代码复用性
|
|
|
|
|
@@ -519,7 +532,9 @@
|
|
|
>
|
|
|
> 2.2 基于RedissonClient对象创建锁对象 入参为锁key,在Redis中锁结构hash中作为锁名称
|
|
|
>
|
|
|
- > 2.3 调用锁对象lock方法或者tryLock方法。没有看门狗机制:多进程同时执行业务,违背排他性
|
|
|
+ > 2.3 调用锁对象lock方法或者tryLock方法。如果没有看门狗机制:多进程同时执行业务,违背排他性
|
|
|
+ >
|
|
|
+ > 开启看门狗条件:lock,tryLock方法入参为空,默认框架底层锁释放时间为
|
|
|
>
|
|
|
> lock方法会一直阻塞到获取锁成功
|
|
|
>
|
|
@@ -730,21 +745,50 @@
|
|
|
|
|
|
除了展示“商品”信息外,还需要在订单确认页面中采用隐藏域方式,流水号、跟本次订单签名信息
|
|
|
|
|
|
+策略+工厂模式:处理不同商品类别结算逻辑;不同虚拟物品发货
|
|
|
+
|
|
|
流水号:订单确认页渲染接口服务端,采用UUID生成本次订单流水号字符串,将流水号存入Redis设置ttl-5分钟。
|
|
|
|
|
|
签 名:订单确认页面中将所有订单VO参数参与加密 对称加密算法(排序好订单vo所有值+"秘钥") 得到对应签名值
|
|
|
|
|
|
- 步骤二:订单提交(余额支付)
|
|
|
|
|
|
- > 业务流程:“商品”三种:1.VIP会员 2.专辑 3.声音,其中VIP会员及专辑支持两种付款方式(余额跟微信),声音仅支持余额付款。当选择余额支付,首先**业务校验**(一、验证流水号**采用lua脚本(判断跟删除原子性)**验证流水号及删除流水号。二、验签,采用订单确认页相同对称加密算法对订单VO信息+秘钥再次进行签名比对两次签名值是否一致);经过业务校验后,`核心业务1`:保存订单,保存订单其中订单编号(确保全局唯一:采用年月日+雪花算法);保存订单商品明细列表;保存优惠明细列表,此时订单状态:**未支付。**`核心业务2`:扣减账户余额且增加账户变动日志,只要余额扣减成功将地订单状态修改为:**已支付**。`核心业务3`:虚拟物品发货,专辑/声音新增购买记录即可(订单详情页面中声音付费标识-查询用户购买情况);VIP会员新增会员购买记录,更新用户VIP标识及延长会员过期时间。
|
|
|
- >
|
|
|
- > **亮点**:考虑到代码可扩展性,后续可能会再增加其他商品类型代码编写大量if else if,采用**策略+工厂设计模式**(**展开**)符合编程“**开闭原则**”,如果增加新增项目类型,不需要修改现有代码,再次新增对应购买项目策略实现类即可。
|
|
|
- >
|
|
|
- > **亮点**:在三项核心业务对应三个服务三个数据库面临分布式事务问题,**采用阿里巴巴/Apache Seata框架**提供AT模式解决(**展开**工作原理+写隔离/全局锁机制)分布式事务问题;当时遇到问题:由于项目采用全局异常处理机制,导致(Feign远程调用)即使分之事务发生异常,被全局异常类拦截完成处理,导致Feign请求没有异常,导致**分布式事务失效**。解决方案:1.事务发起方判断Feign响应业务状态码(成功业务状态码Integer是200,使用==判断判断返回false,原因:Integer将-128~127将常用数值对应Integer对象进行缓存,改为equals判断),手动抛出异常,被事务发起方感知异常 2. 分之事务业务逻辑中例如:扣减账户结果失败,手动调用Seata全局事务回滚方法,参考:https://seata.apache.org/zh-cn/blog/seata-spring-boot-aop-aspectj
|
|
|
+> 业务流程:“商品”三种:1.VIP会员 2.专辑 3.声音,其中VIP会员及专辑支持两种付款方式(余额跟微信),声音仅支持余额付款。当选择余额支付,首先**业务校验**(一、验证流水号**采用lua脚本(判断跟删除原子性)**验证流水号及删除流水号。二、验签,采用订单确认页相同对称加密算法对订单VO信息+秘钥再次进行签名比对两次签名值是否一致);经过业务校验后,`核心业务1`:保存订单,保存订单其中订单编号(确保全局唯一:采用年月日+雪花算法);保存订单商品明细列表;保存优惠明细列表,此时订单状态:**未支付。**`核心业务2`:扣减账户余额且增加账户变动日志,只要余额扣减成功将地订单状态修改为:**已支付**。`核心业务3`:虚拟物品发货,专辑/声音新增购买记录即可(订单详情页面中声音付费标识-查询用户购买情况);VIP会员新增会员购买记录,更新用户VIP标识及延长会员过期时间。
|
|
|
+>
|
|
|
+> **亮点**:考虑到代码可扩展性,后续可能会再增加其他商品类型代码编写大量if else if,采用**策略+工厂设计模式**(**展开**)符合编程“**开闭原则**”,如果增加新增项目类型,不需要修改现有代码,再次新增对应购买项目策略实现类即可。
|
|
|
+>
|
|
|
+> **亮点**:在三项核心业务对应三个服务三个数据库面临分布式事务问题,**采用阿里巴巴/Apache Seata框架**提供AT模式解决(**展开**工作原理+写隔离/全局锁机制)分布式事务问题;当时遇到问题:由于项目采用全局异常处理机制,导致(Feign远程调用)即使分之事务发生异常,被全局异常类拦截完成处理,导致Feign请求没有异常,导致**分布式事务失效**。解决方案:1.事务发起方判断Feign响应业务状态码(成功业务状态码Integer是200,使用==判断判断返回false,原因:Integer将-128~127将常用数值对应Integer对象进行缓存,改为equals判断),手动抛出异常,被事务发起方感知异常 2. 分之事务业务逻辑中例如:扣减账户结果失败,手动调用Seata全局事务回滚方法,参考:https://seata.apache.org/zh-cn/blog/seata-spring-boot-aop-aspectj
|
|
|
+>
|
|
|
+> 分布式事务解决方案Seata-AT模式工作原理:
|
|
|
+>
|
|
|
+> 一阶段:
|
|
|
+>
|
|
|
+> 1. 事务发起方开启全局事务,Seata生成本次全局事务ID,全局事务ID会在OpenFeign远程调用通过请求头传递到下游服务,全局事务内包含多个分之事务,就能做到同生共死
|
|
|
+> 2. 某个分之事务RM,通过Seata生成分之事务跟全局事务绑定
|
|
|
+> 3. 开启本地事务包含业务数据+回滚日志记录,必须获取到业务记录ID对应全局锁,此时会立即提交本地事务
|
|
|
+> 4. 上报RM事务状态到TC(Seata服务端)
|
|
|
+>
|
|
|
+> 二阶段:
|
|
|
+>
|
|
|
+> TC会根据所有RM在1阶段提交事务状态决定二阶段提交或回滚。
|
|
|
+>
|
|
|
+> RM:收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC,异步和批量地删除相应 UNDO LOG 记录
|
|
|
+>
|
|
|
+> RM:收到 TC 的分支回滚请求,开启一个本地事务内根据xId+branchId相应的 UNDO LOG 记录,得到更新前记录完成数据回滚。
|
|
|
+>
|
|
|
+> **AT优点:无代码入侵(Seata底层基于undo日志表实现补偿)、一阶段提交本地事务,性能较好**
|
|
|
+>
|
|
|
+> **AT缺点:最终一致性解决方案、全局锁+日志多少影响性能**
|
|
|
+>
|
|
|
+> **全局锁机制:**加分项
|
|
|
+>
|
|
|
+> - 一阶段本地事务提交前,需要确保先拿到 **全局锁**
|
|
|
+> - 拿不到 **全局锁** ,不能提交本地事务。
|
|
|
+> - 拿 **全局锁** 的尝试被限制在一定范围内(300ms重试30次),超出范围将放弃,并回滚本地事务,释放本地锁
|
|
|
|
|
|
如果用户选择第三方支付平台:微信涉及调用微信支付SDK,用户付款动作。基于延迟任务,完成延迟关单:将超时未支付订单自动关闭;
|
|
|
|
|
|
-#### 10. 利用流水号机制避免小程序回退造成的订单重复提交-采用Lua脚本确保判断删除原子性
|
|
|
+#### 10. 利用流水号机制避免回退造成的订单重复提交-采用Lua脚本确保判断删除原子性
|
|
|
|
|
|
- 订单结算页-生成流水号
|
|
|
|
|
@@ -780,18 +824,23 @@
|
|
|
> 4. 采用加签相同方式进行再次签名:md5(明文参数, **秘钥**) 得到新签名
|
|
|
> 5. 判断两次签名值是否一致 一致:继续后续业务 不一致:参数被篡改,业务终止
|
|
|
|
|
|
-对称加密:效率高,安全系数低
|
|
|
|
|
|
-非对称加密:效率低,安全性系数高
|
|
|
|
|
|
-面试延伸:场景涉及要求安全系数很高Restful接口调用,如何防止该接口参数在网络传输中被篡改?
|
|
|
+对称加密:加密解密采用相同秘钥值例如:md5、HS256。效率高,安全系数低
|
|
|
+
|
|
|
+非对称加密:加密跟解密使用公私钥例如:RSA。效率低,安全性系数高
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+面试延伸:场景涉及要求安全系数很高Restful接口调用,如何防止该接口请求体业务参数在网络传输中被篡改?
|
|
|
|
|
|
答:采用非对称加密(RSA加密方式),产生两把“钥匙”:
|
|
|
|
|
|
- 公钥:用来加密信息,A给B发送消息,拿着公钥进行加密。必须用配套私钥进行解密
|
|
|
-
|
|
|
- 私钥:用来解密信息,收到来自A的消息,拿着私钥进行解密
|
|
|
|
|
|
+
|
|
|
+
|
|
|
#### 12. 采用策略模式+工厂模式优化不同类型商品处理虚拟物品发货业务
|
|
|
|
|
|
背景:目前项目中提供三种商品类别(产品经理规划后续可能还会进一步丰富会员体系,增加其他商品类别-视频)如果仍然采用传统if else if判断处理不同商品类别,代码可维护性很差,扩展性很差。 特点:不同类型商品“发货”逻辑各不一样,采用行为型设计模式:策略模式 为了能具备“开闭原则”采用工厂统一管理所有策略,提供策略标识返回策略实现类。
|