xiaozhigang

长风破浪会有时,直挂云帆济沧海。

安装部署指南

开发过程均使用的Ubuntu,下面说明指南均基于Ubuntu使用。

中间件安装

需要安装 redis,mysql,minio,rabbitmq 自行搜索,服务器安装即可。

redis参考链接:https://cloud.tencent.com/developer/article/1639658

mysql参考链接:https://blog.csdn.net/xz2005/article/details/130145465

minio参考链接:https://www.cnblogs.com/hunttown/p/17358797.html

rabbitmq参考链接:https://www.cnblogs.com/hunttown/p/17352729.html

安装验证:redis和mysql可用本地连接工具可连接使用,minio和rabbitmq均有可视化界面,可在可视化界面登陆打开。

注意:注意服务器的端口有没有打开

服务器43.154.80.35中已有安装好的中间件,可直接使用,账号密码,代码中的setting.py文件中都有,如果要修改成其他地址中间件,可直接在replace.py中修改相应字段,运行统一修改。

注:现在settings.py文件设置的是服务器43.154.80.35的内网ip,服务器账号密码和各中间件账号密码私信发送。

网络环境安装

服务器要求,

外网,ubuntu

安装软件

privoxy,tor,obfs4proxy

graph LR

a[代码]--8118端口-->b[privoxy]--9050端口-->c[tor]--网桥/obfs4proxy-->d[暗网]
代码

解压压缩包,网站目录下的setting文件中配置了代理端口,将请求发送到本级的8118端口

image-20240828231130808

privoxy

1、安装:sudo apt-get install privoxy

2、修改配置文件:

​ 进入配置文件 vim /etc/privoxy/config

​ 修改接收代理:listen-address 127.0.0.1:8118

​ 修改转发数据:最后一行添加 forward-socks5t / 127.0.0.1:9050 . (注意最后的点不能丢)

image-20240828232233501

image-20240828232433118

​ 配置完重启使配置生效

tor

1、安装tor:sudo apt-get install tor

2、安装obfs4proxy: sudo apt-get install obfs4proxy

3、tor配置文件中添加网桥,vi /etc/tor/torrc,进行如图配置

​ 查看tor 9050端口是否打开,若没有打开,去掉注释打开端口

image-20241003092814823

​ 配置obfs4proxy

​ 配置网桥,地址可用 obfs4 51.178.86.168:54874 773A7F4428AE519A892152EDA963477D85EE672A cert=xghcVpPhAAktkvVpYY6LDsE5iVayo4ADztSEwj0YcqGERxr3+v+RqScaOCC1O/uxeZinWA iat-mode=0,建议多申请几个做备用,网桥也会过期不可用

image-20240828232652882

​ 配置完重启使配置生效

验证网络通路

检测privoxy端口使用:netstat -an | grep 8118

检测tor端口使用:netstat -an | grep 9050

检测整个网络通路:curl -x http://127.0.0.1:8118 http://u5lyidiw4lpkonoctpqzxgyk6xop7w7w3oho4dzzsi272rwnjhyx7ayd.onion/

通路截图:

image-20241003091320866

驱动安装

chrome和chromedriver安装

可借鉴:https://blog.csdn.net/weixin_44184990/article/details/123590435

Google-chrome:版本- Google Chrome 127.0.6533.88

Chromedriver:版本- ChromeDriver 127.0.6533.88

安装路径:/usr/bin/

虚拟环境安装

python环境:安装python,可参考https://blog.csdn.net/qq_45536969/article/details/130124934

虚拟环境:

​ 安装虚拟环境:安装可参考https://blog.csdn.net/m0_64880493/article/details/132964831

​ 安装依赖:安装requirements.txt文件中的所有依赖 pip install -r requirements.txt

代码运行

以下均以43.154.148.210服务器为例

1、打开虚拟环境,虚拟环境在 /opt/dwSpiders/dw-env/bin/下,命令:source /opt/dwSpiders/dw-env/bin/activate,各自安装的目录地址不一样,需要修改命中的路径,

2、进入网站目录,以torrez网站为例,命令:cd /opt/dwSpiders/torrez

3、添加修改cookie,43.154.80.35服务器上的redis中有示例,使用tor登陆后可按示例将cookie填写到自己的redis中(注:leakbase网站没有验证码,登陆逻辑已经写好,直接第4步运行即可)

image-20240902223652351

4、启动代码:python3 run_torrez.py &

5、查看日志:如果当前窗口运行代码,则会自动弹出日志,如果不是,则进入网站目录下的logs目录,查看当前日期日志文件。

6、cookie过期:网站的cookie会过期,过期后直接在redis库中替换就行

7、如需要重跑,需要删除redis相应网站下dupefilter和bloom_fliter_img,这是url请求的指纹和图片的请求指纹,去重使用。

8、mq的使用,在每个网站目录下都加了mq的配置,如要使用,在setting文件中打开即可

image-20240902523652351

9、不建议使用start.sh统一启动,可进入各自网站目录下单独启动运行,根据不同的服务器目录,start.sh需要配置不同的虚拟环境目录和代码目录

中间件初始化

mysql

MySQL的初始化比较简单,在安装好的MySQL中安装 dw-spider 库,库中需要新建5张表(goods、original_page、post、site、user),如图。建表语句代码db目录下有。

image-20240902223652352

minio

minio的初始化也比较简单,在其中新建dw-bucket,如图。

image-20240902223652353

redis

redis中需要预置8个网站的cookie:Onniforums、asap、breachforums、darkdock、dread、mgmgrand、nexus、torrez

