面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 01:40

什么是区块链技术?请解释其核心特点和去中心化原理

区块链是一种分布式账本技术,通过密码学将数据区块按时间顺序链式连接,实现去中心化存储与验证。核心特点:去中心化(无单点控制,全网节点共持账本)、不可篡改(每个区块头包含前一区块哈希,篡改任一区块需重算后续所有区块哈希)、透明性(交易公开可验证)、安全性(共识机制防恶意攻击)。去中心化原理:传统系统依赖中心服务器,区块链中所有节点对等存储完整账本,通过共识算法(PoW/PoS/PBFT)对新区块达成一致。公有链任何人可参与,联盟链需授权,私有链由单一组织控制。追问哈希指针如何保证区块不可篡改?如果攻击者控制了51%算力会发生什么?PoW、PoS、PBFT 三种共识机制分别适用于什么场景?它们的吞吐量和最终确认时间有何差异?默克尔树在区块链中起什么作用?轻节点如何利用默克尔证明验证交易?公有链、联盟链、私有链在去中心化程度上有什么区别?各自适合什么业务?区块链的"不可能三角"指什么?为什么无法同时满足去中心化、安全性和可扩展性?写段代码// 简化版区块链:哈希链与工作量证明class Block { constructor(data, prevHash) { this.data = data; this.prevHash = prevHash; this.nonce = 0; this.hash = this.mine(); } mine() { let h; do { this.nonce++; h = sha256(this.prevHash + this.data + this.nonce); } while (!h.startsWith('0000')); // 难度:前4位为0 return h; }}class Chain { constructor() { this.blocks = [new Block('genesis', '0')]; } add(data) { this.blocks.push(new Block(data, this.blocks.at(-1).hash)); } isValid() { return this.blocks.every((b,i) => i===0 || b.prevHash === this.blocks[i-1].hash); }}
服务端阅读 05月29日 01:40

WebGL 中的阴影(Shadow)是如何实现的?

WebGL 阴影的主流实现方式是阴影贴图(Shadow Mapping):先从光源视角渲染场景生成深度图,再从相机视角渲染时将每个片段变换到光源空间,比较其深度与深度图中的值——若片段深度更大则处于阴影中。软阴影通过 PCF(Percentage Closer Filtering)对深度图多次采样取平均实现。两大典型问题:阴影痤疮(shadow acne)由深度精度误差导致,用基于法线-光线夹角的 bias 修复;彼得潘效应(peter panning)由 bias 过大导致阴影脱离物体,改用正面剔除渲染深度图解决。大场景用级联阴影贴图(CSM)按距离分配不同分辨率。追问为什么阴影贴图会产生阴影痤疮?bias 值如何根据表面法线和光线方向动态计算?PCF 软阴影的采样半径越大越模糊,但会带来什么性能问题?Poisson Disk 采样有何优势?级联阴影贴图(CSM)如何划分级联?级联接缝处的接缝问题怎么处理?点光源阴影为什么用立方体贴图?渲染立方体阴影贴图需要几次 draw call?WebGL 1.0 没有深度纹理附件,如何用 RGBA 编码深度值?精度损失如何补偿?写段代码// PCF 软阴影核心逻辑float calcShadow(vec4 lightPos, sampler2D shadowMap, vec3 normal, vec3 lightDir) { vec3 proj = lightPos.xyz / lightPos.w * 0.5 + 0.5; float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005); float shadow = 0.0; vec2 texel = 1.0 / vec2(textureSize(shadowMap, 0)); for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) { float d = texture(shadowMap, proj.xy + vec2(x,y)*texel).r; shadow += proj.z - bias > d ? 1.0 : 0.0; } return shadow / 9.0;}
服务端阅读 05月29日 01:39

WebGL 是什么?它与 OpenGL 有什么关系?

