绩效除了影响年终奖之外,也会影响涨薪的幅度,今年蚂蚁各个绩效年终奖和普调的情况如下:
绩效 3.5 普遍年终奖是 2.5-3.5 个月工资区间,当然好的部门会偏高一些,比如国际业务和数字支付部门不少同学在 3 个月以上,而财保、支付宝、CTO部门有不少人卡在了 2.5 个月左右,最低的也有 2 个月的。涨薪方面除非比较好的部门有机会调薪,普遍是没有涨幅。 绩效 3.5+ 年终奖是 3.5-5.5 个月工资区,涨薪4%-7%左右,高职级的有 3000 左右的期权。 绩效 3.75 绩效年终奖高达 4.5-6 个月工资区间,部分人拿到 10%-18%左右的薪资涨幅,并且还可以拿到 4000 左右的期权,核心业务可能会更高。 绩效 4,属于优秀中的优秀的,这部分人群较少,数据样本少,无数据去参考,但是肯定有想象空间的 整体来说,跟去年差距不大,细节上有微调,蚂蚁今年 2 月份薪酬结构也有变化,原本是 16 薪,然后把其中 1 薪平摊到了每个月 base,所以年终奖从 4 个月变为了 3 个月,整体薪酬包虽然变化不大,但是每个月的到手钱多了。
那这次我们来看看今年 蚂蚁Java后端开发岗的校招面经 ,属于是一面,除了重点拷打实习经历之外,还进行了 20 多道的技术问题拷打 。
有些还是以场景题的形式来展开的,主要拷打的知识内容涵盖Java 虚拟机、MySQL(索引、性能调优、主从复制),分布锁,计算机网络这些内容,虽然这场面试没有手撕算法,但是也不代表蚂蚁不会有算法手撕。
面试问题在下面列出来了,一起来感受一下蚂蚁的面试难度!
蚂蚁(一面挂) 1. Java中的堆和栈的区别是什么?分别放的是什么数据? Java中的堆和栈的区别如下:
存储内容:栈 用于存放局部变量和方法调用的上下文信,。局部变量涵盖基本数据类型变量以及对象引用变量。 堆 用来存储对象实例和数组,不管是通过 new
关键字创建的对象,还是数组,都会在堆上分配内存。 内存分配与回收:栈 内存的分配和回收由系统自动完成。在方法调用时,会为该方法分配栈帧,方法执行结束后,栈帧会被自动弹出,释放内存。堆内存的分配和回收由垃圾回收器(GC)负责,当对象不再被引用时,GC 会在合适的时候回收该对象占用的内存。 访问速度:栈 访问速度快,因为栈内存是连续分配的,并且栈指针的移动操作简单高效。 堆 访问速度相对较慢,因为堆内存的分配是动态的,需要进行内存查找和管理。 空间大小:栈 每个线程都有自己独立的栈空间,栈空间一般较小,通常只有几 MB。 堆 堆内存是所有线程共享的,空间较大,可通过 JVM 参数进行调整。 下是一个简单的 Java 代码示例,用于说明堆和栈中存储的数据:
public class Example { private static String staticVar = "Static" ; // 堆(方法区) private int instanceVar = 10 ; // 堆(对象实例内) public void method () { int localVar = 20 ; // 栈(局部变量) Object obj = new Object(); // obj 引用在栈,Object 实例在堆 int [] arr = new int [ 5 ]; // arr 引用在栈,数组在堆 } }
2. String a = new String("123")有哪些对象? 会创建一个或两个 String
对象,具体情况如下:
字符串常量池中的对象:当代码里出现字符串字面量 "123"
时,Java 会先去字符串常量池查看是否已有值为 "123"
的对象。要是没有,就会在字符串常量池中创建一个 String
对象来存储 "123"
;若已有,就直接使用该对象。 堆内存中的对象: new String("123")
语句会在堆内存里创建一个新的 String
对象,此对象会复制字符串常量池中 "123"
对象的值。 比如下面的代码:
public class StringObjectCreation { public static void main (String[] args) { // 字符串常量池中的对象 String constantPoolStr = "123" ; // 堆内存中的对象 String heapStr = new String( "123" ); // 比较常量池中的对象和堆内存中的对象 System.out.println(constantPoolStr == heapStr); // 输出 false,因为它们是不同的对象 // 比较内容 System.out.println(constantPoolStr.equals(heapStr)); // 输出 true,因为它们的内容相同 } }
代码里的 "123"
会在字符串常量池中创建一个 String
对象。 new String("123")
会在堆内存中创建一个新的 String
对象。 ==
运算符用于比较对象的引用,所以 constantPoolStr == heapStr
结果为 false
。 equals
方法用于比较对象的内容,所以 constantPoolStr.equals(heapStr)
结果为 true
。 所以, String a = new String("123");
一般会创建两个 String
对象,一个在字符串常量池中,另一个在堆内存中。不过,若字符串常量池中已经存在 "123"
对象,那就只会在堆内存中创建一个新的 String
对象。
3. String是不可变的对吧,有什么好处? String
类是不可变的,也就是一旦一个 String
对象被创建,它的内容就不能被改变。主要的好处是:
由于 String
对象不可变,所以多个线程能够同时访问同一个 String
对象,不用担心数据被修改。这使得 String
在多线程环境下使用时无需额外的同步机制,保证了线程安全。 String
类重写了 hashCode()
方法,并且会在创建对象时缓存其哈希码。因为 String
不可变,所以它的哈希码不会改变,这就避免了重复计算哈希码,提高了在哈希集合(如 HashMap
、 HashSet
)中的使用效率。 Java 的字符串常量池利用了 String
的不可变性。当多个字符串字面量具有相同的值时,它们会引用常量池中的同一个 String
对象,从而节省了内存空间。 4. TCP粘包和拆包是什么?怎么解决? 粘包 指发送方在多次发送数据的过程中,数据包在同一个数据流中传输给了接收端,导致接收端无法正确分割数据包。例如,客户端连续发送两个数据包 “ABC” 和 “DEF”,服务端可能一次性收到 “ABCDEF”,这就像是两个包粘在了一起。产生原因主要有以下两点:
发送方原因 :发送方每次写入数据小于套接字(Socket)缓冲区大小,TCP 会将多次写入缓冲区的数据一次发送出去。例如,发送方先写入 “ABC”,此时缓冲区未满,接着又写入 “DEF”,然后 TCP 将 “ABCDEF” 一起发送到接收端。 接收方原因 :接收方读取套接字(Socket)缓冲区数据不够及时。当接收方的应用层没有及时读取接收缓冲区中的数据,新的数据又不断到来,就可能导致多个数据包被缓存,接收方一次读取时就会得到多个粘在一起的包。 半包 指发送方发送的数据大于发送缓冲区,接收端一次接收的数据不是完整的数据。比如,客户端发送一个较大的数据包 “ABCDEFG”,由于数据包大小超过了 TCP 缓存容量,它会被分成多个包发送,服务端第一次可能只收到 “ABC”,这就是半包现象。半包产生的原因主要有以下方面:
发送方原因 :发送方每次写入数据大于套接字(Socket)缓冲区大小,数据包不得不被分割成多个小包进行发送。 一般有三种方式分包的方式:
固定长度的消息
这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。
但是这种方式灵活性不高,实际中很少用。
特殊字符作为边界
我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。
HTTP 是一个非常好的例子。
null HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界。
有一点要注意,这个作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。
自定义消息结构
我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。
struct { u_int32_t message_length; char message_data[]; } message;
当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。
5. mysql索引分类有哪些? 索引类似于书籍的目录,可以减少扫描的数据量,提高查询效率。
如果查询的时候,没有用到索引就会全表扫描,这时候查询的时间复杂度是On 如果用到了索引,那么查询的时候,可以基于二分查找算法,通过索引快速定位到目标数据, mysql 索引的数据结构一般是 b+树,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。 MySQL可以按照四个角度来分类索引。
按「数据结构」分类: B+tree索引、Hash索引、Full-text索引 。 按「物理存储」分类: 聚簇索引(主键索引)、二级索引(辅助索引) 。 按「字段特性」分类: 主键索引、唯一索引、普通索引、前缀索引
。 6. b+树是什么?和b树区别在哪里?它俩的好处有什么? B + 树与 B 树的区别:
数据存储位置 :B 树的每个节点都可以存储数据和键值,而 B + 树的数据只存储在叶子节点,非叶子节点仅存储键值和指针。 节点指针 :B 树的节点指针指向子树,而 B + 树的内部节点指针除了指向子树外,叶子节点之间还通过双向指针连接。 查询方式 :B 树可以在非叶子节点找到数据,而 B + 树必须遍历到叶子节点才能找到数据。不过 B + 树的叶子节点形成有序链表,在进行范围查询时效率更高,而 B 树在范围查询时可能需要多次遍历不同的子树。 B 树和 B + 树的好处:
B 树的好处:适用于随机查找 :由于每个节点都可能存储数据,所以在进行随机查找时,有可能在非叶子节点就找到目标数据,平均查找次数相对较少,对于单一数据的快速定位比较有优势。 插入和删除操作相对简单 :B 树在插入和删除节点时,不需要像 B + 树那样维护叶子节点的链表结构,操作相对简单一些,在某些特定场景下可以提高数据更新的效率。 B + 树的好处:高效的范围查询 :叶子节点的有序链表结构使得范围查询变得非常高效,只需要遍历链表即可获取指定范围内的所有数据,在数据库中经常用于处理区间查询、排序等操作。 磁盘 I/O 性能优化 :因为数据都集中在叶子节点,且树的高度相对较低,所以在进行数据查询时,需要读取的磁盘块数较少,能够有效减少磁盘 I/O 操作,提高查询性能。这对于存储大量数据且存储设备读写速度相对较慢的情况非常重要,如数据库系统中可以显著提升整体性能。 稳定性高 :B + 树的结构更加稳定,因为内部节点不存储数据,只存储键值和指针,所以在插入和删除数据时,节点的分裂和合并操作相对较少,从而减少了树结构的调整,提高了系统的稳定性和可靠性。 7. 你提到的双向链表,它有什么好处? B+ 树的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。
img 8. 什么场景可能会用到b树?还是说b树的设计就是为了衬托b+树的? B树不是陪衬,而是互补,B树牺牲部分范围查询性能,换取更均衡的读写效率和内存紧凑性。
B树的核心优势是:
单点查询更快:如果目标键值在非叶子节点命中,可直接返回数据,无需访问叶子节点。 内存效率更高:适合数据量较小或完全内存驻留的场景(如缓存索引)。 写操作优化:在频繁更新场景下,B树的局部性更好,减少分裂和合并的开销。 以下是一些可能用到 B 树的场景:
内存数据库
:在内存数据库中,由于数据存储在内存中,访问速度非常快,不需要像磁盘存储那样频繁地进行 I/O 操作来读取数据。B 树的节点可以存储数据和键值,在进行随机查找时,有可能在非叶子节点就找到目标数据,平均查找次数相对较少,能够充分利用内存的高速读写特性,快速定位和访问数据,因此适用于对随机访问性能要求极高的内存数据库场景。 小型文件系统 :对于一些小型文件系统,其文件数量相对较少,文件大小也不是特别大,不需要像大型文件系统那样频繁地进行范围查询和批量数据访问。B 树的结构相对简单,插入和删除操作相对容易实现,能够有效地组织和管理文件系统中的文件和目录信息,快速实现文件的查找、创建、删除等操作。 9. mysql慢查询优化,怎么找慢sql的? 可以通过mysql 的慢查询日志,定位到慢查询的 sql,然后针对慢查询的 sql,使用EXPLAIN命令分析SQL执行计划,找出慢查询的原因,比如是否使用了全表扫描,是否存在索引未被利用的情况等,并根据相应情况对索引进行适当修改。
10. 如果有一条走索引的sql有千万数据,比如userID、phoneNumber这种,你觉得时间多长是合理的? 等值查询场景, 理想场景 (索引完全在内存中): 1~10 毫秒 (如主键索引命中缓冲池)。 普通场景 (部分数据需从磁盘加载): 10~50 毫秒 (SSD 环境下随机 I/O 开销可控)。 范围查询的场景( 返回千行级数据):理想场景 (索引和数据均缓存): 10~100 毫秒 (顺序扫描索引叶子链表)。 普通场景 (部分磁盘读取): 100~500 毫秒 (SSD 顺序读性能约 500MB/s,千万数据索引体积通常较小)。 全索引的场景, SSD 环境 : 1~5 秒 (假设索引体积 500MB,顺序读速度 500MB/s), HDD 环境 : 5~10 秒 (机械磁盘顺序读速度约 100MB/s)。 11. 数据库怎么优化,比如一个DB可能在20000TPS? 读写分离: 搭建主从架构, 利用数据库的读写分离,Web服务器在写数据的时候,访问主数据库(master),主数据库通过主从复制将数据更新同步到从数据库(slave),这样当Web服务器读数据的时候,就可以通过从数据库获得数据。这一方案使得在大量读操作的Web应用可以轻松地读取数据,而主数据库也只会承受少量的写入操作,还可以实现数据热备份,可谓是一举两得。
分库分表 :如果单表的数据超过了千万级别,考虑是否需要将大表拆分为小表,减轻单个表的查询压力。也可以将字段多的表分解成多个表,有些字段使用频率高,有些低,数据量大时,会由于使用频率低的存在而变慢,可以考虑分开。还可以不同表放在不同数据库实例上,也就是分库。 使用缓存技术 :引入缓存层,如Redis,存储热点数据和频繁查询的结果,但是要考虑缓存一致性的问题,对于读请求会选择旁路缓存策略,对于写请求会选择先更新 db,再删除缓存的策略。 12. 你提到了读写分离,怎么做? MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
这个过程一般是 异步 的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
null MySQL 集群的主从复制过程梳理成 3 个阶段:
写入 Binlog :主库写 binlog 日志,提交事务,并更新本地存储数据。 同步 Binlog :把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。 回放 Binlog :回放 binlog,并更新存储引擎中的数据。 具体详细过程如下:
MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。 在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
13. 比如你有订单详情页,下单页这种,怎么区分读写? 订单系统的读写分类:
写操作 :下单、支付回调等 强制走主库 ( @Master
注解或匹配 INSERT/UPDATE
语句)。 读操作
:订单详情、列表查询 默认走从库 ,通过AOP或中间件自动路由。 操作类型 示例SQL 路由目标 一致性要求 下单(写) INSERT INTO orders(...)
支付回调(写) UPDATE orders SET status='paid'...
订单详情(读) SELECT * FROM orders WHERE id=123
订单列表(读) SELECT * FROM orders WHERE user_id=1
订单详情页可接受短暂延迟(如1秒内),如果需强一致性可强制走主库。
14. 那主从复制出现网络问题怎么办,比如数据延迟这些问题? 强制走主库方案 :对于大事务或资源密集型操作,直接在主库上执行,避免从库的额外延迟。
15. mysql的锁你知道哪些? 在 MySQL 里,根据加锁的范围,可以分为 全局锁、表级锁和行锁 三类。
锁类型 加锁范围 加锁语句 具体说明 flush tables with read lock
执行该语句后数据库处于只读状态,其他线程的增删改或表结构修改操作都会阻塞 lock tables
对表加表锁,会限制别的线程的读写,也会限制本线程接下来的读写操作 对表进行 CRUD 操作时加 MDL 读锁;对表做结构变更操作时加 MDL 写锁 执行插入、更新、删除操作时,先对表加上「意向独占锁」,然后对该记录加独占锁 有 S 锁(共享锁)和 X 锁(排他锁)之分,满足读写互斥,写写互斥 InnoDB 引擎自动加锁,只存在于可重复读隔离级别 是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
全局锁 :通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做 全库逻辑备份
,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁 ;对一张表做结构变更操作的时候,加的是 MDL 写锁 ;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。 意向锁的目的是为了快速判断表里是否有记录被加锁 。
行级锁 :InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
16. zookeeper听说过吗? zookeeper是 分布式协调服务 ,它能很好地支持集群部署,并且具有很好的分布式协调能力,可以让我们在分布式部署的应用之间传递数据, 保证 顺序一致性(全序广播) 而不是 强一致性, 以下是其常见的应用场景:
配置管理 :在分布式系统中,不同节点往往需要相同的配置信息,如数据库连接参数、服务端口等。ZooKeeper 可以将这些配置信息集中存储,当配置发生变更时,能及时通知到各个节点。例如,一个由多个微服务组成的系统,各个服务实例可以从 ZooKeeper 中获取统一的配置,当配置更新时,ZooKeeper 会通知所有相关服务重新加载配置。 服务注册与发现 :服务注册与发现是微服务架构中的关键环节。服务提供者在启动时将自己的服务信息(如服务名称、地址、端口等)注册到 ZooKeeper 中,服务消费者通过 ZooKeeper 查找并获取服务提供者的信息。当服务提供者发生变化(如上线、下线、故障等)时,ZooKeeper 会实时更新服务列表并通知服务消费者。像 Dubbo 框架就可以利用 ZooKeeper 实现服务的注册与发现。 分布式锁 :在分布式环境下,多个进程或线程可能会竞争同一资源,为了避免数据不一致等问题,需要实现分布式锁。ZooKeeper 可以通过创建临时顺序节点来实现分布式锁。当一个客户端需要获取锁时,它会在 ZooKeeper 中创建一个临时顺序节点,然后检查自己创建的节点是否是序号最小的节点,如果是,则表示获取到了锁;如果不是,则等待前一个节点释放锁。 ZooKeeper 的数据模型类似于文件系统的树形结构,每个节点称为 Znode。
img 每个 Znode 可以存储数据,也可以有子节点。Znode 有不同的类型,包括持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)。
持久节点 :一旦创建,除非主动删除,否则会一直存在。 临时节点 :与客户端会话绑定,当客户端会话结束时,临时节点会自动被删除。 顺序节点 :在创建时,ZooKeeper 会为其名称添加一个单调递增的序号,保证节点创建的顺序性。 ZooKeeper 使用 ZAB协议来保证集群中数据的一致性。ZAB 协议基于主从架构,有一个领导者(Leader)和多个跟随者(Follower)。
img 消息广播 :当客户端发起写请求时,请求会先到达领导者。领导者将写操作封装成一个事务提案,并广播给所有跟随者。跟随者收到提案后,将其写入本地日志,并向领导者发送确认消息。当领导者收到超过半数跟随者的确认消息后,会发送提交消息给所有跟随者,跟随者收到提交消息后,将事务应用到本地状态机。 崩溃恢复 :当领导者出现故障时,ZooKeeper 会进入崩溃恢复阶段。在这个阶段,集群会选举出新的领导者,并确保在新领导者产生之前,不会处理新的写请求。选举过程基于节点的事务 ID 和节点 ID 等信息,保证新选举出的领导者包含了所有已提交的事务。 17. Redis怎么实现分布式锁的? 分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用 。如下图所示: Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
如果 key 不存在,则显示插入成功,可以用来表示加锁成功; 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。 基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。
加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁; 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间; 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端; 满足这三个条件的分布式命令如下:
SET lock_key unique_value NX PX 10000
unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作; NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作; PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。 而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放 if redis.call( "get" ,KEYS[ 1 ]) == ARGV[ 1 ] then return redis.call( "del" ,KEYS[ 1 ]) else return 0 end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
18. 假如拿到锁之后,超时了,还没释放,这个时间该怎么设置? 分布锁的过期时间过短可能导致业务未执行完锁就被释放(引发并发问题),时间过长则可能因实例崩溃导致锁无法及时释放(阻塞其他请求),所以最好 锁超时时间 > 业务最大执行时间 :确保业务逻辑能在锁自动释放前完成。
但是有时候业务会因为 gc 等异常,导致延迟,这样会让锁提前过期了,所以最好可以考虑用 redssion 来实现分布锁,它自带 watchdog 机制,会对 分布锁的过期时间进行自动续期 ,避免锁过期了,而业务还在执行的事情发生。
19. 其他问题 挑一个自己的项目讲讲(拷打半个小时,项目问题答得稀烂) 面试感受:面试官人很好,是自己没把握这次机会,让我好好总结自己的项目