redis的key为 网站名称:网站名称_cookie,示例:asap:asap_cookie

redis的value为 hash类型的键值对,如图

Onniforums:image-20240902223652354

asap:image-20240902223652355

breachforums:image-20240902223652356

darkdock:image-20240902223652357

dread:image-20240902223652358

mgmgrand:image-20240902223652359

nexus:image-20240902223652360

torrez:image-20240902223652361

下图为代码运行后的redis截图,其中截了torrez网站和单独的bloom_filter_img。

bloom_filter_img:为所有图片请求过滤所用,内存放所有已请求的图片地址,其内存放的图片地址不会二次请求,如果重跑需删除,代码运行后会自行添加。

dupefilter:为站内地址请求过滤所有,内存放所有已请求的地址hash值,其内已存放的地址不会二次请求,如果重跑需要删除,代码运行后会自动添加。

start_urls:为代码运行时,起始请求的地址,重跑时需要删除,运行后会自动添加,起始地址请求后会自动删除。

torrez_cookie:为网站cookie,运行时需要添加,且保证cookie在有效期内。

image-20240928172856612

rabbitmq

需要新建5个队列:goods、page、post、site、user。

可在可视化界面新建,如图。

image-20240902223652361

其初始交换机为scrapy,无需配置,代码运行时会自动配置。

注:每个队列都在每个网站的mqPipeline.py文件中绑定,不建议修改。

中间件配置修改

代码中有replace.py文件,运行次文件可统一修改当前文件夹下的setting.py文件中的中间件配置。replcae.py中的old_str字段为当前中间件配置,new_str字段为需要修改后的中间件配置,代码运行将setting文件中的old_str配置修改成new_str配置。

​ 在新公司中,遇到了一个之前没遇到过的业务,那就是对接海外的支付。之前一直以为管理对接有什么技术高山需要登顶,接触之后发现好像也没啥,都是对接接口。按照各种支付平台文档要求去对接就好。

​ 前后对接过几个平台的支付:Alipay、PayPal、Payssion、Stripe 。最后发现套路好像都差不多。目前应用到的支付有两种模式:单次支付、订阅支付。

单次支付

​ 各个平台的单次支付都差不多,大体流程如下:

​ 1、商户系统 调用支付平台 API,生成支付订单。

​ 2、支付平台 返回支付链接 / 支付页面。

​ 3、用户 在支付页面完成支付。

​ 4、支付平台 同步/异步通知支付结果。

​ 5、商户系统 根据通知更新订单状态。

​ 支付平台异步通知支付结果的接口有的需要在平台去注册,有的需要在请求生成支付订单的时候作为参数传进去。

平台 发起支付 支付页面 支付完成回调 特点
Alipay 商户调用支付宝下单接口 跳转支付宝收银台 支付宝回调 + 商户查询 国内主流,强制回调机制
PayPal 商户调用 create payment API 跳转 PayPal 网页/APP PayPal 回调 + IPN 跨境常用,用户体验好
Payssion 商户调用 Payssion API 跳转到对应本地支付方式 Payssion 回调 + 查询 聚合多国本地支付
Stripe 商户调用 PaymentIntent API Stripe Checkout 页面 Webhook 通知 开发者友好,API 统一
sequenceDiagram
    participant U as 用户
    participant M as 商户系统
    participant A as 支付平台

    rect rgb(230,230,250)
    U->>M: 调用商户支付
    M->>A: 调用统一收单下单接口 (各平台不同)
    A-->>M: 返回支付链接
    M->>U: 添加回调页面返回用户
    U->>A: 打开支付链接完成支付
    A->>M: 打开回调页面
    A-->>M: 异步回调通知支付结果
    M->>A: 可选:查询订单确认
    end

订阅支付

​ 几个平台的订阅支付,就有所不同了,主要分三种:

1、以alipay为代表,在订单和授权都完成的情况下,每期扣款都需要商户主动调取相应接口。

2、以PayPal / Stripe为代表,在订单和授权都完成的情况下,支付平台会每期自动扣款。

3、以Payssion为代表,每次都需要重新生成订单,重走流程,需要用户手动确认。

sequenceDiagram
    participant User as 用户
    participant App as 商户系统
    participant Pay as 支付平台
    
    Note over User,Pay: 订阅签约流程

    User->>App: 选择订阅
    App->>Pay: 创建签约 / 订阅计划
    Pay-->>App: 返回授权页面链接
    App->>User: 跳转到授权页面
    User->>Pay: 确认授权(免密代扣)
    Pay-->>App: 返回协议号/订阅ID
    Pay-->>User: 回跳到 return_url

    Note over App,Pay: 后续周期扣款

    alt 支付宝
        App->>Pay: 主动调用 alipay.trade.create (带 agreement_no)
        Pay-->>App: 返回扣款结果 + notify
    else PayPal / Stripe
        Pay-->>User: 到期日自动扣款
        Pay-->>App: Webhook 通知结果
    else Payssion
        App->>Pay: 每期重新生成支付订单
        User->>Pay: 手动完成支付
    end

各级缓存

spring中有三级缓存:一级二级三级。三个都是存储bean相关的内容,但是具体存储的内容有所差异。

一级缓存 二级缓存 三级缓存
名称 singletonObjects earlySingletonObjects singletonFactories
类型 ConcurrentHashMap HashMap HashMap
定义 定义是在DefaultSingletonBeanRegistry类中 定义是在DefaultSingletonBeanRegistry类中 定义是在DefaultSingletonBeanRegistry类中
缓存内容 存放就绪状态的Bean 早期曝光的Bean 创建用于获取Bean的工厂类-ObjectFactory实例

