首页   

蚂蚁集团今年的年终奖。。。

Python爱好者社区  · Python  · 5 天前

正文

绩效除了影响年终奖之外,也会影响涨薪的幅度,今年蚂蚁各个绩效年终奖和普调的情况如下:

  • 绩效 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(索引、性能调优、主从复制),分布锁,计算机网络这些内容,虽然这场面试没有手撕算法,但是也不代表蚂蚁不会有算法手撕。

面试问题在下面列出来了,一起来感受一下蚂蚁的面试难度!

img

蚂蚁(一面挂)

1. Java中的堆和栈的区别是什么?分别放的是什么数据?

Java中的堆和栈的区别如下:

  • 存储内容:栈用于存放局部变量和方法调用的上下文信,。局部变量涵盖基本数据类型变量以及对象引用变量。用来存储对象实例和数组,不管是通过new关键字创建的对象,还是数组,都会在堆上分配内存。
  • 内存分配与回收:栈内存的分配和回收由系统自动完成。在方法调用时,会为该方法分配栈帧,方法执行结束后,栈帧会被自动弹出,释放内存。堆内存的分配和回收由垃圾回收器(GC)负责,当对象不再被引用时,GC 会在合适的时候回收该对象占用的内存。
  • 访问速度:栈访问速度快,因为栈内存是连续分配的,并且栈指针的移动操作简单高效。访问速度相对较慢,因为堆内存的分配是动态的,需要进行内存查找和管理。
  • 空间大小:栈每个线程都有自己独立的栈空间,栈空间一般较小,通常只有几 MB。堆内存是所有线程共享的,空间较大,可通过 JVM 参数进行调整。

下是一个简单的 Java 代码示例,用于说明堆和栈中存储的数据:






    
publicclassExample{
    privatestatic String staticVar = "Static"// 堆(方法区)
    privateint instanceVar = 10;              // 堆(对象实例内)

    publicvoidmethod(){
        int localVar = 20;                     // 栈(局部变量)
        Object obj = new Object();             // obj 引用在栈,Object 实例在堆
        int[] arr = newint[5];               // arr 引用在栈,数组在堆
    }
}

2. String a = new String("123")有哪些对象?

会创建一个或两个 String 对象,具体情况如下:

  • 字符串常量池中的对象:当代码里出现字符串字面量 "123" 时,Java 会先去字符串常量池查看是否已有值为 "123" 的对象。要是没有,就会在字符串常量池中创建一个 String 对象来存储 "123";若已有,就直接使用该对象。
  • 堆内存中的对象:new String("123") 语句会在堆内存里创建一个新的 String 对象,此对象会复制字符串常量池中 "123" 对象的值。

比如下面的代码:

publicclassStringObjectCreation{
    publicstaticvoidmain(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 不可变,所以它的哈希码不会改变,这就避免了重复计算哈希码,提高了在哈希集合(如 HashMapHashSet)中的使用效率。
  • Java 的字符串常量池利用了 String 的不可变性。当多个字符串字面量具有相同的值时,它们会引用常量池中的同一个 String 对象,从而节省了内存空间。

4. TCP粘包和拆包是什么?怎么解决?

粘包指发送方在多次发送数据的过程中,数据包在同一个数据流中传输给了接收端,导致接收端无法正确分割数据包。例如,客户端连续发送两个数据包 “ABC” 和 “DEF”,服务端可能一次性收到 “ABCDEF”,这就像是两个包粘在了一起。产生原因主要有以下两点:

  • 发送方原因:发送方每次写入数据小于套接字(Socket)缓冲区大小,TCP 会将多次写入缓冲区的数据一次发送出去。例如,发送方先写入 “ABC”,此时缓冲区未满,接着又写入 “DEF”,然后 TCP 将 “ABCDEF” 一起发送到接收端。
  • 接收方原因:接收方读取套接字(Socket)缓冲区数据不够及时。当接收方的应用层没有及时读取接收缓冲区中的数据,新的数据又不断到来,就可能导致多个数据包被缓存,接收方一次读取时就会得到多个粘在一起的包。

半包指发送方发送的数据大于发送缓冲区,接收端一次接收的数据不是完整的数据。比如,客户端发送一个较大的数据包 “ABCDEFG”,由于数据包大小超过了 TCP 缓存容量,它会被分成多个包发送,服务端第一次可能只收到 “ABC”,这就是半包现象。半包产生的原因主要有以下方面:

  • 发送方原因:发送方每次写入数据大于套接字(Socket)缓冲区大小,数据包不得不被分割成多个小包进行发送。

一般有三种方式分包的方式:

  • 固定长度的消息;
  • 特殊字符作为边界;
  • 自定义消息结构。

固定长度的消息

这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。

但是这种方式灵活性不高,实际中很少用。

特殊字符作为边界

我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。

HTTP 是一个非常好的例子。

null
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
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
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 写锁
表级锁(意向锁)
执行插入、更新、删除操作时自动加锁
执行插入、更新、删除操作时,先对表加上「意向独占锁」,然后对该记录加独占锁
行级锁(记录锁)
表中的一条记录
InnoDB 引擎自动加锁
有 S 锁(共享锁)和 X 锁(排他锁)之分,满足读写互斥,写写互斥
行级锁(间隙锁)
表中的记录间隙
InnoDB 引擎自动加锁,只存在于可重复读隔离级别
用于解决可重复读隔离级别下幻读的现象
行级锁(Next-Key Lock)
表中的一个范围及记录本身
InnoDB 引擎自动加锁
是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
  • 全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份 ,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

  • 表级锁:MySQL 里面表级别的锁有这几种:

    • 表锁:通过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
img

每个 Znode 可以存储数据,也可以有子节点。Znode 有不同的类型,包括持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)。

  • 持久节点:一旦创建,除非主动删除,否则会一直存在。
  • 临时节点:与客户端会话绑定,当客户端会话结束时,临时节点会自动被删除。
  • 顺序节点:在创建时,ZooKeeper 会为其名称添加一个单调递增的序号,保证节点创建的顺序性。

ZooKeeper 使用 ZAB协议来保证集群中数据的一致性。ZAB 协议基于主从架构,有一个领导者(Leader)和多个跟随者(Follower)。

img
img
  • 消息广播:当客户端发起写请求时,请求会先到达领导者。领导者将写操作封装成一个事务提案,并广播给所有跟随者。跟随者收到提案后,将其写入本地日志,并向领导者发送确认消息。当领导者收到超过半数跟随者的确认消息后,会发送提交消息给所有跟随者,跟随者收到提交消息后,将事务应用到本地状态机。
  • 崩溃恢复:当领导者出现故障时,ZooKeeper 会进入崩溃恢复阶段。在这个阶段,集群会选举出新的领导者,并确保在新领导者产生之前,不会处理新的写请求。选举过程基于节点的事务 ID 和节点 ID 等信息,保证新选举出的领导者包含了所有已提交的事务。

17. Redis怎么实现分布式锁的?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:nullRedis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

  • 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
  • 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000
  • lock_key 就是 key 键;
  • 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[1then
  return redis.call("del",KEYS[1])
else
  return0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

18. 假如拿到锁之后,超时了,还没释放,这个时间该怎么设置?

分布锁的过期时间过短可能导致业务未执行完锁就被释放(引发并发问题),时间过长则可能因实例崩溃导致锁无法及时释放(阻塞其他请求),所以最好锁超时时间 > 业务最大执行时间:确保业务逻辑能在锁自动释放前完成。

但是有时候业务会因为 gc 等异常,导致延迟,这样会让锁提前过期了,所以最好可以考虑用 redssion 来实现分布锁,它自带 watchdog 机制,会对分布锁的过期时间进行自动续期,避免锁过期了,而业务还在执行的事情发生。

19. 其他问题

  • 挑一个自己的项目讲讲(拷打半个小时,项目问题答得稀烂)
  • 反问:怎么提升业务能力
  • 面试感受:面试官人很好,是自己没把握这次机会,让我好好总结自己的项目

© 2024 精读
删除内容请联系邮箱 2879853325@qq.com