写点什么

真的要动起来了

作者:王中阳Go
  • 2025-06-25
    北京
  • 本文字数:5598 字

    阅读完需:约 18 分钟

真的要动起来了

先来唠唠

今晚和学员开完最后一个会议后,带着一个 20 多岁的同事去锻炼,据他所说他已经有一年多没有专门花时间去运动过了,现在比之前胖了 20 多斤


跑了二十多分钟,气喘吁吁,满头大汗,这样可不行啊,身体素质太差了,所以我决定以后只要工作不多,就带着他去运动。


在看这篇文章的你们想必大多数是程序员,工作的时候一直坐着,我建议你们平时工作和学习无论有多忙,都要花一点时间去锻炼,赚钱固然重要,但身体永远是革命的本钱


运动和学习在一个方面是一样的,那就是要坚持,如果你也和我这个同事一样甚至更严重,一定要早点动起来!


话就唠到这,重要的还是下面的 Java 面经

面经分享

守护线程在扫描表中状态为 NEW 的记录进行处理的时候,如果有多个节点多个守护线程都同时扫描表中的记录,如何避免并发操作,一条表记录怎么避免被重复处理了?

示例回答:


在实际分布式系统中很常见,核心思路就是让多个节点或线程在处理同一条记录时能够“互斥”操作。举个例子,我们可以用数据库的行级锁机制,比如在查询的时候用SELECT FOR UPDATE SKIP LOCKED这样的语句(Oracle 或 PostgreSQL),或者 SQL Server 里的WITH (ROWLOCK, UPDLOCK, READPAST)。这样执行的时候,数据库会直接把符合条件的记录锁住,其他线程来查的时候会发现这条记录已经被锁了,直接跳过,这样就避免了多个线程同时捞到同一条数据的情况。


还有一种常见做法是加版本号乐观锁。比如表里加个 version 字段,处理的时候先查当前的版本号,更新的时候必须匹配这个版本号才能成功。比如执行UPDATE table SET status='PROCESSING', version=version+1 WHERE id=123 AND version=5,如果其他线程已经更新了这条数据,版本号变了,这条 SQL 就不会生效,这样后面来的线程就知道这条记录已经被处理过了。


如果是跨多个服务节点的情况,可以结合分布式锁,比如用 Redis 或者 Zookeeper。比如处理前先用 Redis 的 setnx 命令抢锁,只有拿到锁的节点才能处理这条记录,处理完再释放锁。不过这里要注意锁的过期时间和续期问题,防止死锁或者锁提前释放导致的问题。


另外在设计表的时候,可以通过状态机的约束来规避重复处理。比如规定状态只能从 NEW 到 PROCESSING 再到 COMPLETED,绝对不能回退。这样即使有并发,数据库的唯一性约束和状态流转条件会直接拦截非法操作。实际项目中通常会混合使用这些方案,比如先用分布式锁控制节点间的竞争,再用数据库行锁确保单节点内多线程的互斥,最后用版本号或状态机做最终一致性校验。

当前节点扫描到这些记录然后 hash 取余后,当前节点怎么知道自己是几号节点呢

这个问题在实际分布式系统中通常通过节点身份标识机制来解决。举个具体例子:假设用 Kubernetes 部署,每个 Pod 的名字会带有序号(比如app-0app-1),节点启动时解析自己的 Pod 名就能知道自己是 0 号还是 1 号节点。再比如用 Zookeeper 的话,节点启动时会向注册中心申请一个唯一 ID,类似排队领号,系统自动分配不重复的编号,节点拿到后存到本地文件,后续重启直接复用这个 ID,避免冲突。还有些系统会根据节点 IP 算哈希值再对总节点数取余,但这种要确保 IP 池和节点数匹配,否则可能有哈希碰撞的问题。总之核心就是通过环境信息、动态注册或配置让节点明确自己的“身份”,这样才能正确参与哈希分片。

两个事务同时对一条数据库记录比如是 id 为 1,金额为 100 的记录进行更新,都是对该条记录加 10 块钱,如何保证最终得到的结果是 120 块?

示例回答:


