真的要动起来了

先来唠唠
今晚和学员开完最后一个会议后,带着一个 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-0
、app-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,有这几个常用方法:
用数据库的原子操作:直接把加减操作写在 SQL 里,比如
update account set money=money+10 where id=1
。这时候数据库内部会给这条记录加锁,第一个事务执行时会锁定记录,把 100 变成 110,第二个事务必须等第一个提交后才能执行,这时候它会读取到 110,再+10 变成 120。这个是最推荐的做法,因为数据库自己就能处理并发。加行级锁:在事务里先用
select for update
锁住这条记录。比如第一个事务先执行select * from account where id=1 for update
,这时候第二个事务想执行同样的 select 就会被卡住,直到第一个事务提交。这样第一个事务把 100 变成 110 后,第二个事务才能读到 110 继续加 10。这种方法适合需要复杂计算的场景,但锁的开销稍大。版本号控制:给表加个 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 连接的四元组定位+应用层会话管理”。:
初次握手建立连接客户端(比如浏览器)先发一个 HTTP 请求,带
Upgrade: websocket
头,相当于敲门说:“我要升级成 WebSocket 协议”。这时候服务端会根据请求里的Sec-WebSocket-Key
生成响应,同意升级。这个过程就像交换接头暗号,确认双方都支持 WebSocket。TCP 通道绑定握手成功后,底层 TCP 连接不会关闭。服务端内核会记录这个连接的四元组信息:客户端 IP、客户端端口、服务端 IP、服务端端口。比如客户端是
192.168.1.10:54321
,服务端是10.0.0.1:8080
,这个组合唯一标识了一个连接。会话 ID 管理服务端在应用层会给每个连接分配一个 Session ID(比如 Java 的
javax.websocket.Session
)。比如张三的浏览器连接进来,服务端生成session_001
,李四进来生成session_002
。后续发消息时,服务端只要查这个 ID 就知道发给谁。路由过程简化版当服务端要给张三发消息时:
从内存中找到
session_001
对应的 Socket 对象通过这个 Socket 绑定的 TCP 连接(四元组已确定)
操作系统根据目标 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 业务逻辑相关的拦截(如权限、用户状态校验)。
如何自定义注解?
示例回答:
一、注解的本质与元注解
本质:注解是一种特殊接口,通过
@interface
声明,编译后生成java.lang.annotation.Annotation
的实现类。元注解(控制注解行为):
@Retention
:指定注解生命周期(RUNTIME
/CLASS
/SOURCE
),运行时反射需用RUNTIME
。@Target
:指定可应用位置(类 / 方法 / 字段等,如ElementType.METHOD
)。
二、定义注解的核心要素
属性声明:
以无参数方法形式定义,支持基本类型、String、Class、枚举、数组及其他注解。
示例:
String value() default "";
(建议设默认值,避免强制赋值)。特殊规则:
若属性名为
value
且唯一,使用时可省略属性名(如@MyAnno("参数")
)。
三、注解的使用与解析
使用方式:
直接标注在类、方法、字段上(如
@LogOperation("查询数据")
)。解析方式:
运行时:通过反射
getAnnotation()
获取注解信息(需RetentionPolicy.RUNTIME
)。编译时:通过
Annotation Processor
处理(如 Lombok)。
四、典型应用场景
业务逻辑抽象:日志记录、权限校验(结合 AOP 实现切面逻辑)。
参数校验:替代手动校验(如
@NotNull
、@Range
)。框架配置:替代 XML(如 Spring 的
@Service
、@RequestMapping
)。代码生成:编译期自动生成辅助代码(如
@Data
生成 getter/setter)。
五、关键注意事项
生命周期选择:仅运行时需要反射解析的注解必须声明
@Retention(RUNTIME)
。属性设计:避免复杂类型,优先使用基础类型和字符串。
与 AOP 结合:注解常作为 AOP 切面的切入点(如
@Pointcut("@annotation(LogOperation)")
)。
总结流程
定义注解:元注解声明规则 → 属性设计(含默认值)→ 标注使用 → 反射 / AOP 解析处理。核心价值:将重复逻辑抽象为标签,实现代码解耦与约定大于配置。
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/30eb3bd47c6f423eb341d3170】。文章转载请联系作者。
评论