image-20240427123420433

一级缓存:就绪状态的bean,保存在该缓存中的Bean所实现Aware子接口的方法已经回调完毕,自定义初始化方法已经执行完毕,也经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理。

二级缓存:早期曝光的bean,一般只有处于循环引用状态的bean才会被保存在该缓存中,所实现的Aware子接口方法还未回调,自定义初始化方法还未执行。

三级缓存:存放获取bean的工厂类-ObjectFactory实例,在IoC容器中,所有刚被创建出来的bean,默认都会保存到该缓存中。

解决循环依赖问题

什么是循环依赖:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class CircularServiceA {
@Autowired
private CircularServiceB circularServiceB;

}
@Service
public class CircularServiceB {
@Autowired
private CircularServiceA circularServiceA;
}

在 A 和 B 循环依赖的场景中:

B populatedBean 查找依赖项 A 的时候,从一级缓存中虽然未获取到 A,但是发现 A 在创建中。

此时,从三级缓存中获取 A 的 singletonFactory 调用工厂方法,创建 getEarlyBeanReference A 的早期引用并返回。

B 引用到 A ,B 就可以初始化完毕,然后 A 同样也可以初始化完毕了。

image-20240427154008953

AOP代理

说是二级缓存其实已经能解决循环依赖的问题,但是为什么需要三级缓存呢。说是解决AOP代理的问题。

如果只有二级缓存,虽然能解决循环依赖,但是在查询二级缓存中返回的实例,而不是我们需要的代理对象,所以添加了三级缓存,在三级对象中获取实例的时候,会经过判断,如果是代理对象则返回代理对象,如果是实例就返回实例。

其实具体的我说的也不是很清楚,可以参考连接:https://zhuanlan.zhihu.com/p/377878056

简介

现在大模型大行其道,我们普通人没有那么多资源,有没有办法搭建一个大模型玩一下呢,答案肯定是有的,我们可以搭建一个知识库,用一下大模型。

总所周知,大模型的训练是需要大量资源的,我们没有这么多资源,那我们就得想办法绕过训练或者减少训练。这时候知识库就是一个比较好的选择,它不需要对大模型进行大量的训练,大模型只是帮我们生成一个类人话的答案。

架构

目前比较用的比较多的就是LangChain框架,这是一种基于Langchain 与 ChatGLM 等大语言模型的本地知识库问答应用实现。

可以从上面的原理图看出知识库的整个实现原理。

1、先加载文件,文件可以是结构化的也可以是非结构化的

2、读取文本,这就很好理解了,将加载进来的文件读取

3、分割文本,将读取的文本分割成一段一段的,便于提取其中的关键字和让内容更内敛

4、向量化,将分割后的文本向量化,

5、存储,将向量化后的文本存入向量数据库中,当然可能也有一些结构型数据,直接存入关系型数据库中

6、问句向量化,将我们提问的问句给向量化,理论上和上面的文本向量化一样,问句也是一个文本

7、向量化匹配,通过问句的向量去向量库里面匹配文本,可能会匹配出多个,我们只取top n

8、生成prompt,将匹配到的文本作为上下文和问题一起生成prompt

9、将prompt提交给LLM生成回答

上面就是知识库的原理和流程。可以看到,其实大模型在这里只是扮演一个类人化回答生成的作用,回答中的知识点其实都是在向量化匹配的过程中匹配出来的。所以这里不需要大模型做大量的训练。

LangChain只是做了一个框架,上面的原理和流程也只是一个大纲,具体的细节我们还是有很多可操控空间的。

比如说文本分割,框架提供的默认分割方法是分割到什么程度,我们需要的又是分割到什么程度,如果框架提供的分割粒度比较答,一篇文章分割之后,分割成了几个大段,我们后面匹配到了,一起放到大模型中生成回答也是一个巨大的计算量或者有些大模型都不支持,文本大了,相当于参数就多了,那生成回答的计算量就变大了,甚至有些参数都支持不了这么多参数。

在比如说,在向量匹配的过程中,我们只能使用框架提供的匹配方法嘛,当然不是,毕竟框架不可能面面俱到,我们可能要做一些权重微调,或者换一种算法,再或者有的时候需要我们去关系型数据库中匹配。

最后,既然大模型只是一个生成类人回答的作用,那我们是不是就可以把LLM就是一个接口,其中具体是什么大模型,我们不是特别关注,毕竟只要能给我们根据知识点生成一段通顺的回答就行,至于是LangChain还是chatGLM,又或者是阿里清华的大模型,我们都不是特别关注。

简介

配置中心可以兼顾配置实时性、配置的流程管理、以及分布式场景的应用。

配置实时性:传统的静态配置方式想要修改某个配置,则需要修改后重启,如果想要实现动态修改,可以使用数据库,采用轮询的方式,访问数据库获得配置数据。轮询频率低的话对配置数据变更的感知就慢,频率高的话,就会消耗过多的性能。

配置管理流程:权限管理、灰度管理、版本管理、格式检验和安全配置

分布式场景:随着采用分布式开发模式,项目之间的相互引用不断增多,相互之间的调用复杂度也指数上升,需要配置中心治理。

功能

灰度发布:当配置修改影响较大时,需要先在部分实例中生效,验证配置变更符合预期之后再推送到所有实例。

权限管理:对配置变更的权限管控,以及对审计权限的管控