假设数据库里 id 为 1 的记录金额是 100 块,这时候两个事务同时过来都要加 10 块。如果直接用普通的 update 语句,比如update account set money=110 where id=1,这时候第二个事务也会执行同样的操作,最后结果可能只变成 110,而不是预期的 120。这是因为两个事务都读取到了初始值 100,各自加了 10 之后直接覆盖写入,导致第二个事务的+10 被覆盖了。


要保证最终是 120,有这几个常用方法


  1. 用数据库的原子操作:直接把加减操作写在 SQL 里,比如update account set money=money+10 where id=1。这时候数据库内部会给这条记录加锁,第一个事务执行时会锁定记录,把 100 变成 110,第二个事务必须等第一个提交后才能执行,这时候它会读取到 110,再+10 变成 120。这个是最推荐的做法,因为数据库自己就能处理并发。

  2. 加行级锁:在事务里先用select for update锁住这条记录。比如第一个事务先执行select * from account where id=1 for update,这时候第二个事务想执行同样的 select 就会被卡住,直到第一个事务提交。这样第一个事务把 100 变成 110 后,第二个事务才能读到 110 继续加 10。这种方法适合需要复杂计算的场景,但锁的开销稍大。

  3. 版本号控制:给表加个 version 字段。比如第一个事务读取到 version 是 1,更新时执行update account set money=110, version=2 where id=1 and version=1。如果第二个事务也读取到 version 是 1,等它提交时发现 version 已经变成 2 了,更新就会失败。这时候需要让第二个事务重新读取新值 110,再执行update ... money=120, version=3。这个方法不需要锁,但需要代码里处理重试逻辑。


优先用原子操作的 update 语句,让数据库处理并发;如果业务逻辑复杂不能用原子操作,再用行锁或版本号控制。

websocket 的底层原理,服务端是如何经过层层链路找到客户端的?

示例回答:


WebSocket 服务端找到客户端的核心原理,其实可以理解为“靠 TCP 连接的四元组定位+应用层会话管理”。:


  1. 初次握手建立连接客户端(比如浏览器)先发一个 HTTP 请求,带Upgrade: websocket头,相当于敲门说:“我要升级成 WebSocket 协议”。这时候服务端会根据请求里的Sec-WebSocket-Key生成响应,同意升级。这个过程就像交换接头暗号,确认双方都支持 WebSocket。

  2. TCP 通道绑定握手成功后,底层 TCP 连接不会关闭。服务端内核会记录这个连接的四元组信息:客户端 IP、客户端端口、服务端 IP、服务端端口。比如客户端是192.168.1.10:54321,服务端是10.0.0.1:8080,这个组合唯一标识了一个连接。

  3. 会话 ID 管理服务端在应用层会给每个连接分配一个 Session ID(比如 Java 的javax.websocket.Session)。比如张三的浏览器连接进来,服务端生成session_001,李四进来生成session_002。后续发消息时,服务端只要查这个 ID 就知道发给谁。

  4. 路由过程简化版当服务端要给张三发消息时:

  5. 从内存中找到session_001对应的 Socket 对象

  6. 通过这个 Socket 绑定的 TCP 连接(四元组已确定)

  7. 操作系统根据目标 IP 和端口,经过网卡、路由器层层转发,最终到达张三的浏览器


关键点:


  • 整个过程依赖 TCP/IP 协议栈的路由能力,服务端不需要关心中间经过多少交换机

  • 如果客户端在 NAT 后(比如手机 4G 网络),公网 IP 其实是运营商网关的,但 TCP 端口映射由 NAT 设备自动维护

  • 长连接断开后(比如网络波动),需要客户端重新握手建立新连接,这时会产生新 Session ID


比如用 Node.js 实现时,每来一个新连接就会创建一个WebSocket对象,这个对象里存了客户端连接的所有信息,服务端直接操作这个对象就能找到对应的客户端。

ssh 加密原理,公钥加密,私钥解密,公钥比较短,私钥比较长,公钥和私钥为什么可以相互解密,这是什么原理?公钥和私钥的算法有了解过嘛?为什么两个互不相干的密钥可以相互解密?