WebGL 是基于 OpenGL ES 的浏览器端 3D 图形 API,关系链为 OpenGL → OpenGL ES → WebGL。WebGL 继承了 OpenGL ES 的可编程渲染管线,通过 GLSL 编写顶点着色器和片段着色器控制 GPU 渲染,但运行在浏览器沙箱中,用 JavaScript 调用,且去除了 OpenGL 的固定管线。核心渲染流程:顶点数据经顶点着色器变换 → 图元装配 → 光栅化 → 片段着色器着色 → 帧缓冲输出。WebGL 1.0 基于 OpenGL ES 2.0,WebGL 2.0 基于 OpenGL ES 3.0。追问WebGL 和 Canvas 2D 的本质区别是什么?Canvas 2D 能调用 GPU 吗?顶点缓冲对象(VBO)的作用是什么?为什么不用 CPU 每帧传顶点数据?WebGL 的统一变量(uniform)和属性变量(attribute)分别在什么阶段使用?WebGL 2.0 相比 1.0 新增了哪些关键能力?VAO 和 3D 纹理对开发有什么影响?浏览器如何保证 WebGL 的安全性?哪些 OpenGL ES 特性被有意移除了?写段代码// WebGL 初始化:创建着色器程序并绘制三角形const gl = canvas.getContext('webgl');const vs = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vs, 'attribute vec2 a_pos; void main(){ gl_Position=vec4(a_pos,0,1); }');gl.compileShader(vs);const fs = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fs, 'precision mediump float; void main(){ gl_FragColor=vec4(1,0,0,1); }');gl.compileShader(fs);const prog = gl.createProgram();gl.attachShader(prog, vs); gl.attachShader(prog, fs);gl.linkProgram(prog); gl.useProgram(prog);const buf = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, buf);gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0.5, -0.5,-0.5, 0.5,-0.5]), gl.STATIC_DRAW);const loc = gl.getAttribLocation(prog, 'a_pos');gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);gl.drawArrays(gl.TRIANGLES, 0, 3);
服务端阅读 05月29日 01:38

DNS 是什么?域名解析的递归查询和迭代查询有什么区别?

DNS(Domain Name System)是将域名映射为 IP 地址的分布式数据库系统,是互联网基础设施的核心组件。当浏览器访问域名时,解析流程为:浏览器缓存 → 系统缓存(hosts)→ 本地 DNS 服务器 → 根域名服务器 → 顶级域服务器 → 权威 DNS 服务器,逐级查询直到获得最终 IP。其中客户端到本地 DNS 是递归查询(服务器代为完成全部查询),本地 DNS 到根/TLD/权威服务器是迭代查询(每级只返回下一级地址,由本地 DNS 继续追问)。追问常见的 DNS 记录类型有哪些?A 记录映射域名到 IPv4,AAAA 到 IPv6,CNAME 将域名指向另一个域名(别名),MX 指定邮件服务器,NS 指定域名服务器,TXT 用于验证和配置(如 SPF、DKIM)。TTL 在 DNS 中起什么作用?TTL(Time To Live)决定解析结果的缓存有效期。短 TTL 可加快 DNS 变更生效(如故障切换),但增加查询量;长 TTL 减少查询但变更传播慢。CDN 切换节点时需要权衡 TTL 设置。DNS 劫持是什么?DNSSEC 如何防护?DNS 劫持是中间人篡改 DNS 响应返回恶意 IP。DNSSEC 通过对 DNS 响应数字签名验证数据来源和完整性,客户端可验证响应未被篡改。CDN 如何利用 CNAME 实现就近访问?用户域名 CNAME 到 CDN 域名后,CDN 的权威 DNS 根据用户 IP 返回最近的边缘节点 IP。部分服务商采用 CNAME Flattening,在权威 DNS 层直接返回 A 记录,避免 CNAME 链过长影响性能。写段代码# 常用 DNS 查询命令dig example.com A # 查 A 记录dig example.com CNAME # 查 CNAMEnslookup example.com # 简单查询host -t MX example.com # 查邮件记录
服务端阅读 05月29日 01:38