版本管理&回滚:当配置变更不符合预期时,需要根据配置发布版本回滚

配置格式校验:配置数据一般会以一种配置格式存储,配置中心会对配置数据的格式进行校验,防止格式错误导致的各种问题

监听查询:当排查问题或者进行统计的时候,需要指导一个配置被哪些实例使用到,以及一个实例使用了哪些配置

多环境:生产环境中,配置中心常常需要涉及到多环境或者多集群,业务在开发环境和生产环境隔离开,或者根据不同的生产线存在多个生产环境,各环境隔离之后相互影响就小。

多集群:当对稳定性要求比较高,不允许各个环境相互影响的时候,需要将多环境通过多集群的方式进行物理隔离

常用配置中心

Apollo

spring cloud config

overall-architecture

nacos

nacos_arch.jpg

对比

spring cloud config Apollo nacos
配置界面 不支持 支持 支持
配置实时推送 支持(spring cloud bus) 支持(http长轮询) 支持(http长轮询)
版本管理 支持(git) 支持 支持
配置回滚 支持(git) 支持 支持
灰度发布 支持 支持 支持
权限管理 支持 支持 支持
多集群 支持 支持 支持
多环境 支持 支持 支持
监听查询 支持 支持 支持
多语言 java 多语言 多语言
配置格式校验 不支持 支持 支持
分布式部署 复杂 复杂 简单
数据一致性 git保证数据一致性,config service从git读取数据 数据量模拟消息队列,Apollo定时读取消息队列 http异步通知
通信协议 http、amqp http http

简述

之前面试问到了工作中用到的注册中心和市面上的有啥异同。

公司用的注册中心:Zookeeper

市面上除此之外还有其他注册中心:Eureka、Consul、Nacos

组件名称 所属公司 组件介绍
Zookeeper Apache Zookeeper是一个分布式协调工具,可以实现注册功能
Eureka Netflix Spring最早的注册中心,目前已经进入停更进维
Consul Hashicorp Consul简化了分布式环境中的服务的注册和发现流程,通过HTTP或者DNS接口发现,支持外部Saas提供者等
Nacos Alibaba Nacos致力于发现、配置和管理微服务。Nacos提供了一组简单易用的特性集,快速实现动态服务发现、服务配置、服务元数据以及流量管理

Zookeeper

遵循CP原则(一致性,分区容错性),牺牲了高可用。所以任何时候请求都能得到一致的数据结果,但是不能保证每次服务请求都是可达的。比如请求的时候leader节点宕机,或者集群中半数以上节点不可用,那么将无法处理请求。

Eureka

遵循AP原则(可用性,分区容错性),牺牲了数据一致性。也就是能保证每次服务请求都是可达的,但是不能保证每次的请求结果都是一致的。Eureka集群没有主从之分,采用的是Peer to Peer对等通信,这是一种去中心化的架构,每个节点都需要添加一个或多个有效的serviceUrl指向其他节点,每个节点都是其他节点的副本。集群中只要还有一个节点存活,那么服务就是可用的,但是不饿能保证查到的数据是最新的或者一致的。

除此之外还有一种保护机制,如果在15分钟内超过85%的节点都没有正常心跳,Eureka则认为客户端与注册中心之间出现了网络故障,此时会出现以下几种情况:

1
2
3
4
5
1、Eureka不再从注册表中移除因长时间没有收到心跳而过期的服务

2、Eureka仍然能够接受新服务注册和查询请求,但不会同步到其他节点上,只保证当前节点可用

3、当网络稳定时,当前实例新注册的信息会被同步到其他节点

Consul

遵循CP原则,Hashicorp公司开发,用于实现分布式系统的服务发现与配置。使用的时Raft算法,比zookeeper使用的Paxos算法更简单,虽然保证了强一致性,但是可用性方面性能有所下降,比如在服务注册方面,Raft协议要求半数以上的节点都写入才算注册成功,leader节点宕机之后,在重新选取出leader节点之前都会导致不可用。

Nacos

遵循AP原则,也可支持CP原则,阿里开源,nacos = 注册中心 + 配置中心,总之很强大,推荐。

总结

Nacos Eureka Consul Zookeeper
一致性协议 CP+AP AP AP CP
健康检查 TCP/HTTP/MySQL/Client Beat Client Beat TCP/HTTP/gRPC/Cmd keep alive
负载均衡 权重/metadata/Selector Ribbon Fabio -
自动注销实例 支持 支持 不支持 支持
访问协议 HTTP/DNS HTTP HTTP/DNS TCP
监听支持 支持 支持 支持 支持
多数据中心 支持 支持 支持 不支持
跨注册中心同步 支持 不支持 支持 不支持
SpringCloud集成 支持 支持 支持 不支持

简述

今天面试了两个,一个下午,一个晚上,下午面试没有聊的很深,貌似很着急招人的样子,晚上聊了很久,更类似于聊天探讨形式。

面试问题

有些问题记不上来了,将记忆清楚和答的不好的记录复盘。

问题1

问:

我们使用的注册中心、配置中心和市面使用的spring或者nacos的异同。

复盘:

公司使用的是zookeeper,市面常见的主要有4种Eureka、Zookeeper、Consul、Nacos

具体的对比差异没有答好,简述了注册中心AP,CP的差异。

在配置中心上,目前常用的有springcloud config、nacos、apollo

具体的差异也没答上。

后续整理归纳一篇文章。

问题2

问:自定义经典线程池拒绝策略

复盘:

相当于知识盲区了,没有答上,只知道有4种拒绝策略:

