中兴开奖了,拿到了SSP!

你好,我是 Guide。

前几天,我整理分享了招银网络今年校招开奖的情况,软开的整体总包在 26w~31w(部分可能更低或者更高),一般是 12 薪,公积金按照 12% 标准缴纳,还是挺不错的。

中兴也开奖了,星球里有球友还拿到了 SSP Offer。

根据网上已经爆出的薪资来看,中兴今年给出的 SSP Offer 还挺多的。

下面是中兴今年已经开奖岗位的薪资情况:

  • 软开:(15~18)k*12,西安,白菜
  • 软开:(16~19)k*12,南京,白菜
  • 软开:(19~22)k *12,上海,SP
  • 软开(未来领军):(25~28)k*12,深圳,SSP

怎么说呢,开的真心不高,甚至有点低,很多拿到中兴 SSP 的都是硕士 985,人家手里还有其他更好的 Offer,肯定就不会考虑了。

中兴的未来领军招聘要求比较高,会要求你在某个领域有深入研究、论文或者比赛获奖经历。不过,走这个方向拿到 Offer 的,很多都是 SPP,开的薪资也比较高。

中兴的公积金是按照 8% 缴纳,年终通常比较低。另外,工作强度就不多说了,和菊厂差不多,但没有菊厂给的多。

中兴也有招聘 Java 的,下面给大家分享一篇中兴 Java 软开(未来领军)的一面面经。

实习期间做了什么?

如果你有实习经历的话,自我介绍之后,第二个问题一般就是聊你的实习经历。面试之前,一定要提前准备好对应的话术,突出介绍自己实习期间的贡献。

很多同学实习期间可能接触不到什么实际的开发任务,大部分时间可能都是在熟悉和维护项目。对于这种情况,你可以适当润色这段实习经历,找一些简单的功能研究透,包装成自己参与做的,大部分同学都是这么做的。不用担心面试的时候会露馅,只要不挑选那种明显不会交给实习生做的任务,你自己也能讲明白就行了。不过,还是更建议你在实习期间尽量尝试主动去承担一些开发任务,这样整个实习经历对个人提升也会更大一些。

示例:

  1. 负责订单模块核心流程开发,实现订单状态的精确流转,并保障与库存、支付等模块的数据一致性。
  2. 负责行为风控黑名单看板的开发,支持查看拉黑用户、批量拉黑以及取消拉黑。
  3. 基于 Redisson + AOP 封装限流组件,实现对核心接口(如付费、课程搜索)的限流,有效防止恶意请求冲击。

注入 Bean 的注解知道哪些?

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource@Inject 都可以用于注入 Bean。

Annotation Package Source
@Autowired org.springframework.bean.factory Spring 2.5+
@Resource javax.annotation Java JSR-250
@Inject javax.inject Java JSR-330

@Autowired@Resource使用的比较多一些。

@Autowired 和 @Resource 的区别是什么?

@Autowired 是 Spring 内置的注解,默认注入逻辑为先按类型(byType)匹配,若存在多个同类型 Bean,则再尝试按名称(byName)筛选

具体来说:

  1. 优先根据接口 / 类的类型在 Spring 容器中查找匹配的 Bean。若只找到一个符合类型的 Bean,直接注入,无需考虑名称;
  2. 若找到多个同类型的 Bean(例如一个接口有多个实现类),则会尝试通过属性名或参数名 与 Bean 的名称进行匹配(默认 Bean 名称为类名首字母小写,除非通过 @Bean(name = "...")@Component("...") 显式指定)。

当一个接口存在多个实现类时:

  • 若属性名与某个 Bean 的名称一致,则注入该 Bean;
  • 若属性名与所有 Bean 名称都不匹配,会抛出 NoUniqueBeanDefinitionException,此时需要通过 @Qualifier 显式指定要注入的 Bean 名称。

举例说明:

java 复制代码
// SmsService 接口有两个实现类:SmsServiceImpl1、SmsServiceImpl2(均被 Spring 管理)

// 报错:byType 匹配到多个 Bean,且属性名 "smsService" 与两个实现类的默认名称(smsServiceImpl1、smsServiceImpl2)都不匹配
@Autowired
private SmsService smsService;