Zustand 中间件怎么使用?有哪些内置中间件?

Zustand 中间件以函数组合方式包裹 create 的回调,从内到外依次嵌套。内置三个核心中间件:persist(状态持久化到 localStorage/sessionStorage)、devtools(接入 Redux DevTools 调试)、immer(简化不可变更新,可直接写 state.user.name = 'new')。组合顺序:devtools 在最外层,persist 在内层,中间件顺序影响 set/get 的拦截链。追问persist 的 partialize 怎么过滤不需要持久化的字段?partialize: (state) => ({ user: state.user }) 只持久化 user,token 等敏感或临时字段不会写入存储。反序列化时缺失字段会使用 create 中的初始值填充。immer 中间件解决了什么问题?React 要求状态不可变更新,深层嵌套需逐层展开 {…s, user: {…s.user, name: 'new'}},代码冗长易出错。immer 让你直接赋值 state.user.name = 'new',内部通过 Proxy 生成新对象。中间件组合顺序有影响吗?有。devtools(persist(fn)) 中 persist 在内层,持久化后的状态变化才会被 devtools 捕获;反过来的话 devtools 记录的是持久化前。一般推荐 devtools 在外、persist 在内。如何自定义中间件?中间件本质是高阶函数:(config) => (set, get, api) => config(fnSet, fnGet, api),在 fnSet/fnGet 中插入自定义逻辑(如日志、节流、权限校验),然后调用原 set/get。persist 持久化的状态怎么版本迁移?persist 配置中提供 migrate 函数:migrate: (persisted, version) => { if (version === 0) return { …persisted, newField: 'default' }; return persisted; },配合 version 字段标识当前版本,自动执行迁移。写段代码import { create } from 'zustand'import { persist, devtools } from 'zustand/middleware'const useStore = create( devtools( persist( (set) => ({ token: '', setToken: (t: string) => set({ token: t }), }), { name: 'auth', partialize: (s) => ({ token: s.token }) } ), { name: 'AuthStore' } ))
服务端阅读 05月29日 01:38

Babel 是什么?它的编译流程和 polyfill 方案有哪些?

Babel 是 JavaScript 转译器,将 ES6+ 语法转为向后兼容的 ES5 代码,确保在旧浏览器中正常运行。编译流程分三步:parse(@babel/parser 将源码转 AST)→ transform(插件遍历 AST 并修改节点)→ generate(@babel/generator 将 AST 还原为代码)。注意 Babel 只转换语法,不补齐新 API(如 Promise、Array.includes),需要 polyfill 方案配合。追问Babel 的 plugin 和 preset 是什么关系?plugin 是最小转换单元(如 @babel/plugin-transform-arrow-functions),preset 是插件集合(如 @babel/preset-env 包含所有 ES6+ 转换插件)。执行顺序:plugins 先于 presets,plugins 正序执行,presets 逆序执行。@babel/preset-env 的工作原理是什么?根据 browserslist 配置的目标环境,按需引入转换插件和 polyfill,避免对已支持语法的多余转换。配合 useBuiltIns: 'usage' 可实现按需注入 polyfill。polyfill 方案有哪些?core-js 和 transform-runtime 有什么区别?core-js 直接在全局注入缺失 API,适合应用项目;@babel/plugin-transform-runtime 将 polyfill 抽离为模块引用(不污染全局),适合库开发,还能避免 helpers 重复打包。Babel 能替代 Webpack 吗?不能。Babel 只做语法转译,不做模块打包、代码分割、Tree Shaking、资源处理,这些是 bundler 的职责。两者配合使用。写段代码// babel.config.jsmodule.exports = { presets: [ ['@babel/preset-env', { targets: '> 0.25%, not dead', useBuiltIns: 'usage', corejs: 3 }] ], plugins: ['@babel/plugin-transform-runtime']};
服务端阅读 05月29日 01:38

Spring Boot 中如何实现异步编程?