1
2
3
4
5
6
7
1、AbortPolicy:默认拒绝策略,抛出RejectExecutionException异常

2、CallerRunsPolicy:不抛异常,而是让提交任务的线程自己去执行这个任务

3、DiscardOldestPolicy:策略会丢失队列中最老的以恶搞任务,然后重新尝试新提交被拒绝的任务

4、DiscardPolicy:抛弃被拒绝的任务,不会做任何处理也不会抛出异常。

自定义的拒绝策略没有了解过,可以写篇文章总结了解一下。

问题3

问:优化SQL方案

复盘:知道一些优化方案,但是没有条理性

需要整理一下,最好整理成一个全面的方法论

问题4

问:redis和数据库同步一致性方案

复盘:知道新跟新数据库,再更新缓存

但是全面的方法论没有将出来。

问题5

问:jdk 虚拟线程,新版jdk特性

复盘:jdk8之后都没有了解,这块短板等补齐,目前面试很少问到这个,但是得了解补齐.

问:新版jdk的垃圾回收器

复盘:同上

问题6

问:jdk调优工具

复盘:知识盲点。得补齐,可能后续面试工作中用不到,但是得了解一下。

问题7

问:如何打破双亲加载机制

复盘:知道双亲加载机制,但是如何打破倒是没有深入了解,自己也写篇文章总结一下。

总结

总体来说,每个问题都有自己知道的点,但是好多没有全面的回答。回答的都是一个个的点,没有梳理成一个面,体现自己的逻辑性,条理性和全面性

简述

这次面试场面有点大,6个面试官,但只有3个面试官提问了。前两个面试官回答的还行,后一个面试官直接问懵了。

自我介绍

19年毕业,4年多工作经验,华为这边工作。

目前团队业务是以洞察、评估、规划和收益四个部分向运营商提供数字化机会点发现。

项目是以全球数据沙盘项目为基础,业务向外扩展OTN to 楼宇,OTN综合承载等项目。

自己近项目组以来,从构建测试网络开始了解全球数据沙盘项目,到路网算法开始参与项目,然后开始OTN to 楼宇参与业务拓展,到最后的模型收编和原子能力编排重构项目。

回溯

可以说一下自己参与开源项目dubbo,个人学习构建商城项目的框架代码,以及个人博客文章的分享。

也可以说一下自己在团队内的定位。

可以画个思维导图,下次按思维导图介绍。

第一个面试官

MySQL索引

问:简述一下mysql的索引

答:索引类似于书的目录,加快查找数据,它的存在空间换时间。空间就不用说了,索引的存在必然消耗空间,时间的话就是提高了查找效率,减少了消耗的时间。索引分为聚簇索引非聚簇索引,以InnoDB而言,索引以B+数的形式存储,只不过聚簇和非聚簇在存储细节上有所不同。聚簇也就是主键索引,在叶子节点上是以主键和数据一起存放的,而非聚簇索引的叶子节点存储的是普通索引和主键。在查找数据的时候,如果是主键查询责直接通过B+数找到数据,非聚簇索引的话,要通过B+树先找到主键,在通过主键查到数据,这个过程叫做回表。为了提高查找的效率,所以我们一般在查询的时候尽量使用索引,且减少回表。

回溯

点应该都答到了,感觉面试官还比较满意回答,具体的细节和描述的方法可以更提高一点,显得更有条理性。

总述:索引类似于书的目录,空间换时间,占用了磁盘空间,减少了查找时间。

分述:分聚簇索引非聚簇索引,二者在存储上的差异(B+树叶节点的不同),在查询上的差异(回表)。以及我们在实际使用的时候需要注意的点(减少回表,最左覆盖,索引下推,等)。

追问:既然说到B+树,说一下10个节点的红黑树需要查询一个节点的时间消耗

答:时间复杂度O(ln),红黑树的第一层是一个节点,第二层是2个节点,第三层是4个,第5层是8个。查询的话上来先比较根节点,如果比根节点大往右,否则往左…(简述了一下查询流程)…查询像二分查找,先找中间值,比大小等

回溯

应该是面试官想听到的,现场感觉面试官比较满意,但是自己不是很清楚红黑树,不清楚红黑树的非叶子节点是否保存了数据,还是和B+树一样只做比较。

增加实际业务中的的案例,走联合索引的案例,调换where中的条件顺序。

SpringMVC流程

问:说一下MVC的查询流程

答:客户端 请求到 DispatcherServletDispatcherServletHandlerMapping,然后返回结果给DispatcherServletDispatcherServlet在将handler转发到适配器,适配器会有一些过滤拦截啥的,通过后到处理器,也就是controller->service->map,然后结果在原路返回到DispatcherServletDispatcherServlet在拿到结果后回去调用视图解析器啥的,去将返回的结果解析渲染。

回溯

回答面试官比较满意,听其语义主要想听到DispatcherServlet,但是此流程中记得是在适配器中会有拦截过滤,不是特别确定,已经返回结果后的视图解析渲染,两个组件名称记得不是特别清楚。

img

服务间通信

问:你们有多模块多服务吗,服务间的调用是用的什么。

答:有,上面自我介绍中也说到了,我们所有业务基本可以分为:洞察,评估,规划,收益,四个模块。模块间的调用用的是http请求。

追问:请求的结果是要转换的,你们是怎么转换的。

