先来唠唠
今晚和学员开完最后一个会议后,带着一个20多岁的同事去锻炼,据他所说他已经有一年多没有专门花时间去运动过了,现在比之前胖了20多斤。
跑了二十多分钟,气喘吁吁,满头大汗,这样可不行啊,身体素质太差了,所以我决定以后只要工作不多,就带着他去运动。
在看这篇文章的你们想必大多数是程序员,工作的时候一直坐着,我建议你们平时工作和学习无论有多忙,都要花一点时间去锻炼,赚钱固然重要,但身体永远是革命的本钱。
运动和学习在一个方面是一样的,那就是要坚持,如果你也和我这个同事一样甚至更严重,一定要早点动起来!
话就唠到这,重要的还是下面的Java面经:
示例回答:
在实际分布式系统中很常见,核心思路就是让多个节点或线程在处理同一条记录时能够“互斥”操作。举个例子,我们可以用数据库的行级锁机制,比如在查询的时候用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,绝对不能回退。这样即使有并发,数据库的唯一性约束和状态流转条件会直接拦截非法操作。实际项目中通常会混合使用这些方案,比如先用分布式锁控制节点间的竞争,再用数据库行锁确保单节点内多线程的互斥,最后用版本号或状态机做最终一致性校验。
这个问题在实际分布式系统中通常通过节点身份标识机制来解决。举个具体例子:假设用Kubernetes部署,每个Pod的名字会带有序号(比如app-0、app-1),节点启动时解析自己的Pod名就能知道自己是0号还是1号节点。再比如用Zookeeper的话,节点启动时会向注册中心申请一个唯一ID,类似排队领号,系统自动分配不重复的编号,节点拿到后存到本地文件,后续重启直接复用这个ID,避免冲突。还有些系统会根据节点IP算哈希值再对总节点数取余,但这种要确保IP池和节点数匹配,否则可能有哈希碰撞的问题。总之核心就是通过环境信息、动态注册或配置让节点明确自己的“身份”,这样才能正确参与哈希分片。
示例回答:
假设数据库里id为1的记录金额是100块,这时候两个事务同时过来都要加10块。如果直接用普通的update语句,比如update account set money=110 where id=1,这时候第二个事务也会执行同样的操作,最后结果可能只变成110,而不是预期的120。这是因为两个事务都读取到了初始值100,各自加了10之后直接覆盖写入,导致第二个事务的+10被覆盖了。
要保证最终是120,有这几个常用方法:
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。这种方法适合需要复杂计算的场景,但锁的开销稍大。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服务端找到客户端的核心原理,其实可以理解为“靠TCP连接的四元组定位+应用层会话管理”。:
Upgrade: websocket头,相当于敲门说:“我要升级成WebSocket协议”。这时候服务端会根据请求里的Sec-WebSocket-Key生成响应,同意升级。这个过程就像交换接头暗号,确认双方都支持WebSocket。192.168.1.10:54321,服务端是10.0.0.1:8080,这个组合唯一标识了一个连接。javax.websocket.Session)。比如张三的浏览器连接进来,服务端生成session_001,李四进来生成session_002。后续发消息时,服务端只要查这个ID就知道发给谁。session_001对应的Socket对象关键点:
比如用Node.js实现时,每来一个新连接就会创建一个WebSocket对象,这个对象里存了客户端连接的所有信息,服务端直接操作这个对象就能找到对应的客户端。
示例回答:
关于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。这种设计下,即使黑客拿到公钥和密文,想暴力破解也需要数百年时间。
反过来用私钥加密(比如数字签名),公钥能解密验证,是因为签名过程其实是私钥持有者对信息做特定数学变换,而公钥能验证这个变换是否匹配。就像盖了防伪印章的文件,大家用公钥这个"验钞机"就能确认真伪]。这种双向可逆性,本质都是基于同一个数学难题的正向/逆向计算复杂度差异。
示例回答:
过滤器是 Web 容器级别的通用拦截组件,适合处理全局请求(如编码、日志);拦截器是 Spring 框架内的业务拦截组件,适合处理与 Spring 业务逻辑相关的拦截(如权限、用户状态校验)。
示例回答:
@interface声明,编译后生成java.lang.annotation.Annotation的实现类。@Retention:指定注解生命周期(RUNTIME/CLASS/SOURCE),运行时反射需用RUNTIME。@Target:指定可应用位置(类 / 方法 / 字段等,如ElementType.METHOD)。String value() default "";(建议设默认值,避免强制赋值)。value且唯一,使用时可省略属性名(如@MyAnno("参数"))。@LogOperation("查询数据"))。getAnnotation()获取注解信息(需RetentionPolicy.RUNTIME)。Annotation Processor处理(如 Lombok)。@NotNull、@Range)。@Service、@RequestMapping)。@Data生成 getter/setter)。@Retention(RUNTIME)。@Pointcut("@annotation(LogOperation)"))。定义注解:元注解声明规则 → 属性设计(含默认值)→ 标注使用 → 反射 / AOP 解析处理。 核心价值:将重复逻辑抽象为标签,实现代码解耦与约定大于配置。