Spring Boot 通过 @EnableAsync + @Async 实现声明式异步编程。在配置类上标注 @EnableAsync 开启支持,在方法上标注 @Async 即可在独立线程执行;默认使用 SimpleAsyncTaskExecutor(每次新建线程),生产环境应自定义 ThreadPoolTaskExecutor 并通过 @Async("executorName") 指定线程池。有返回值的方法返回 CompletableFuture,调用方可通过 future.get() 或 CompletableFuture.allOf() 组合多个异步结果。异常处理方面,返回 CompletableFuture 的方法异常会传播到 future,void 方法需实现 AsyncUncaughtExceptionHandler。注意同类内部调用 @Async 方法不生效(绕过代理)。追问ThreadPoolTaskExecutor 的核心参数如何设置? IO 密集型核心线程数可设为 CPU 核数 * 2,队列容量适当放大;CPU 密集型核心线程数为 CPU 核数 + 1,队列不宜过大;拒绝策略推荐 CallerRunsPolicy 由提交线程执行降速。@Async 方法为什么同类调用不生效?如何解决? @Async 依赖 AOP 代理,this.method() 绕过代理直接调用原始方法;解决方案是注入自身代理 @Lazy private XxxService self 后通过 self.method() 调用。CompletableFuture 的 thenCombine 和 thenCompose 有何区别? thenCombine 将两个独立异步结果合并(BiFunction),thenCompose 用于异步结果串联(flatMap 语义,前一步结果决定后一步操作)。@Async 和 @Transactional 一起使用有什么陷阱? 事务上下文绑定 ThreadLocal,异步方法在新线程执行导致事务不传播;应在异步方法内部调用另一个 Bean 的事务方法,而非叠加注解。如何将 RequestContext 传播到异步线程? 使用 TaskDecorator 在任务提交时捕获主线程的 RequestAttributes,在异步线程执行前设置,执行后清除。写段代码@EnableAsync@Configurationpublic class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); ex.setMaxPoolSize(20); ex.setQueueCapacity(100); ex.setThreadNamePrefix("async-"); ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); ex.initialize(); return ex; }}
服务端阅读 05月29日 01:38

Zustand 中如何用 TypeScript 定义 Store 类型?

定义一个包含状态和方法的 interface,作为 create 的泛型参数传入即可:create((set, get) => ({…}))。TypeScript 会自动推断 set 回调中 state 的类型,方法参数也能正确约束。关键点:状态和方法写在同一个 interface 中,方法参数可引用自身类型(如 setUser: (user: StoreState['user']) => void)。追问中间件会改变 store 类型,怎么处理类型组合?persist/devtools 等中间件会向 store 注入额外属性(如 persist.clearStorage),需定义扩展类型 type Store = StoreState & StorePersist,再用 create() 包裹。zustand v4+ 的中间件泛型签名已支持自动推断,大部分场景无需手动拼接。StateCreator 泛型怎么用?StateCreator 用于拆分 store 逻辑时约束每个切片的类型,Mutators 描述中间件链对 set/get 的改写,U 是未包装的原始类型。手动组合切片时需要用到。selector 的返回类型怎么保证?useStore(s => s.count) 自动推断为 number。自定义 selector 返回派生值时,TypeScript 能推断返回类型;如果需要显式标注,写 useStore((s) => s.items.length)。泛型 store 怎么定义?function createSlice() 返回 StateCreator>,通过泛型参数让切片适配不同实体(如 createSlice()、createSlice()),避免为每个实体写重复逻辑。set 的 partial 参数类型为什么不是 Partial?set 接受 Partial 或 (state) => Partial,但嵌套对象是浅合并,深层更新需展开:set(s => ({ user: { …s.user, name: 'new' } })),否则丢失同级字段。写段代码interface BearState { bears: number inc: (n: number) => void}const useBearStore = create<BearState>((set) => ({ bears: 0, inc: (n) => set((s) => ({ bears: s.bears + n })),}))// 组件中const bears = useBearStore((s) => s.bears) // number