答:我们的请求不涉及到请求结果的转换,不是那种强依赖。我们模块间的数据交互都是放在数据库的,比如洞察处理完的数据会放在数据库,然后调用评估的模块开始处理,评估模块处理的时候会调用数据库的数据处理,处理的结果会放回数据库,而不是返回给评估模块,这样更解耦。

回溯

其实对这请求调用的方式记得不是特别清楚了,需要复习回顾一下。

https://blog.csdn.net/Andy19891117/article/details/134981402

RPC调用和http调用,RPC基于传输层TCP协议,Http基于应用层http协议,所以RPC调用的传输效率更高。Http规定了返回的格式而RPC没有。

第二个面试官

jar包加载

问:简历中说的到的原子能力编排是怎么理解的,包是spring jar还是啥,编排是怎么理解。

答:jar包是fast jar,不是那种有启动类的可执行jar包,编排的话,是我们把能力抽取成一个个的jar包,再以可视化配置的方式,类似于链式调用。

追问:编排的结果,jar包的顺序执行,这个是以什么数据结构存储的。

答:存在数据库中,是以字符串的形式存储的,在读取到数据库中字符串,在以特定的字符将字符串分割开,然后遍历执行。

追问:前端是怎么配置调用后端的jar的

答:我们是按迭代内的业务需求配置上线的,前台只是提供一个可视化的界面供开发配置,其实我们更习惯直接在配置文件xml中配置,配置完的xml文件会提交到代码仓,在我们的部署阶段,会将这配置文件解析成字符串保存在数据库中,供读取。

追问:这个项目里的调度和引擎服务怎么理解。

答:这个网关,调度,引擎内容都比较简单,网关的话直接对外,调度只负责调用原子能力jar,引擎是加载运行jar。这么分是为了职责更单一内敛。

追问:jar包是怎么加载的。

答:读取到lib目录下的依赖,先将依赖通过双亲依赖加载给加载进来,然后在通过同一规范的jar包入口,开始调用jar内的方法。

追问:双亲加载有三个加载器,加载器是啥。

答:记得不是特别清了,只记得一个是系统加载,两个是加载外部文件依赖加载。

回溯

面试感觉一般,虽然大部分都答到了,但是还有结果点不熟。

1、jar包的分类不熟

fat jar

2、jar包的加载,了解大概流程,不了解其具体细节。

3、三个父加载器忘了,没想起来名字。

4、怎么卸载jar包

启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如java.lang.Object等。启动类加载器是虚拟机实现的一部分,它通常是由本地代码实现的,不是Java类。

扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库的类加载器。扩展类库包括javax和java.util等包,它们位于jre/lib/ext目录下。

应用程序类加载器(Application Class Loader):也称为系统类加载器,它负责加载应用程序的类。它会搜索应用程序的类路径(包括用户定义的类路径和系统类路径),并加载类文件。

注解加载

问:注解是怎么加载的

答:看过源码,记得不是特别清了,spring启动类上会有enable开启注解加载,后面通过一层层的调用,大概3~4层,读取到lib目录下的文件,然后再通过条件判断是否加载。

回溯

回答一般,之前确实看过,现在确实忘了

自动配置原理:SpringBoot 项目的核心注解 @SpringBootApplication,这个注解位于启动类上方。@SpringBootApplication 看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。自动装配核心功能的实现是通过@EnableAutoConfiguration内部的AutoConfigurationImportSelector类。AutoConfigurationImportSelector 类实现了 ImportSelector 接口,也就实现了这个接口中的 selectImports 方法,该方法主要用于获取所有符合条件的类的全限定类名,需要为这些类创建对象并加载到 IoC 容器中。@ConditionOnXXX 中的所有条件都满足,该类才会生效

​ @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制。

​ @Configuration:允许在上下文中注册额外的 bean 或导入其他配置类,作用与 applicationContext.xml 的功能相同。

​ @ComponentScan: 扫描包下的类中添加了@Component (@Service,@Controller,@Repostory,@RestController)注解的类 ,并添加的到spring的容器中,可以自定义不扫描某些 bean。

image-20240329210812763

getAutoConfigurationEntry:

第 1 步:判断自动装配开关是否打开。默认 spring.boot.enableautoconfiguration = true,可在 application.properties 或 application.yml 中设置

第 2 步:用于获取 EnableAutoConfiguration 注解中的 exclude 和 excludeName。

第 3 步:从 META-INF/spring.factories 读取需要自动装配的所有配置类。

第 4 步:这一步有经历了一遍筛选过滤,@ConditionOnXXX 中的所有条件都满足,该类才会生效

https://blog.csdn.net/m0_59749089/article/details/131280769

第三个面试官

bean加载

问:如果我想再bean初始化时候改变其变量值,或者说再方法前打印一个日志。

答:使用代理,再调用方法前后打印日志。

追问:不是代理,是在初始化的时候。

答:在xml文件中,配置修改,在bean初始化的时候给其赋值。

回溯

貌似这也不是面试官想要的,确实不知道是啥,得查资料研究看看。

bean的加载过程:

加载bean信息–>实例化bean–>属性填充–>初始化阶段–>后置处理

https://blog.csdn.net/m0_46897923/article/details/129850717

容器化

问:你们容器化用的怎么样。

答:我们部门目前才开始推,我们还没有开始使用容器化,但是自己也搭环境简单使用过容器化,比如部署一个redis、mysql等。

回溯

熟悉了解一下容器化,这方面得了解确实不够,只是简单得使用了一下。

多线程

问:核心线程数10,最大线程数20,现有1w个并发,为什么线程数一直是10,不是队列里有1w。

答:不知,尝试分析,是不是资源不够,没办法新增。