// 正确:属性名 "smsServiceImpl1" 与实现类 SmsServiceImpl1 的默认名称匹配
@Autowired
private SmsService smsServiceImpl1;

// 正确:通过 @Qualifier 显式指定 Bean 名称 "smsServiceImpl1"
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;

实际开发实践中,我们还是建议通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。

@Resource属于 JDK 提供的注解,默认注入逻辑为先按名称(byName)匹配,若存在多个同类型 Bean,则再尝试按类型(byType)筛选

@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。

java 复制代码
public @interface Resource {
    String name() default "";
    Class<?> type() default Object.class;
}

如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定nametype属性(不建议这么做)则注入方式为byType+byName

java 复制代码
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;

简单总结一下

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显式指定名称,@Resource可以通过 name 属性来显式指定名称。
  • @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

考虑到 @Resource 的语义更清晰(名称优先),并且是 Java 标准,能减少对 Spring 框架的强耦合,我们通常更推荐使用 @Resource ,尤其是在需要按名称注入的场景下。而 @Autowired 配合构造器注入,在实现依赖注入的不可变性和强制性方面有优势,也是一种非常好的实践。

推荐阅读:Spring 常见面试题总结(Spring 基础、IoC、AOP、MVC、事务、循环依赖等)

项目中密码是则呢么保存的?

如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。

Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的接口是 PasswordEncoder ,如果你想要自己实现一个加密算法的话,也需要实现 PasswordEncoder 接口。

PasswordEncoder 接口一共也就 3 个必须实现的方法。

java 复制代码
public interface PasswordEncoder {
    // 加密也就是对原始密码进行编码
    String encode(CharSequence var1);
    // 比对原始密码和数据库中保存的密码
    boolean matches(CharSequence var1, String var2);
    // 判断加密密码是否需要再次进行加密,默认返回 false
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

官方推荐使用使用 Bcrypt 这种密钥派生算法(Key Derivation Function,简称 KDF,也称为密码哈希算法)。

我之前分享过一篇文章详细介绍:简历别再写 MD5 加密密码了!

MySQL InnoDB 和 MyISAM 区别?

MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默认存储引擎,可谓是风光一时。

虽然,MyISAM 的性能还行,各种特性也还不错(比如全文索引、压缩、空间函数等)。但是,MyISAM 不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。

MySQL 5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。

言归正传!咱们下面还是来简单对比一下两者:

1、是否支持行级锁

MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了!

2、是否支持事务

MyISAM 不提供事务支持。

InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。

3、是否支持外键

MyISAM 不支持,而 InnoDB 支持。

外键对于维护数据一致性非常有帮助,但是对性能有一定的损耗。因此,通常情况下,我们是不建议在实际生产项目中使用外键的,在业务代码中进行约束即可!

阿里的《Java 开发手册》也是明确规定禁止使用外键的。

不过,在代码中进行约束的话,对程序员的能力要求更高,具体是否要采用外键还是要根据你的项目实际情况而定。

总结:一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。

4、是否支持数据库异常崩溃后的安全恢复

MyISAM 不支持,而 InnoDB 支持。

使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log

5、是否支持 MVCC

MyISAM 不支持,而 InnoDB 支持。

讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。

6、索引实现不一样。

虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。

InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。

7、性能有差别。

InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。

8、数据缓存策略和机制实现不同。

InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。

总结

  • InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
  • MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
  • MyISAM 不支持外键,而 InnoDB 支持。
  • MyISAM 不支持 MVCC,而 InnoDB 支持。
  • 虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
  • MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
  • InnoDB 的性能比 MyISAM 更强大。

最后,再分享一张图片给你,这张图片详细对比了常见的几种 MySQL 存储引擎。

索引为什么快?

索引之所以快,核心原因是它大大减少了磁盘 I/O 的次数

它的本质是一种排好序的数据结构,就像书的目录,让我们不用一页一页地翻(全表扫描)。

在 MySQL 中,这个数据结构是B+树。B+树结构主要从两方面做了优化:

  1. B+树的特点是"矮胖",一个千万数据的表,索引树的高度可能只有 3-4 层。这意味着,最多只需要3-4 次磁盘 I/O,就能精确定位到我想要的数据,而全表扫描可能需要成千上万次,所以速度极快。
  2. B+树的叶子节点是用链表连起来的 。找到开头后,就能顺着链表顺序读下去,这对磁盘非常友好,还能触发预读。

如何分析 SQL 语句是否走索引查询?

我们可以使用 EXPLAIN 命令来分析 SQL 的 执行计划 ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。

EXPLAIN 并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。

EXPLAIN 的输出格式如下:

sql 复制代码
mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
|  1 | SIMPLE      | cus_order | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 997572 |   100.00 | Using filesort |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
1 row in set, 1 warning (0.00 sec)

各个字段的含义如下:

列名 含义
id SELECT 查询的序列标识符
select_type SELECT 关键字对应的查询类型
table 用到的表名
partitions 匹配的分区,对于未分区的表,值为 NULL
type 表的访问方法
possible_keys 可能用到的索引
key 实际用到的索引
key_len 所选索引的长度
ref 当使用索引等值查询时,与索引作比较的列或常量
rows 预计要读取的行数
filtered 按表条件过滤后,留存的记录数的百分比
Extra 附加信息

更多 MySQL 高频知识点和面试题总结,可以阅读笔者写的这几篇文章:

项目中引入 Redis 具体做了哪些事情?

Redis 除了可以用来缓存高频访问的数据之外,还以用来实现:

  • 分布式锁 :通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:如何基于 Redis 实现分布式锁?
  • 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
  • 消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  • 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
  • 分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
  • 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。

面试中,根据你项目的实际情况去回答即可!

相关阅读:

为什么删除 Redis 而不是更新 Redis

这里探讨的是 Cache Aside Pattern(旁路缓存模式),这是我们平时使用比较多的一个缓存读写模式。

这个策略模式下的缓存读写步骤:

  1. 先更新 db;
  2. 直接删除 cache 。

:

  1. 从 cache 中读取数据,读取到就直接返回;
  2. cache 中读取不到的话,就从 db 中读取数据返回;
  3. 再把 db 中读取到的数据放到 cache 中。

为什么删除 cache,而不是更新 cache?

主要原因有两点:

  1. 对服务端资源造成浪费 :删除 cache 更加直接,这是因为 cache 中存放的一些数据需要服务端经过大量的计算才能得出,会消耗服务端的资源,是一笔不小的开销。如果频繁修改 db,就能会导致需要频繁更新 cache,而 cache 中的数据可能都没有被访问到。
  2. 产生数据不一致问题 :并发场景下,更新 cache 产生数据不一致性问题的概率会更大。

数据不一致举例说明一下:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。这个过程可以简单描述为:

  1. 请求 1(写操作) 先把 cache 中的 A 数据删除;
  2. 请求 2 (读操作) 过来,发现缓存里没有数据 A,于是从 db 中读取旧数据;
  3. 与此同时,请求 1 再把 db 中的 A 数据更新。
  4. 请求 2 把从数据库读到的旧值写入了缓存。

这就会导致请求 2 读取到的是旧值。并且,数据库里是新值,缓存里却是旧值,数据不一致了!

在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?

理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。

如果更新数据库成功,而删除缓存失败怎么解决?

简单说有两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
  2. 增加缓存更新重试机制(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试删除缓存,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。

更多 Redis 高频面试题总结,可以阅读笔者写的这几篇文章:

JVM 哪些区域会出现 OOM?

JVM 运行时数据区域如下:

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

像虚拟机栈、本地方法栈、堆、直接内存等都可能会出现 OutOfMemoryError

OOM 怎么排查?

我们可以通过 MAT、JVisualVM 等工具分析 Heap Dump 找到导致OutOfMemoryError 的原因。

以 MAT 为例,其提供的泄漏嫌疑(Leak Suspects)报告是 MAT 最强大的功能之一。它会基于启发式算法自动分析整个堆,直接指出最可疑的内存泄漏点,并给出详细的报告,包括问题组件、累积点(Accumulation Point)和引用链的图示。

如果"泄漏嫌疑"报告不够明确,或者想要分析的是内存占用过高(而非泄漏)问题,可以切换到**支配树(Dominator Tree)**视图。这个视图将内存对象关系组织成一棵树,父节点"支配"子节点(即父节点被回收,子节点也必被回收)。

下面是一段模拟出现 OutOfMemoryError的代码:

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class SimpleLeak {

    // 静态集合,生命周期与应用程序一样长
    public static List<byte[]> staticList = new ArrayList<>();

    public void leakMethod() {
        // 每次调用都向静态集合中添加一个 1MB 的字节数组
        staticList.add(new byte[1024 * 1024]); // 1MB
    }

    public static void main(String[] args) throws InterruptedException {
        SimpleLeak leak = new SimpleLeak();
        System.out.println("Starting leak simulation...");

        // 循环添加对象,模拟内存泄漏过程
        for (int i = 0; i < 200; i++) {
            leak.leakMethod();
            System.out.println("Added " + (i + 1) + " MB to the list.");
            Thread.sleep(200); // 稍微延时,方便观察
        }

        System.out.println("Leak simulation finished. Keeping process alive for Heap Dump.");
        // 保持进程存活,以便我们有时间生成 Heap Dump
        Thread.sleep(Long.MAX_VALUE);
    }
}

为了更快让程序出现 OutOfMemoryError 问题,我们可以故意设置一个较小的堆 -Xmx256m

IDEA 设置 VM 参数的方式如下图所示:

具体设置的 VM 参数是:-Xmx128m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=simple_leak.hprof,其中:

  • -Xmx128m:设置 JVM 最大堆内存为 128MB。
  • -XX:+HeapDumpOnOutOfMemoryError:当 JVM 发生 OutOfMemoryError 时,自动生成堆转储文件(.hprof)。
  • -XX:HeapDumpPath=simple_leak.hprof:指定 OOM 时生成的堆转储文件路径及文件名(这里是 simple_leak.hprof)。

运行程序之后,会出现 OutOfMemoryError并自动生成了 Heap Dump 文件。

bash 复制代码
Starting leak simulation...
Added 1 MB to the list.
Added 2 MB to the list.
Added 3 MB to the list.
......
Added 113 MB to the list.
Added 114 MB to the list.
Added 115 MB to the list.
java.lang.OutOfMemoryError: Java heap space
Dumping heap to simple_leak.hprof ...
Heap dump file created [124217346 bytes in 0.121 secs]

我们将 .hprof 文件导入 MAT 后,它会首先进行解析和索引。完成后,可以查看它的 "泄漏嫌疑报告" (Leak Suspects Report)

下图中的 Problem Suspect 1 就是可能出现内存泄露的问题分析:

  • cn.javaguide.SimpleLeak 类由 sun.misc.Launcher$AppClassLoader 加载,占用 120,589,040 字节(约 115MB,占堆 98.80%),是内存占用的核心。
  • 内存主要被 java.lang.Object[] 数组 占用(120,588,752 字节),说明 SimpleLeak 中可能存在大量 Object 数组未释放,触发内存泄漏。

Problem Suspect 1 的可以看到有一个 Details,点进去即可看到内存泄漏的关键路径和对象占比:

可以看到:SimpleLeak 中的静态集合 staticList 是内存泄漏的 "根源",因为静态变量生命周期与类一致,若持续向其中添加对象且不清理,会导致对象无法被 GC 回收。

相关推荐
绝无仅有3 小时前
腾讯MySQL面试深度解析:索引、事务与高可用实践 (二)
后端·面试·github
IT_陈寒3 小时前
SpringBoot 3.0实战:这套配置让我轻松扛住百万并发,性能提升300%
前端·人工智能·后端
JaguarJack3 小时前
开发者必看的 15 个困惑的 Git 术语(以及它们的真正含义)
后端·php·laravel
Victor3564 小时前
Redis(91)Redis的访问控制列表(ACL)是如何工作的?
后端
努力进修4 小时前
Rust 语言入门基础教程:从环境搭建到 Cargo 工具链
开发语言·后端·rust
Victor3564 小时前
Redis(90)如何配置Redis的身份验证?
后端
程序员爱钓鱼5 小时前
Python编程实战 - 函数与模块化编程 - 参数与返回值
后端·python·ipython
程序员爱钓鱼5 小时前
Python编程实战 - 函数与模块化编程 - 局部变量与全局变量
后端·python·ipython
摇滚侠8 小时前
Spring Boot3零基础教程,KafkaTemplate 发送消息,笔记77
java·spring boot·笔记·后端·kafka