# tingshu-parent **Repository Path**: olddriver0/tingshu-parent ## Basic Information - **Project Name**: tingshu-parent - **Description**: 谷粒随想-听书 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-11-19 - **Last Updated**: 2024-11-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 谷粒随想-听书 ## day01-项目初始化 ### 1. 项目相关 #### 1.1 telnet命令检测端口是否能连通 ```bash telnet www.baidu.com 80 ```
windows打开方法: ![](assets/telnet.png)
#### 1.2 @Primary 注解 @Primary注解用于标识默认的bean,当有多个bean的时候,可以指定一个bean为默认的bean,当使用@Autowired注解的时候,会优先使用@Primary注解的bean。 例如:`com.atguigu.tingshu.common.config.redis.RedisConfig` ### 2. 问题 #### 2.1 [SpringBoot做了什么?](https://www.yuque.com/u25703258/xrqdl3/gviw20etdme1h8u8?singleDoc) #### 2.2 [TCP三次握手、四次挥手](https://www.yuque.com/u25703258/le8d73/kkonm1koc88euy8f?singleDoc) #### 2.3 [MySQL逻辑架构](https://www.yuque.com/u25703258/ix8lzo/lff3hvdietzlympd?singleDoc) #### 2.4 JVM-堆中有什么? (对象-头(Mark_Word) 数据 对齐部分) (todo) #### 2.5 可见性关键字(读屏障)? (todo) -------- ## day02-创建专辑 - [x] /api/album/category/getBaseCategoryList:获取分类级联列表 - [x] /api/album/category/findAttribute/{category1Id}:根据一级分类id获取标签 - [x] /api/album/fileUpload:上传专辑图片 - [x] /api/album/albumInfo/saveAlbumInfo:创建专辑 ### 1. 项目相关 #### 1.1 SQL语句放在哪个Mapper?(驱动表) `根据一级分类id获取标签`接口中,驱动表是`base_attribute`,则应该在`BaseAttributeMapper`中。 #### 1.2 非空检查注解 (字符串、数值) ```java @NotEmpty(message = "专辑标题不能为空") @Length(min = 2, message = "专辑标题的长度必须大于2") private String albumTitle; @Positive(message = "三级分类不能为空") private Long category3Id; ``` #### 1.3 创建专辑逻辑需注意事项 1. 专辑表`album_info`,创建后得到主键id(todo:未做登录,userId暂时写死) 2. 专辑-标签关联表`album_attribute_value`,创建根据专辑id一对多的标签关联 3. 专辑统计表`album_stat`,每个专辑都有4个统计数据,需要创建4条统计信息 #### 1.4 规范 1. 频繁变化的字段,不和主表放一起(例如专辑表中的统计数据,应该单独出去) 2. 数据库不能为空的字段需要校验,其次是长度校验、数据类型校验(否则会导致索引失效) 3. 使用视图:MySQL会缓存视图的执行计划,这意味着在多次查询同一个视图时,MySQL可能会重用之前的执行计划,从而提高查询效率。 4. 查询语句:mapper层一般使用select***命名,service层一般使用find或get ------------------- ### 2. 问题 #### 2.1 [@ConfigurationProperties 注解需要注意什么?](https://www.yuque.com/u25703258/xrqdl3/kccce97u86r3df0h?singleDoc) #### 2.2 接口和抽象类有什么区别? #### 2.3 [mybatis有几级缓存?](https://www.yuque.com/u25703258/xrqdl3/rbk0p3rv7mbf2dt2?singleDoc) #### 2.4 bean的生命周期 #### 2.5 什么是注入循环依赖? #### 2.6 可重复读 #### 2.7 Spring如何实现事务? ## day03-创建声音 ### 1. 接口 1. - [x] /api/album/albumInfo/findUserAlbumPage/1/10 分页条件查询专辑列表 + 专辑表 inner join 专辑统计表(使用inner可以避免脏数据判断) + 一个专辑对应4个统计数据,需要将4行数据转换成4列数据(**行列转换**) ```sql # 经过分组后,MAX函数操作的是4行数据,这里的if会进行4次判断,只有满足条件的才会赋予正确值,不满足条件的为0。 # 实际上就是利用MAX+条件筛选出需要的那一行数据,这里换成SUM也可以实现 SELECT MAX(if(t2.stat_type = '0404', stat_num, 0)) stat_type1 GROUP BY t1.id ``` 2. - [x] /api/album/albumInfo/removeAlbumInfo/{id} 删除专辑(三张表) + 删除专辑表的数据,需要加入userId作为条件(不能删除其他人的) + 一个疑问:老师这里用mapper的delete方法判定rows<0为失败(为什么删除rows会小于0?:并发?) + 删除统计表数据 + 删除专辑-标签关联表数据 3. - [x] /api/album/albumInfo/getAlbumInfo/{id} 回显专辑用于编辑(两张表:专辑 标签) + 返回数据 AlbumInfo ,使用业务组装即可 + 查询专辑(注意条件userId和专辑非空判断) + 查询专辑的标签信息 4. - [x] put /api/album/albumInfo/updateAlbumInfo/{id} 更新专辑(两张表) + update专辑(也需要注意userId条件) + 删除旧的标签关联,新增新的标签关联 + 由于更新时plus传入了旧的update_time,所以需要更改更新策略 ```java @TableField(value = "update_time", updateStrategy = FieldStrategy.NEVER) ``` 5. - [x] get /api/album/albumInfo/findUserAllAlbumList 查询该用户所有专辑(新增声音表单中下拉列表) + 注意:虽然这里前端没有给分页参数,但为了性能必须分页,预留分页参数前端选择是否传递 + 专辑按照最新在前排序(按照id排序) 6. - [x] post /api/album/trackInfo/uploadTrack 上传声音 + [云点播 VOD-SDK](https://cloud.tencent.com/document/product/266/10276) + 这里需要一个临时本地地址存储文件作为中转(但是这里没有清理本地临时文件,地址使用的是绝对路径) > 本地平台minio(fastdfs)和云平台的选择: > > todo:上行、下行带宽? 7. - [x] post /api/album/trackInfo/saveTrackInfo 新增声音 (两张表: 声音、声音统计) + 请求参数拷贝到do + ordernum需要查出最大值+1 + 声音的一些额外信息,需要根据id去查(创建VodService)[获取媒体详细信息-腾讯云](https://cloud.tencent.com/document/product/266/31763) + 保存声音do得到声音id(设置userId) + 保存声音统计信息 ```java /** * bigDecimal中参数用字符串可以避免精度转换丢失问题 */ trackInfo.setMediaDuration(new BigDecimal(metaData.getDuration().toString())); ``` 8. - [x] post: /api/album/trackInfo/findUserTrackPage/1/10 分页条件查询声音列表(声音表join声音统计表) + 注意行列转换 9. - [x] delete: /api/album/trackInfo/removeTrackInfo/{id} 删除声音(声音表+声音统计表) 10. - [x] /api/album/trackInfo/getTrackInfo 回显声音用于编辑(注意条件userId) 11. - [x] put:/api/album/trackInfo/updateTrackInfo/{id} 更新声音 + 判断修改的声音是否存在 + 判断是否更换了声音源,更新了则需要将媒体数据更换 + updateById之前时,为了避免id被覆盖,再setId一次 ### 2. 接口问题索引 #### 2.1 行列转换(1) #### 2.2 mybaits-puls更新策略的使用(4) #### 2.3 预留分页参数优化(5) #### 2.4 本地平台和云平台的选择(6:todo) #### 2.5 BigDecimal精度丢失问题(7) ### 3. 其他问题 #### 3.1 生产环境不能用System.out.println() + System.out是一个静态常量 + print方法中有同步代码块(锁),会严重影响性能 #### 3.1 ArrayList 多线程下两个问题: ```java ArrayList list = new ArrayList<>(); for (int i = 0; i < 100; i++) { new Thread(() -> { list.add(1); System.out.println(list); // 这里发生并发修改异常 }).start(); } ``` 1. 执行100个线程进行add(),元素小于100: + **modCount**:记录目前实际有多少元素 + add()方法中有modCount++,但这不是原子操作,意味着多线程下中途会被插队,可能导致执行add的次数 > modCount的值。字节码如下: + ```shell # 从 ArrayList 对象中获取 modCount 字段的值,并推入操作数栈: 2 getfield #63 # 将整数 1 推入操作数栈: 5 iconst_1 # 将操作数栈顶部的两个整数相加,并将结果放回操作数栈。 6 iadd # 将操作数栈顶部的整数值赋给 ArrayList 对象的 modCount 字段。 7 putfield #63 ``` 2. 线程中使用System.out.println(list)会发生ConcurrentModificationException + **expectedModCount**:期望有多少元素 + sout调用了list的toString()方法,每次调用时会new一个迭代器,迭代器中初始化expectedModCount = modCount,但是在next()时会进行比较,过程中modCount可能被其他线程修改 #### 3.3 类加载器都加载了什么 + 启动类加载器加载: jre环境下rt.jar核心包 + 扩展类加载器: ext/ + 应用类加载器 #### 3.4 事务的传播行为 (以A嵌套B举例) + **PROPAGATION_REQUIRED**: A有B融入,A无B新建 + **PROPAGATION_SUPPORTS**:A有B融入,A无B无 + **PROPAGATION_MANDATORY**: A有B融入,A无B报错 + **PROPAGATION_REQUIRES_NEW**:不管A,B都新建 + **PROPAGATION_NOT_SUPPORTED**:代表B无事务,但不影响A的事务 + **PROPAGATION_NEVER**:A有B报错,B必须无事务 + **PROPAGATION_NESTED**:A有B新建,A无B新建 ------- 静态方法的锁在方法区,锁的是字节码文件 堆锁在对象头((Mark Word--->64位--->1位偏向锁标识位(0/1) 2位-锁标识位(01/10/00)---> 4位(1111)年龄15晋升 无锁 偏向锁 轻量级锁 重量级锁)) (monitorenter,放两次锁 自旋锁/自适应自旋锁 ## day04-登录 ### 1. 目标 1. 建立登录状态切面验证(common/service-util) 1. 切入点:@LoginRequired注解 2. 切面类:LoginRequiredAspect 3. 报出未登录错误让小程序进入登录页 2. 接口-登录: /api/user/wxLogin/wxLogin/{code}(service-user) 1. 准备restTemplate的bean用于发送http请求 2. 准备WxLoginProperties存储授权登录相关的id、secret、url 3. restTemplate调用 [小程序登录后端调用的接口](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html),得到openid 1. 查询数据库,openid不存在,先创建用户 2. openid存在,得到userId 4. 生成token并返回 1. ```shell # 使用keytool指定算法生成密钥文件(jdk自带) keytool -genkeypair -alias tingshu -keyalg RSA -keypass tingshu -keystore tingshu.jks -storepass tingshu -validity 365 ``` 2. ```shell # 安装openssl执行命令获得公钥,存入SystemConstant keytool -list -rfc --keystore tingshu.jks | openssl x509 -inform pem -pubkey ``` 3. 创建PrivateKeyConfig配置类,使用KeyStoreKeyFactory将私钥bean加载到容器中 4. 使用`JwtHelper.encode(负载, SHA256结合私钥)` 生成token > code为用户登录凭证。 > > 授权登录: > > + 三方平台登录成功后,根据三方返回的用户唯一凭证进行创建用户,绑定其关联关系,1个userId对应一个唯一凭证 > + **后面多个渠道注册的用户如何进行关联呢**? > > 两种方式的区别: > > + 小程序微信授权登录方式 > + code由前端调用wx.login()获取,有效期五分钟。 > + 前端将code发送给后端,后端再请求三方登录接口,获取唯一凭证 > + 网页auth2.0授权登录方式 > + 打开三方登录页面,指定返回类型为code > + 三方登录成功后,将code发 送到指定的地址 3. 接口-获取用户信息:/api/user/wxLogin/getUserInfo(这是第一个需要登录的接口,开始完善切面) 1. 被@LoginRequired注解修饰,经过切面类校验token 1. 通过`(ServletRequestAttributes) RequestContextHolder.getRequestAttributes() `获取request,从而取出token > `RequestContextHolder`中的当前线程的reques是什么时候设置的? > > `DispatcherServlet`继承关系: > > ![image-20240819201203845](assets/image-20240819201203845.png) > > 1. `HttpServletBean` 进行初始化工作 > 2. `FrameworkServlet` 初始化 `WebApplicationContext`,并提供service方法预处理:service()、get()等方法中的`processRequest()`设置request到了本地线程 2. token进行非空判断 3. try catch进行公钥校验`JwtHelper.decodeAndVerify(token, SHA256结合公钥)`,校验失败抛错 4. 解析出载荷,拿到userId,存入ThreadLocal,以便其他需要登录的接口使用(**存在一个问题,其他的接口的业务方法可能在无登录状态下被调用,导致没有userId可以取**) 5. 最后在finally中清除掉ThreadLocal中的数据(不清除会引发oom) 2. 根据ThreadLocal中的userId取出详情即可(同时修改掉以前写死的userId) 4. 登录和切面的完善: + 重复登录:登录时,判断和redis中设备号是否一致 + 多端登录:(如果不做重复登录校验,这里可以换做存token) + 允许多端:直接返回token + 不允许多端: + 登录时,提示前端进行二次校验(短信、邮件等),校验成功后更新设备号 + 切面中,校验令牌成功后,判断redis中设备号是否存在 + 不存在(已过期):可进行续期或提示登录过期 + 存在:判断是否一致,不一致则提示被踢下线 5. JWT校验过程: ```java verifier.verify(this.signingInput(), this.crypto); // 内部形参是content、sig // content:base64(头字节序列) + . + base64(载荷字节序列) this.signingInput() = Codecs.concat(new byte[][]{Codecs.b64UrlEncode(this.header.bytes()), JwtHelper.PERIOD, Codecs.b64UrlEncode(this.content)}); // sig:签名部分处理,这里的buffer是签名部分 crypto = Codecs.b64UrlDecode(buffer); // Signature 使用算法:SHA256withRSA,并使用公钥初始化 Signature signature = Signature.getInstance(this.algorithm); // SHA256withRSA signature.initVerify(this.key); // 公钥 signature.update(content); // content // 执行匹配校验 // SHA256withRSA + content + 公钥 // 是否是和 SHA256withRSA + content + 私钥 生成的签名 相匹配(不是相等) signature.verify(sig) ``` ![image-20240816115234403](assets/image-20240816115234403.png) ![image-20240816091846397](assets/image-20240816091846397.png) ## day05-RabbitMq ### 今日目标 #### 1. mq准备工作 1. mq建立虚拟主机/tingshu,并创建用户tingshu-user1,分配权限 2. 建立common/rabbitmq-utils,引入amqp依赖 3. 配置类RabbitMqAccountInitConfig,初始化队列交换机等信息 1. 使用@Qualifier注解参数注入 4. rebbitMq的确认机制和退回机制配置(三种方式todo,@PostStruct、ApplicationLitener、ApplicationAware) 1. spring.rabbitmq.publisher-returns=true 2. spring.rabbitmq.publisher-confirm-type=correlated 3. (最佳:后续使用定时任务进行统一重发) #### 2. 创建账户 AccountConsumer 监听消息, 失败两次丢弃(判断消息是否重发的消息) > 偏移量offset是队列中的概念,编号Tag是针对消费者的概念 ```java String msg = new String(message.getBody()); MessageProperties messageProperties = message.getMessageProperties(); long deliveryTag = messageProperties.getDeliveryTag(); try { userAccountService.initAccount(Long.valueOf(msg)); channel.basicAck(deliveryTag, false); }catch (Exception e){ //消费消息失败,拒绝消费,两次机会 if(messageProperties.getRedelivered()){ log.error("初始化账户失败两次,丢弃消息:" + e.getMessage()); channel.basicReject(deliveryTag, false); }else{ log.error("初始化账户失败一次,重新投递,tag会变化"); channel.basicReject(deliveryTag, true); } } ``` ### 问题: #### 1. ThreadLocal原理 ![image-20240819210956684](assets/image-20240819210956684.png) 1. JDK8的设计方案的好处 1. 每个Map存储的Entry数量减少 2. 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用 #### 2. 异步的选择? 1. 异步、mq优缺点 1. 异步线程额外开销(异步编排、异步请求、异步方法) 2. mq多引入一个组件,多一份风险;异步 削峰 解耦 排队 2. 如何选择mq、两种: 1. JMS(java message service):生产者、消费者、消息队列必须运行在java平台(rocketMQ)不跨平台不跨语言 2. AMQP(网络协议):RabbitMq、Kafka:数据格式byte[] 跨语言跨平台 1. rabbitMq可以独立部署,适合做业务各种场景(应用各种模式) 2. kafka依赖zookeeper:追求极致的性能 3. rabbitMq消息堆积问题解决? 1. 简单模式改为工作模式,但会引发消费者竞争 2. 避免消费者竞争,就用发布订阅模式(生产者绑定交换机)(广播->路由->主题) 4. 惰性队列 1. 降低内存占用,提高性能 2. 会降低消费效率 5. 仲裁队列 1. 解决集群之间的数据同步问题、 1. 保证高可靠性和消息持久性 #### 3. 三套微服务框架 1. dubbo(内部调用是rpc协议,注册中心zookeeper)alibaba的前作 2. alibaba(内部调用是http协议) 3. spring cloud(注册中心ureka,熔断器, ribbon 、gateway、config) ### 三个集合 #### 1. HashMap + 数据结构:数组+链表+红黑树 + 节点存放:hash、key、value、next + put时初始化16 + 会对hash值进行再hash,无符号右移16位,与原hash进行异或,混合高位和低位的信息,从而减少散列碰撞的可能性 + 通过 h & (16 - 1) 忽略高位,进行高效取模(16为数组长度) + 扩容触发条件: 1. 元素数量 > 扩容因子(默认0.75) * 长度 2. 链表长度 > 8:链表达到8时会尝试转树,转树时数组长度不足64会先尝试扩容,并重新hash;链表>8,数组长度>64则树化(为了避免在小数组下频繁树化) 为什么转红黑树? 1. 二叉树:链表实际上也是一棵二叉树,但是查询效率低 2. 平衡二叉树(AVL):高度差不超过1的二叉树,但高度过高时,增加元素进行平衡时,可能造成整树重构 3. 红黑树:将树的重构细化到小树,解决AVL根节点发生变化时代价过高的问题 4. 扩展:Mysql为什么使用B+树?(哈希索引不适合做范围查询和排序) 1. 二叉树只有两个子节点,而每个子节点的查询都会进行io,高度越高,io次数越多 2. 多叉树:在一个节点中存放更多的数据,降低高度 3. B树(Balance Tree)平衡多叉树: + 每个节点被称为页,子节点最多多少被称为多少阶 + n阶B树,每页最多存放n-1个数据,和n个指针 + 可以降低io次数 4. B+树(为了解决B树每页存放数据过多,非叶子节点只存 索引8bit+指针6bit) + MySQL InnoDB的默认的页大小是16KB,存放行格式(record_type、next_record、列值、事务id等) + 非叶子节点仅用于索引,不保存数据记录。在页内形成单向链表 + 所有关键字都在叶子节点出现,叶子节点在页间形成双向链表 + 聚簇索引、非聚簇索引 + 3层高的1024阶,可以存储2^24(1600w左右)个1kb的数据 #### 2. ConcurrentHashMap 关键点:volatile+cas+锁 + `volatile Node[] table`: 保证节点数组的可见性 + put时: + 如果头节点为空,则进行CAS,失败则重新put + 如果头节点的hash值为MOVED(正在扩容),则帮助其扩容后再put + 如果头节点不为空且没有在扩容,则抢锁(锁为头节点)进行put + get时,因为有volatile,所以随时可见 + remove时: + 也有帮助扩容 + 上锁移除 #### 3. CopyOnWriteArrayList 关键点: + `volatile Object[] array`:保证数据的可见性 + add时: + 加锁 + Arrays.copyOf()复制长度+1的数组,并设置入值 + 将新数组引用赋值给array + get时,因为有volatile,所以随时可见 + remove时: + 加锁 + System.arraycopy()复制数组 ## day06-elasticsearch **概念:**ElasticSearch是一个基于Lucene使用Java开发的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。 + Lucene:Java信息检索程序库的核心工具包(es和solr都是它的实现) **DSL语句:**对象过程,标准的json,数据操作的集合语句 **数据类型:**接收的任何数据和返回的任何数据都只支持json字符串 | es核心概念 | mysql | | :-------------------------: | :------: | | 索引(index) | 库 | | 类型(type) | 表 | | 域(field) | 列 | | 文档(document) | 数据row | | 映射(mapping)数据规则限制 | 建表语句 | **es的域:** | 种类 | 是否索引 | 是否分词 | 是否存储 | 案例 | | --------------------- | -------- | -------- | -------- | ---------------------- | | StringField (keyword) | T | F | T/F | 品牌、分类、身份证号 | | 数字类型 | T | F | T/F | 数量、销量、库存 | | StoreField | F | F | T | 图片地址 | | TextField | T | T | T/F | 商品名字、商品详细描述 | **倒排索引** + 索引域:分词器分出来的词 + 文档域:原始数据 **分词器**:将要给文档中的词语找出来 + 使用场合:1. 存储 2. 搜索 + 种类 + 标准分词器:英文单词、汉字(对中文没啥用) + ik分词器(用的最多)两种模式: + ik_smart(最小分词-粗分) + ik_max_word(最细分词-细分) + 拼音分词器 + 工作流程:(例如:班长在成都尚硅谷学习Java!) 1. 标准过滤:去掉无意义的字、标点符号(班长成都尚硅谷学习Java) 2. 大小写过滤:将所有英文大写转为小写(班长成都尚硅谷学习java) 3. 停用词过滤 4. 提取词语 ## day07 - es初始化 ### es 查询函数 1. 查询所有文档,不经过索引域 ```java client.setQuery(QueryBuilders.matchAllQuery()) ``` 2. 文档下标查询,不经过索引域(**这里的下标是_id,不是域id**) ``` client.prepareGet("java0315_new", "article", "55") ``` 3. 字符串查询,查询2次, 输入的条件**分词** ```java client.setQuery(QueryBuilders.queryStringQuery("ELASTICSEARCH").field("title")) ``` 4. 匹配查询,查询2次,输入的条件**分词**(和字符串查询差不多,api上使用的区别) ```java .setQuery(QueryBuilders.matchQuery("title", "ELASTICSEARCH")) ``` 5. 词条查询,查询2次,不分词 ```java .setQuery(QueryBuilders.termQuery("title", "Elasticsearch".toLowerCase())) ``` 6. 通配符查询,查询2次,不分词(再测试一下,有问题) ```java .setQuery(QueryBuilders.wildcardQuery("title", "*lasticsearch")) ``` 7. 相似度查询 ```java ``` 8. 组合查询 ```java ``` 9. 分页查询 ```java ``` 10. 高亮查询 ```java ``` 单体查询: matchQuery termQuery 查询两次, 词条查询,不分词 wildcardQuery 查询两次,模糊查询 * 不分词 fuzzyQuery 查询两次,相似度查询 不分词 默认偏移量2 组合查询: boolQuery must should mustNot 范围查询: rangeQuery 分页: setSize() setFrom() 起始行从0开始 排序: addSort 高亮: highlighter()注意空指针问题(没有查询高亮的字段体哦阿健) ### 业务: 1. 更新专辑时,如果isOpen=1,需要将数据存入es;删除时需要在es中删除(使用mq通知) 2. rabbitmq定义两个:upper(es新增),down(es删除)两个队列 3. search微服务中消费消息的监听器 4. search: dao层,继承ElasticsearchRepository,这是对es的连接dao;启动类添加注解扫描dao层 5. 消息消费: 1. album专辑详情内部接口,需要开启/client前缀放行`WebSecurityConfig requestMatchers()` 2. user作者详情内部接口 3. album分类视图内部接口 4. album统计信息内部接口,并用BigDecimal算权重分数 5. album标签列表内部接口 6. 将alubm数据库内容转到es中 kibana:绑定数据视图 ## day08 - 搜索、提示词 1. 修改登录切面 2. 接口:根据1级id查询前7的3级id分类 3. 接口:根据1级id查询全部的3级id分类,注意这里要展示二级分类 1. 查询视图 2. 二级分类分桶 4. 接口(search服务):首页频道数据 1. 根据1级分类查询7个三级分类(远程调用album) 2. 根据7个三级分类查询es中全部专辑(elasticsearchClient) 1. 请求对象初始化 2. 构建查询条件 3. 根据三级分类id进行分桶,在桶里根据热度值进行降序取前6的数据 5. 接口(search服务):首页全局专辑搜索 1. 查询条件构造 1. 指定索引 2. 关键字条件 1. 组合条件查询方便后续扩展条件 2. 名字和描述should 3. 分类id条件(全部->里面的筛选才传,接口写完再写) 4. 标签条件(写完再写)(nested嵌套数据过滤) 5. 分页 6. 排序 7. 高亮 - 2. 解析搜索结果 1. 命中数据的总数 2. 命中数据 -> 高亮数据 3. 分页数据,计算总页数 6. 提示词入es 7. 提示词接口 > 两个重点: > > 1. 搜索专辑时 > 1. 将专辑数据库中的内容同步到es > 2. es中建立专辑的索引, ```java // 指定字段,单词条查询 builder.query( query -> query.term( term -> term .field("category3Id") .value(1014) ) ) { "query": { "term": { "category3Id": { "value": 1014 } } } } // 指定字段,多字段查询 builder.query( query -> query.terms( terms -> terms .field("category3Id").terms( item -> item.value(fieldValues) ) ) ); { "query": { "terms": { "category3Id": [ 1014, 1010, 1011, 1012, 1013, 1016, 1017 ] } } } ``` ### 扩展: 1. 组合查询中,must和filter怎么选择? + must会参与评分 + filter则不会,可以加快效率 2. **TF-IDF (Term Frequency-Inverse Document Frequency)** + 文档中词的频率(TF)和该词在整个文档集合中的罕见度(IDF)都会影响评分 ## day09 - 专辑详情 ### 业务: 1. 查询专辑详情 search微服务(远程调用)4个 1. 定义线程池 2. 异步编排 2. 分页查询专辑的声音列表 1. vip状态和vip过期时间存入token的载荷,并且在校验登录的切面类中存入本地线程;初次登录时的注册行为需要初始化这两个属性 2. 三种付费方式判断 ### 回顾: #### 1. redis缓存、布隆过滤器 ![image-20240823120406810](assets/image-20240823120406810.png) #### 2. 线程池 试一下completablefuture main测试 线程池最大线程数2的29次方-1,做了&截断,前3位记录线程状态(5种) 最大线程数=核心线程->逻辑密集型(业务层) 最大线程数=核心线程*2 -> io密集型(数据源连接池) (+1防线程假死) #### 3. 异步编排 ## day10 - 缓存切面 ### 业务 1. 自定义缓存注解 1. prefix前缀(redis的key) 2. 自定义缓存切面 1. 拼接前缀(方法名)+参数字符串生成redis的key时,使用args转成list可以多一个中括号 2. ### 回顾 1. 本地锁的选择? 1. 如果必须全部成功,选择synchronized,因为可以不用考虑释放锁(更适合工具类) 2. 如果允许快速失败,选择lock(更适合业务层) 寻址地址 ## day11 - 订单 会员种类:vip_service_config表 1. 实现接口:查询所有的会员种类 2. 实现接口:trade确认订单(订单微服务,返回OrderInfoVo) + 购买专辑 + 校验(1. 专辑不能重复购买 2. 专辑不能处于未支付 3. vip价格不一样) + 购买声音 + 准备工作:(album服务) + 查询当前专辑下,当前声音以及以后的所有的声音 + 过滤用户已经购买过的声音,远程调用user + todo-过滤用户准备要买的声音(订单微服务) + 查询声音的单价 + 对结果进行每10集进行分页,最大50集 + 记得考虑只有1集的情况 + 校验(1. 声音不能重复购买 2. 不能有未支付的声音订单 3. 声音不打折) + 购买会员 + 校验(1. 会员可以重复购买 2. 不能有未支付的会员订单 3. 会员有折扣) 注意feign的拦截器,将请求头参数带过去 3. 提交订单,使用签名,返回订单号(这里没有判断是否买过,和价格,是否合适????) 1. 判断是否停留超过5分钟,防止数据发生变化 2. 校验签名 3. 存入三张order相关的表,生成订单 ## day12 - 自动取消订单 1. 提交订单优化 1. 重复提交 1. 单端多次 2. 多端并发 2. 同一种类型互斥 3. 弄一个分布式死锁 2. 分页查询订单列表 1. 注意使用声明式分步查询 3. 查询订单详情 4. 取消订单 1. 重复取消问题(同一个用户的同一笔订单同时只能被一个线程取消) 1. 要注意幂等性处理 2. 需要删除步骤1中的死锁 2. 区分手动和自动取消 锁消失? ELK 日志采集框架(es+log4j+kibana) 整理订单话术 cim 用户行为分析 对账 TTL机制:消息的过期时间 死信: 1. 消息到期都没有被消费 2. 消息被拒绝后没有放回队列 3. 消息超出队列的限制(消息大小) 定时任务场景:(对时间精度要求不高) 1. 数据预热(提前准备用户需要的数据) ### 话术:如何实现订单业务? 主要分为两个步骤:订单确认和订单生成。 我这里以项目里的开通会员为例,用户在确定好购买的会员种类后,会进入一个【确认订单】的页面。 **确认订单:** 1. 主要是对用户所选的商品,经过折扣减免等,计算出所需金额后返回 2. 同时会使用 **分布式id** 生成一个流水号,我这里使用的是 **雪花算法** (可能会问雪花算法),流水号将在提交订单时有用。 3. 用户确认好的商品信息及金额等信息后, 提交订单。这里有两种思路: 1. 前端只传流水号,那么后端需要通过持久化等手段,例如:将流水号作为key,提交信息作为value保存到redis,生成订单时从redis中读取,但这种方式在大量订单下会导致redis数据量过多 2. 为了减轻redis的压力,我这里参考了自包含令牌的做法,将商品及金额信息以及流水号进行**摘要算法**生成不可逆密文,并使用非对称加密方式,私钥加密后作为签名返回给前端。用户确认好订单后将信息和签名发送给后端,进行订单生成。这样做可以不需要后端存储订单信息,同时可以保证不被篡改。 **订单生成:** 1. 拿到前端传递的数据后,首先使用公钥对签名解密判断信息是否被篡改 2. 如果校验成功,就可以相信数据可靠,此时就可以根据订单信息生成订单,入库。 3. 当然了,这里存在了一些缺陷: 1. 如果用户在确认订单页面停留时间过长,价格发生了变化怎么办? 1. 我们的产品对这里的需求是,为了保证用户体验,只要用户进了确认订单页面,5分钟内价格就算变动也不需要重新计算。 2. 我在生成流水号的接口中,返回了一个时间戳作为进入确认订单页面的时间 3. 在生成订单前,我先判断了该页面是否停留超过了5分钟 2. 如果一个用户重复下单呢?重复下单分为两种情况: 1. 单端重复提交 1. 单端 2. 多端提交 ## day13 - 支付 重复通知支付成功? 不同渠道都支付成功? 订单被取消后,订单支付成功? 支付成功,但回调多次失败,订单超时取消时需要主动查询订单支付状态。(这个接口可以提供给用户间接查询,但要限制查询频率) 支付服务通用化改造,通配符routing key获取所有的消息可以财务汇总 1. order微服务发起支付,远程调用pay服务获取支付地址 1. 调用之前查询order信息 2. 为了避免反复调用微信接口,需要判断是否重复申请,重复申请则返回上一次的内容(存入redis, 多渠道支付,使用hash结构保存) 2. pay服务发起微信支付接口,需要HttpClient工具 1. 微信做了限制,订单号可以反复申请,但每次数据必须一致,不一致则报错 4. 每成功从微信获取到链接,交易流水则需要保存起来,需要异步(注意本地线程数据)初始化本次交易的流水信息,入库 3. order服务查询订单支付情况,未支付的订单将发起远程调用 1. 如果支付成功,需要异步去修改流水状态 2. todo:修改订单状态 4. wx异步回调接口(没有并发问题不需要锁,但需要做幂等性处理) 1. 修改流水状态,查不到流水就是非法访问,可以什么都不做 2. 如果是未支付,则改成已支付 3. 如果是已支付,需要判断三方流水号是否匹配,不匹配则是多渠道支付,需要退款 订单确认(临时订单) -> 订单提交(生成未支付订单) ## day14 - 支付成功广播 (昨天还漏了取消订单时,查询订单是否已经支付,并且修改状态的逻辑) 1. 支付模块通知订单模块更新订单状态 1. 如果消息发丢了(可靠性投递;自动取消订单也会触发;主动查询) 2. 更新订单需要加锁,并且与取消订单互斥 3. 需要把订单的锁解锁 4. 广播orderNo(账户、专辑、搜索、用户) 5. 其他微服务需要feign查询order的信息 2. 账户服务 1. 消费记录录入 2. 充值记录分页查询 3. 消费记录分页查询 4. 余额查询充值todo 3. 专辑服务 1. 会员类型跳过 2. 专辑类型购买量+1 3. 声音类型加到专辑的购买量 4. 搜索服务 1. 会员跳过 2. 专辑:修改购买量,修改热度值 3. 声音:远程调用获得专辑id,再修改购买量热度值 5. 用户服务 1. 专辑购买记录(先判断是否被处理过) 2. 声音购买记录(先判断是否被处理过,需要远程查询专辑的id) 3. 会员购买记录(先判断是否被处理过,获取购买时长,计算终止时间) 1. 同时需要修改userInfo表的状态 ## day15 - 余额支付 1. 可用余额查询,返回bigDecimal 2. 提交充值订单(是一笔账户订单) 1. 注意: 1. 必须限制充值上限,否则涉及到退款(会扣手续费扣税) 2. 多渠道的付款需要退款,一个订单对应一笔交易 2. 步骤 1. 充值金额判断,不能超过1000 2. 查询可用余额,加起来不能超过1000(compareTo) 3. 组装insert订单(事务) 3. 额外 1. 不能重复并发充值,加分步式锁 tryLock 只试一下(redisson没法直接设定过期时间,用redistemplate设定)15分钟(解锁时机是充值成功或取消) 2. 用户不能存在未支付的充值订单(但这种情况是手动删除锁会发生) 3. 申请支付地址 1. 查询该orderNo的订单未支付是否存在 2. 缓存支付地址(这里的key没用userid,为什么说别人去申请了)15分钟 4. 准备支付成功的通知队列(注意下通知时是写死的路由) 5. 支付成功通知的消费 1. 只处理非已支付的情况(因为有可能是未支付,也有可能是被取消了) 2. 更新rechangeInfo表(和取消订单用同一个锁) 3. 增加可用余额 4. 保存充值记录 6. 发送取消订单的消息进行消费(死信延迟、消费) 1. 记得加锁(和修改订单用同一个锁) 2. 要删除重复充值锁 3. 只取消未支付的 4. 修改为取消 7. 主动取消 8. 余额支付 1. 订单服务中,提交订单时如果是余额payWay,则发送消息进行余额扣款(哪个用户哪笔订单扣多少钱,扣完之后需要发送消息通知订单服务;如果扣失败了,也有取消订单的任务兜底) 1. 思考:这里是把提交订单紧接着就进行支付了 2. 消息是异步的,应该要用feign先判断余额够不够才对? 2. 账户服务监听扣减余额 1. 加锁 tryLock ,只执行一次 2. 判断是否已经被记录 3. 扣减余额,新增记录 4. 通知order(loadbanlanceClient)(5次通知:死循环(线程占据)、定时任务、延迟消息)