回溯

确实不知,等了解。

问:synchronized的用法

答:修饰代码块或者方法,注意synchronized()中括号内的内容,可以是变量,对象和class类,对应锁的颗粒不一样。

追问:对象和class类有什么不一样。

答:对象是可以重复new的,但是class类只有一个,跟静态变量一样,用的时候要注意,别用错导致没锁好。

追问:线程的睡眠和唤醒是怎么样的。

答:A、B两个线程,A运行到某个地方,停住了睡眠了,B开始运行,运行到某个地方,通过A的方法唤醒A。

回溯

1、感觉追问的对象和类有什么不一样,没回答到他想要的,复习一下。

2、睡眠唤醒的模型有点忘了,再代码实操一下。

kafka

问:kafka里的消费者组是什么。

答:消费者的集合,一堆消费者组成的一个集合。

问:topic里的分区能被消费者组内的多个消费者消费吗

答:不能,一个分区只能被消费者组内一个消费者消费。

问:怎么保证生产者生产的消息,顺序消费。

答:都放在一个分区内,kafka不保证topic内的消息是顺序的,但是保证分区内的数据是顺序消费的。

问:消息是怎么划分分区的。

答:根据key的计算,具体的计算方法不知道。

问:如果key为null会怎么样。

答:不知

回溯

1、消费者组的概念

2、分区和消费者组、消费者的消费关系模型

3、分区的指定逻辑。

4、key为null会怎样。

随机选取一个分区缓存发送,过了分区的缓存时间后在随机选取一个分区缓存发送。

https://blog.csdn.net/yizhiniu_xuyw/article/details/109206709

现在chatgpt这么火,我们也可以通过人工智能为我们的工作提供便利。

可行性

1、代码开源,不需要冲头开发,基本拿来可用。

2、单业务领域小数据量训练的可行性,单个公司一般聚焦于一个领域业务,多领域公司可以下分到各部门。单业务领域的训练数据就会小很多,基本可以内部清洗提供。

安全性

1、内部服务器部署,机器人服务部署于内部服务器,保证数据安全性。

交互性

1、通过开放接口和外部交互,如钉钉,微信。降低交互成本,提升交互沟通效率。

优越性

1、人工智能的参与,提升员工工作效率,通过员工和人工智能沟通可以提升员工的工作效率,比如程序员可以问个代码怎么写啊类似的,不做缀叙。

2、部署在内部服务器的化,还可以通过其和内部任务交互,比如日常的一些小任务,可以程序化之后,部署服务器,暴露接口和人工智能交互,我们通过聊天软件通知智能机器人,让他去触发这些任务。

3、降低工作量的重复,不知道大家有没有遇到过这样的情况,有时候,想请同事帮忙处理一下什么问题或这任务,同事由于有之前处理的经验或者记录,一两分钟可能就处理好了,而自己从头开始处理的话可能需要一两个小时。如果我们把这种任务程序化之后,交由机器人触发,我们需要的时候只需要以聊天的形式,通知机器人触发任务,那得多爽,这样就可以提高工作效率。

4、提升人工智能的准确性,从而提高员工的效率,员工在和人工智能交互的过程也是一种训练,而且还是一种监督训练,可以提供人工智能的回答准确性,从而可以提高下次和人交互的准确性。

机会点

当此人工智能在单个领域训练的准确性很高的,可以考虑将此接口暴露出去,商业化,从而盈利。

​ 在java中,我们主要用到的锁有两个:synchronized、ReentrantLock。一个是内置锁(语言级),一个是显示锁,一般如果我们不思考只图方便的话,会直接使用synchronized锁。其实ReentrantLock更灵活多变,适用更多的场景。我们可以通过下面的表对比一下两者。

