真的要动起来了

先来唠唠

今晚和学员开完最后一个会议后,带着一个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. 路由过程简化版当服务端要给张三发消息时:
    • 从内存中找到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 业务逻辑相关的拦截(如权限、用户状态校验)。

如何自定义注解?

示例回答:

一、注解的本质与元注解
  1. 本质:注解是一种特殊接口,通过@interface声明,编译后生成java.lang.annotation.Annotation的实现类。
  2. 元注解(控制注解行为):
    • @Retention:指定注解生命周期(RUNTIME/CLASS/SOURCE),运行时反射需用RUNTIME
    • @Target:指定可应用位置(类 / 方法 / 字段等,如ElementType.METHOD)。
二、定义注解的核心要素
  1. 属性声明:
    • 以无参数方法形式定义,支持基本类型、String、Class、枚举、数组及其他注解。
    • 示例:String value() default "";(建议设默认值,避免强制赋值)。
  2. 特殊规则:
    • 若属性名为value且唯一,使用时可省略属性名(如@MyAnno("参数"))。
三、注解的使用与解析
  1. 使用方式:
    • 直接标注在类、方法、字段上(如@LogOperation("查询数据"))。
  2. 解析方式:
    • 运行时:通过反射getAnnotation()获取注解信息(需RetentionPolicy.RUNTIME)。
    • 编译时:通过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 解析处理。
核心价值:将重复逻辑抽象为标签,实现代码解耦与约定大于配置。

欢迎关注 ❤

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值