示例回答:

ssh 加密原理

关于 SSH 的加密原理,咱们可以这么理解——这其实是一套“非对称加密”的玩法。举个生活中的例子,就像你有个带锁的箱子,公钥相当于谁都能拿到的锁,私钥就是你兜里藏的钥匙。比如 A 要给 B 传秘密文件,A 用 B 给的锁(公钥)把箱子锁上,这时候只有 B 用自己的钥匙(私钥)才能打开。


公钥短私钥长的问题,其实是个观察偏差。实际生成时两者长度是一样的(比如 2048 位),只是存储格式不同。比如公钥可能只显示"ssh-rsa AAAA..."这种 BASE64 编码,而私钥文件会多存生成时用的质数参数,所以看起来更长


这种机制能防中间人攻击的关键在于:服务器第一次连接时会让你核对公钥指纹(像快递单号一样),确认后再把公钥存到 known_hosts 里。下次连接如果指纹对不上就会报警,防止有人冒充服务器。整个过程就像你第一次收快递要核对快递员工牌,以后认脸就行了。

公钥和私钥的算法

公钥和私钥的算法主要有这么几种,先说最常用的:


第一种是 RSA,它基于大数分解难题,简单说就是用两个超大质数相乘生成密钥,但反过来分解这个乘积非常困难。比如你用 SSH 登录服务器或者访问 HTTPS 网站,背后基本都是 RSA 在起作用。现在主流的密钥长度是 2048 位,安全性和性能比较平衡。


第二种是 DSA(数字签名算法),专门用来做签名的,比如验证文件完整性。它基于离散对数问题,但和 RSA 不同的是它不支持加密只用于签名。早期 SSH 可能会用它,但现在逐渐被更高效的算法取代了。


第三种是 ECC 椭圆曲线加密(比如 ECDSA),这两年越来越流行。它的特点是能用更短的密钥实现和 RSA 同等的安全性。比如 256 位的椭圆曲线密钥安全强度相当于 RSA 3072 位,特别适合手机、物联网这些资源有限的设备。


还有现在推荐的新算法 Ed25519,属于椭圆曲线家族的一员。比如你用ssh-keygen生成密钥时选这个类型,生成的密钥比 RSA 短但安全性更高,运算速度还快,GitHub 现在都推荐用它替代 RSA 了。


其他像 ElGamal、Diffie-Hellman 这些算法虽然也能生成密钥对,但更多用在密钥交换环节而不是直接加密。比如 HTTPS 握手时用的临时密钥交换,可能就会用到 Diffie-Hellman 的变种。


实际应用中,像 SSH 登录常用 RSA 或 Ed25519,区块链的钱包地址多用椭圆曲线,而 TLS 协议里 RSA 和 ECC 都会出现。

为什么两个互不相干的密钥可以相互解密?

这个问题其实是非对称加密的核心设计精妙之处。我们可以用快递柜的例子来理解:假设你有一个带两个钥匙的柜子,一把是公共寄存钥匙(公钥),谁都可以用这把钥匙把东西锁进柜子;另一把是私人取件钥匙(私钥),只有你自己能打开柜子取东西。这两个钥匙看起来互不相干,但其实是根据同一个数学“模具”制造出来的。


具体来说,像 RSA 这样的算法是基于大质数分解难题的。举个简化例子:选两个超大的质数 p=61 和 q=53,算出 N=3233 作为公钥的一部分。这时候私钥其实是(p,q)的组合,而公钥是(N, 某个计算出来的指数 e)。加密时用 N 和 e 做数学运算,解密必须知道 p 和 q 才能快速计算。这两个密钥就像数学上的“阴阳两极”——虽然看起来不同,但通过质数分解形成了强关联性。


实际应用中,当用公钥加密数据时,相当于把信息转换成只有对应私钥才能解开的数学谜题。比如用公钥加密"123",实际是计算 123^e mod N 这样的复杂运算,而解密需要私钥参数 d 来计算(密文)^d mod N,只有知道 p 和 q 才能快速算出 d。这种设计下,即使黑客拿到公钥和密文,想暴力破解也需要数百年时间。