特性/维度 synchronized ReentrantLock
所属包 Java 语言层面(内置关键字) java.util.concurrent.locks
可重入性 ✅ 支持 ✅ 支持
是否必须手动解锁 ❌ 自动(方法或代码块退出时) ✅ 必须手动 unlock()
是否可中断获取锁 ❌ 不支持 ✅ 支持 lockInterruptibly()
是否支持尝试获取锁 ❌ 不支持 ✅ 支持 tryLock()(立即或超时)
是否支持公平锁 ❌ 不支持 ✅ 支持(new ReentrantLock(true)
是否支持条件队列 ❌ 不支持 ✅ 支持 Condition(类似 wait/notify
性能优化(偏向锁等) ✅ JVM 自动优化 ❌ 无(完全由开发者控制)
是否支持可见调试 ❌ 无法查看状态 ✅ 可查看是否被锁定、等待线程等
死锁处理 不易发现 可结合 tryLock() 等策略避免死锁

​ 其实在上述这些维度方面都没啥好说的,这些一般都知道。都知道synchronized是关键字而ReentrantLock是一个类,然后两者都能加锁,这锁怎么去加,有什么效果,都是知道的。

​ 那synchronized和ReentrantLock又分别是怎么加锁的呢?

synchronized

加锁原理(底层)
  • Java 对象头中包含了 Mark Word,在没有加锁时存储哈希值、GC信息等。
  • 当线程进入 synchronized 块时,会尝试修改对象头的 Mark Word,以标识当前线程持有锁。
  • JVM 使用了多种锁优化策略(偏向锁、轻量级锁、重量级锁):
    • 偏向锁:如果只有一个线程访问,尽量不加锁,提高性能。
    • 轻量级锁:使用 CAS 尝试获取锁,适用于低竞争。
    • 重量级锁(互斥锁):竞争激烈时,会挂起线程,使用 Monitor 来实现,依赖操作系统的互斥机制。
加锁过程简要流程图(逻辑)
flowchart TD
    A[线程进入 synchronized] --> B[检查对象头 Mark Word
是否偏向、锁状态] B --> C{对象是否空闲?} C -->|是| D[尝试通过 CAS 加锁 轻量级锁] C -->|否| E{CAS 加锁是否成功?} E -->|成功| F[获得锁 轻量级] E -->|失败| G[有竞争 → 升级为重量级锁
线程挂起等待]
字节码层面

编译后使用 javap -v Test.class 查看,会看到如下结构:

1
2
3
4
5
public void syncBlock() {
synchronized (this) {
System.out.println("Hello");
}
}

反编译字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
asm

0: aload_0
1: dup
2: monitorenter // 进入同步块
3: getstatic java/lang/System.out : Ljava/io/PrintStream;
6: ldc "Hello"
8: invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
11: monitorexit // 退出同步块
12: goto 20
15: astore_1
16: monitorexit // 异常时也要退出同步块
17: aload_1
18: athrow

monitorentermonitorexit 是 JVM 指令,由 JVM 解释器直接处理,底层是调用 native 方法去操作对象的 Monitor。

JVM实现细节

HotSpot 中,对象的 Monitor 实际上是 ObjectMonitor

1
2
3
4
5
6
7
class ObjectMonitor {
...
ObjectWaiter * _EntryList; // 等待锁的线程队列
Thread * _Owner; // 当前持有锁的线程
int _recursions; // 重入次数
...
};

Monitor 与每个对象的 对象头(Mark Word) 关联,当竞争严重时锁会升级:

  • 偏向锁:只记录线程 ID,适合无竞争场景
  • 轻量级锁:线程通过 CAS 抢占锁
  • 重量级锁:膨胀为 ObjectMonitor,线程挂起和唤醒都依赖 OS 的 park/unpark

ReentrantLock

加锁原理(底层)
  • ReentrantLock 是基于 AQS(AbstractQueuedSynchronizer) 实现的。
  • 调用 lock() 会尝试通过 CAS 修改一个 state 状态变量,来标识是否持有锁。
  • 若获取失败,线程会被放入 AQS 的 双向等待队列中,等待唤醒。
加锁过程简要流程图(逻辑)
flowchart TD
    A[线程调用lock] --> B{CAS 尝试将 state 从 0 改为 1}
    B -->|成功| C[获得锁]
    B -->|失败| D[进入 AQS 等待队列]
AQS的队列结构
1
2
3
head -> [Node1] -> [Node2] -> ...
^ ^
线程1 线程2(阻塞)

每个 Node 包含:

  • 等待线程 Thread thread
  • 等待状态 waitStatus
  • 前驱/后继节点 prev / next

底层通过 LockSupport.park()unpark() 来实现线程的阻塞与唤醒。

如何唤醒

​ 两者都是有阻塞队列,队列都是有序的,为什么会出现非公平唤醒呢?

​ **因为”排队” ≠ “唤醒一定按顺序”**。

Java 的锁(尤其是默认的 synchronized 和非公平 ReentrantLock)使用的阻塞队列只是阻塞等待顺序的组织方式,但唤醒后是否真正获得锁,还要竞争(CAS),所以就可能出现:

先排队的线程被唤醒了,但还没抢到锁;此时一个新线程进来,直接CAS成功拿到了锁。

1. synchronized 的非公平行为

synchronized 底层用的是 ObjectMonitor,当有多个线程竞争时,JVM 不保证 先唤醒的一定先拿到锁

1
2
3
4
// ObjectMonitor.cpp
void ObjectMonitor::exit(...) {
// 调用 _EntryList 中的某个线程 unpark(但可能不是严格FIFO)
}

唤醒后还要重新尝试获取锁(CAS),不是直接转移锁的拥有权,所以:

  • 后来的线程进来,如果没有阻塞,仍然可能直接成功拿到锁。

  • 被唤醒的线程,反而还得和新线程竞争,可能继续失败。

2. ReentrantLock 的非公平行为

默认构造的 ReentrantLock()非公平锁,非公平策略体现在 lock() 方法中:

1
2
3
4
5
6
7
final void lock() {
if (compareAndSetState(0, 1)) { // 直接抢锁,不管队列中有没有人
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1); // 抢不到才排队
}
}

也就是说:

  • 没有优先检查 AQS 队列中是否有等待的线程
  • 只要锁是空闲的,谁来谁抢,先到的不一定先拿到

即使队列中有等待线程,新线程仍然可以“插队”成功。

flowchart TD
    A[执行 SQL] --> B[EXPLAIN / EXPLAIN ANALYZE]
    B --> C{是否走索引?}
    C -- 否 --> D[检查 WHERE 条件字段 建立合适索引]
    C -- 是 --> E{扫描行数是否过多?}
    E -- 是 --> F[考虑复合索引 / 覆盖索引
优化表结构] E -- 否 --> G{是否出现 filesort/temporary?} G -- 是 --> H[优化 ORDER BY / GROUP BY
避免临时表] G -- 否 --> I{SQL 是否过于复杂?} I -- 是 --> J[拆分大 SQL
用中间表/子查询优化] I -- 否 --> K{分页或大 OFFSET?} K -- 是 --> L[使用索引+子查询优化分页
避免 LIMIT 大偏移] K -- 否 --> M[执行效率已较优] D --> B F --> B H --> B J --> B L --> B
0%