反过来用私钥加密(比如数字签名),公钥能解密验证,是因为签名过程其实是私钥持有者对信息做特定数学变换,而公钥能验证这个变换是否匹配。就像盖了防伪印章的文件,大家用公钥这个"验钞机"就能确认真伪]。这种双向可逆性,本质都是基于同一个数学难题的正向/逆向计算复杂度差异。

拦截器有没有用过?拦截器和过滤器的区别

示例回答:

1. 所属框架不同
  • 过滤器(Filter):属于 Servlet 规范,由 Web 容器(如 Tomcat)管理,独立于 Spring 框架。

  • 拦截器(Interceptor):属于 Spring MVC 框架,依赖 Spring 容器,只能拦截 Spring 管理的请求。

2. 拦截范围不同
  • 过滤器:可拦截所有 Web 请求(包括静态资源、Servlet 等),范围更广泛。

  • 拦截器:仅拦截 Spring MVC 的 Controller 方法,无法拦截静态资源请求。

3. 执行时机不同
  • 过滤器:在请求进入 Servlet 之前执行,属于 Web 容器的最前端拦截。

  • 拦截器:在 Spring MVC 的处理流程中执行,分为预处理(Controller 前)、后处理(Controller 后)、完成后三个阶段。

4. 对 Spring Bean 的访问能力
  • 过滤器:无法直接访问 Spring 容器中的 Bean,仅能处理基础请求 / 响应参数。

  • 拦截器:可注入 Spring Bean(如 Service、Repository),用于业务逻辑判断(如权限校验)。

总结

过滤器是 Web 容器级别的通用拦截组件,适合处理全局请求(如编码、日志);拦截器是 Spring 框架内的业务拦截组件,适合处理与 Spring 业务逻辑相关的拦截(如权限、用户状态校验)。

如何自定义注解?

示例回答:

一、注解的本质与元注解
  1. 本质:注解是一种特殊接口,通过@interface声明,编译后生成java.lang.annotation.Annotation的实现类。

  2. 元注解(控制注解行为):

  3. @Retention:指定注解生命周期(RUNTIME/CLASS/SOURCE),运行时反射需用RUNTIME

  4. @Target:指定可应用位置(类 / 方法 / 字段等,如ElementType.METHOD)。

二、定义注解的核心要素
  1. 属性声明:

  2. 以无参数方法形式定义,支持基本类型、String、Class、枚举、数组及其他注解。

  3. 示例:String value() default "";(建议设默认值,避免强制赋值)。

  4. 特殊规则:

  5. 若属性名为value且唯一,使用时可省略属性名(如@MyAnno("参数"))。

三、注解的使用与解析
  1. 使用方式:

  2. 直接标注在类、方法、字段上(如@LogOperation("查询数据"))。

  3. 解析方式:

  4. 运行时:通过反射getAnnotation()获取注解信息(需RetentionPolicy.RUNTIME)。

  5. 编译时:通过Annotation Processor处理(如 Lombok)。

四、典型应用场景
  1. 业务逻辑抽象:日志记录、权限校验(结合 AOP 实现切面逻辑)。

  2. 参数校验:替代手动校验(如@NotNull@Range)。

  3. 框架配置:替代 XML(如 Spring 的@Service@RequestMapping)。

  4. 代码生成:编译期自动生成辅助代码(如@Data生成 getter/setter)。

五、关键注意事项
  1. 生命周期选择:仅运行时需要反射解析的注解必须声明@Retention(RUNTIME)

  2. 属性设计:避免复杂类型,优先使用基础类型和字符串。

  3. 与 AOP 结合:注解常作为 AOP 切面的切入点(如@Pointcut("@annotation(LogOperation)"))。

总结流程

定义注解:元注解声明规则 → 属性设计(含默认值)→ 标注使用 → 反射 / AOP 解析处理。核心价值:将重复逻辑抽象为标签,实现代码解耦与约定大于配置。

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

发布于: 刚刚阅读数: 2
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
真的要动起来了_程序员_王中阳Go_InfoQ写作社区