太刺激了,字节跳动今年要招 4000 人!
大家好,我是小林。
真的太刺激了,前几天字节宣布今年实习招 4000+ 人,难得再次看到这么大规模的招聘,量大管饱啊!
我去了字节招聘官网,在招聘后端实习的部门就有 360+ 个,值得一提的是,字节表示转正机会更多,而且不限投递次数,也就是面挂了一个部门,如果流程结束了,也是可以转向其他部门看机会的。
这次招聘主要是面向 26 届的同学,找实习一定要赶早了,越早机会就会多大,也是建议 26 届同学多积累实习经历,这样秋招才会更顺利一些。
早就是优势,这是因为这时候 hc 多,而面试的人不算多,特别是学历不占优势的,最好得在第一批开放招聘的时候,就要开始考虑投的(当天前提是准备好面试了),等后面越来越多 211、985 同学准备到可以去面试了,那么学历不占优势的同学,后面可能就比较难拿到面试机会了。
最近看到一些 26 届双非本同学已经拿到字节实习 offer 了,就是因为投的早,直接约面,再加上实力还不错,直接顺利通过了。
当然,如果还没准备好的同学,也不用光着急,如果自己啥都没准备,直接投,面试机会有可能能拿到,但是一面很容易就挂了,这时候投了也白投了,不如就慢一点,继续打磨自己简历+技术栈,迎接后面的机会,后面一样也是有机会的,根据去年的经验,暑期实习一般会从 2 月份持续 6 月份。
字节面试通常有三轮技术面+hr 面,字节一面偏基础,二面偏压力面,三面就随缘了,每一面基本都有算法,字节对算法还是比较看重的,如果算法没写出来,可能机会就比较渺茫了,甚至有同学反馈,他字节有场面试直接出四道算法题,让他现场手撕,这难度确实蛮大的。
也有同学顺利拿到了字节实习 offer,他投的非常早,属于是官方还没公布的时候,自己刷官方看到有实习机会,直接就投了,是在春节后就开始投的,不到 2 周就面完了。所以大家没事多刷刷各大公司的招聘网站,看到有新岗位出来,最好第一时间投了。
这次我们就来看看, ️字节跳动 Java 后端开发的实习面经,这次分享是一面,一面是偏基础,也就是技术拷打会比较多一些。这场面试主要是拷打了「计算机网络、Java 并发、操作系统、MySQL、Redis、JVM、算法」这些方面的知识点了。
一起看看,你们觉得难得如何呢?
字节一面url输入发生了什么?客户端和服务端之间如何通信?
解析URL:分析 URL 所需要使用的传输协议和请求的资源路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。
缓存判断:浏览器缓存 → 系统缓存(hosts 文件) → 路由器缓存 → ISP 的 DNS 缓存,如果其中某个缓存存在,直接返回服务器的IP地址。
DNS解析:如果缓存未命中,浏览器向本地 DNS 服务器发起请求,最终可能通过根域名服务器、顶级域名服务器(.com)、权威域名服务器逐级查询,直到获取目标域名的 IP 地址。
获取MAC地址:当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
建立TCP连接:主机将使用目标 IP地址和目标MAC地址发送一个TCP SYN包,请求建立一个TCP连接,然后交给路由器转发,等路由器转到目标服务器后,服务器回复一个SYN-ACK包,确认连接请求。然后,主机发送一个ACK包,确认已收到服务器的确认,然后 TCP 连接建立完成。
HTTPS 的 TLS 四次握手:如果使用的是 HTTPS 协议,在通信前还存在 TLS 的四次握手。
发送HTTP请求:连接建立后,浏览器会向服务器发送HTTP请求。请求中包含了用户需要获取的资源的信息,例如网页的URL、请求方法(GET、POST等)等。
服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进行相应的处理。例如,如果是请求网页,服务器会读取相应的网页文件,并生成HTTP响应。
TCP三次握手过程如下。
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而 ️建立连接是通过三次握手来进行的。三次握手的过程如下图:
一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
从上面的过程可以发现 ️第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
TCP 四次挥手如下图:
具体过程:
客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被 ️放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 ️read 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文, ️但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:
如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,
从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。
滑动窗口体现的上层应用有哪些?
HTTP/3.0 是基于QUIC协议来实现的,QUIC是基于UDP协议实现的可靠传输协议,在应用层也会实现滑动窗口,来进行流量控制,防止接收方过载。
限流算法其中一个实现有滑动窗口的方式,滑动窗口算法是对固定窗口算法的改进,将固定窗口划分为多个更小的时间片段。随着时间的推移,窗口不断滑动,统计的是当前滑动窗口内的请求数量。滑动窗口可以避免固定窗口出现的放过两倍请求的问题,因为一个短时间内出现的所有请求必然在一个滑动窗口内,所以一定会被滑动窗口限流。
假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?
在没有任何同步机制的情况下,两个线程并发对同一个整型变量进行 50 次加 1 操作,最终结果可能是 100,也可能小于 100,最坏的结果是 50,也就是最终的结果可能是在 [50, 100] 。
小于 100 情况的分析,由于对整型变量的 num++ 操作不是原子操作,它实际上包含了三个步骤:读取变量的值、将值加 1、将新值写回变量。在多线程环境下,可能会出现线程安全问题。例如,线程 1 和线程 2 同时读取了变量的当前值,然后各自将其加 1,最后都将相同的新值写回变量,这就导致了一次加 1 操作的丢失。这种情况会多次发生,最终结果就会小于 100。
追问:如何保证2 个线程累加之后达到 100?
第一种方式:原子变量的方法。使用 AtomicInteger 替代普通 int,其 incrementAndGet 方法保证原子性,代码如下:
importjava.util.concurrent.atomic.AtomicInteger;
publicclassAtomicIntegerAddition{
privatestaticAtomicInteger num = newAtomicInteger( 0);
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread thread1 = newThread( -> {
for( inti = 0; i < 50; i++) {
num.incrementAndGet;
}
});
Thread thread2 = newThread( -> {
for( inti = 0; i < 50; i++) {
num.incrementAndGet;
}
});
thread1.start;
thread2.start;
thread1.join;
thread2.join;
System.out.println( "最终结果: "+ num.get);
}
}
第二种方式:通过 synchronized 关键字或 ReentrantLock 确保操作的互斥性,代码如下:
publicclassSynchronizedAddition{
privatestaticintnum = 0;
privatestaticfinalObject lock = newObject;
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread thread1 = newThread( -> {
for( inti = 0; i < 50; i++) {
synchronized(lock) {
num++;
}
}
});
Thread thread2 = newThread( -> {
for( inti = 0; i < 50; i++) {
synchronized(lock) {
num++;
}
}
});
thread1.start;
thread2.start;
thread1.join;
thread2.join;
System.out.println( "最终结果: "+ num);
}
}
进程通信的几种方式?
管道:管道分为「有名管道」和「匿名管道」。匿名管道是半双工通信方式,数据单向流动,通常用于有亲缘关系的进程,如父子进程间的临时通信,如果要双向通信,一般需两个管道。有名管道也是半双工,在文件系统中有路径名,允许无亲缘关系的进程通信,任何有适当权限的进程都可打开使用。
信号:用于通知接收进程某个事件已发生,如外部中断(Ctrl+C 产生 SIGINT 信号)、进程控制(kill 命令发送信号)、定时器超时(SIGALRM 信号)、子进程状态变化(父进程收到 SIGCHLD 信号)等,进程可根据收到的信号执行相应动作。
消息队列:消息的链表存于内核,由消息队列标识符标识。进程可按一定规则发送和接收消息,克服了信号传递信息少、管道只能承载无格式字节流及缓冲区大小受限等缺点。
信号量:是一个计数器,用于控制多个进程对共享资源的访问,常作为锁机制,防止进程同时访问共享资源,主要用于进程间及同一进程内不同线程间的同步。有名信号量可跨进程使用,生命周期可超过创建进程。
共享内存:允许多个进程访问同一块内存区域,进程直接读写该区域进行通信,是最快的进程间通信方式,但需配合信号量等同步机制避免数据竞争和一致性问题,可分为匿名共享内存和基于文件的共享内存。
套接字:可用于本地进程间通信和不同机器间的进程通信。用于本地时称本地套接字,用于网络时称网络套接字,提供了跨网络平台的通信机制,使不同主机上的进程能通信。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。例如,进程 A 占用了资源 R1 并等待资源 R2,而进程 B 占用了资源 R2 并等待资源 R1,这样两个进程就会相互等待,形成死锁。
死锁只有 ️同时满足以下四个条件才会发生:
互斥条件:互斥条件是指 ️多个线程不能同时使用同一个资源。
持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是 ️线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 , ️在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
环路等待条件:环路等待条件指的是,在死锁发生的时候, ️两个线程获取资源的顺序构成了环形链。
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是 ️使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
什么是孤儿进程?什么是僵尸进程?
孤儿进程是指父进程在子进程结束之前就已经退出,导致子进程失去了父进程的管理和控制,成为了 “孤儿”。此时,这些子进程会被系统的 init 进程(在 Linux 系统中,进程 ID 为 1)所收养,init 进程会负责回收它们的资源等工作。
僵尸进程是指一个进程已经执行完了它的主要任务,进入了终止状态,但由于某些原因,它的父进程没有调用相应的系统函数(如 wait 或 waitpid )来收集它的退出状态信息,导致该进程虽然已经停止运行,但在系统进程表中仍然保留着一个记录,占据着一定的系统资源。
文件相关(mv mkdir cd ls)
进程相关( ps top netstate )
权限相关(chmod chown useradd groupadd)
网络相关(netstat ip addr)
测试相关(测试网络连通性:ping 测试端口连通性:telnet)
事务的四大特性:
️原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
️一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
️隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
️持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
持久性是通过 redo log (重做日志)来保证的;
原子性是通过 undo log(回滚日志) 来保证的;
隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
一致性则是通过持久性+原子性+隔离性来保证;
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。
那么 ️在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
接下来,通过举例子给大家说明,这些问题是如何发生的。
脏读
️如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取小林的余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取小林的余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。
因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作, ️如果在上面这种情况事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读
️在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取小林的余额数据,然后继续执行代码逻辑处理,在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。
幻读
️在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
举个栗子。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库查询账户余额大于 100 万的记录,发现共有 5 条,然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。
接下来,事务 A 插入了一条余额超过 100 万的账号,并提交了事务,此时数据库超过 100 万余额的账号个数就变为 6。
然后事务 B 再次查询账户余额大于 100 万的记录,此时查询到的记录数量有 6 条, ️发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。
IP地址如何在数据库里存储?
IPv4 地址是一个 32 位的二进制数,通常以点分十进制表示法呈现,例如 192.168.1.1。
️字符串类型的存储方式:直接将 IP 地址作为字符串存储在数据库中,比如可以用 VARCHAR(15)来存储。
-- 创建一个表,使用VARCHAR类型存储IPv4地址
CREATE TABLE ip_records(
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR( 15)
);
-- 插入数据
INSERT INTO ip_records(ip_address)VALUES( '192.168.1.1') ;
️优点:直观易懂,方便直接进行数据的插入、查询和显示,不需要进行额外的转换操作。
️缺点:占用存储空间较大,字符串比较操作的性能相对较低,不利于进行范围查询。
整数类型的存储方式:将 IPv4 地址转换为 32 位无符号整数进行存储,常用的数据类型有 INT UNSIGNED。
-- 创建一个表,使用INT UNSIGNED类型存储IPv4地址
CREATE TABLE ip_records(
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address INT UNSIGNED
);
-- 插入数据,需要先将IP地址转换为整数
INSERT INTO ip_records(ip_address)VALUES(INET_ATON( '192.168.1.1') ) ;
-- 查询时将整数转换回IP地址
SELECT INET_NTOA(ip_address)FROM ip_records ;
️优点:占用存储空间小,整数比较操作的性能较高,便于进行范围查询。
️缺点:需要进行额外的转换操作,不够直观,增加了开发的复杂度。
如果对数据安全性和恢复速度都有比较高要求的话,可以开启这两种持久化方式。
️数据安全性:AOF 持久化以日志的形式记录 Redis 的每一个写操作,在发生故障时可以最大程度地保证数据的完整性。即使 Redis 服务器崩溃,只要 AOF 文件没有损坏,就可以通过重放 AOF 文件中的写操作来恢复数据,数据丢失的可能性非常小。
️恢复速度:RDB 持久化是将 Redis 在某一时刻的内存数据快照保存到磁盘上,在恢复数据时,直接加载 RDB 文件到内存中,速度比重放 AOF 文件要快得多。
JVM通过划分不同内存区域来处理数据和执行任务,主要目的是为了 ️提升内存管理效率、优化垃圾回收(GC)性能、保障线程安全,以及满足不同类型数据的存储需求。以下是具体原因和设计逻辑的详细分析:
️1、目的:提升内存管理效率。
JVM根据数据的生命周期、用途和访问频率,将内存划分为多个区域。例如:
堆:存储对象实例和数组,生命周期较长且频繁被共享,需要动态分配和垃圾回收管理。
方法区:存储类元数据(如类结构、方法字节码)、常量和静态变量,这些数据生命周期长且需要全局访问 。
虚拟机栈:存储线程私有的方法调用栈帧(局部变量、操作数栈等),生命周期与线程同步,分配和回收效率高 。
程序计数器:记录线程当前执行的字节码指令地址,确保线程切换后能恢复执行。
通过隔离不同性质的数据,JVM可以针对性地优化内存分配策略,避免单一区域的内存浪费或竞争。
线程私有的区域(如栈、程序计数器)减少了多线程间的数据竞争,提高并发性能。例如,每个线程独立维护自己的虚拟机栈,无需加锁即可快速操作
️2、目的:垃圾回收的优化。
JVM将堆划分为新生代和老年代,基于“弱代假说”(大部分对象生命周期短)优化GC效率:
新生代使用复制算法(如Survivor区),快速回收短期对象;
老年代使用标记-清除或标记-整理算法,减少长时间存活对象的回收频率;
️3、目的:保障线程安全
每个线程拥有独立的程序计数器,确保多线程切换时能正确恢复执行位置,避免指令混乱。
为Native方法单独分配栈空间,防止Java方法调用与本地代码(如C/C++)的栈操作冲突
JVM的内存区域划分是一种 ️空间换时间的设计哲学,通过牺牲部分内存冗余,换取了更高的执行效率、更灵活的垃圾回收策略,以及更稳定的多线程运行环境。
假设让你实现一个 LRU 算法,具体的实现思路是什么?
LRU 是一种缓存淘汰算法,当缓存空间已满时,优先淘汰最长时间未被访问的数据。
实现的方式是哈希表+双向链表结合。
具体实现步骤如下:
使用哈希表存储数据的键值对,键为缓存的键,值为对应的节点。
使用双向链表存储数据节点,链表头部为最近访问的节点,链表尾部为最久未访问的节点。
当数据被访问时,如果数据存在于缓存中,则将对应节点移动到链表头部;如果数据不存在于缓存中,则将数据添加到缓存中,同时创建一个新节点并插入到链表头部。
当缓存空间已满时,需要淘汰最久未访问的节点,即链表尾部的节点。
上面这种思想方式,LRU 算法可以在 O(1) 的时间复杂度内实现数据的插入、查找和删除操作。每次访问数据时,都会将对应的节点移动到链表头部,保证链表头部的节点是最近访问的数据,而链表尾部的节点是最久未访问的数据。当缓存空间不足时,淘汰链表尾部的节点即可。
算法:另一棵树的子树
leetcode:572. 另一棵树的子树(简单)
部门业务是什么?架构是怎么样?toC 还是 to B业务?
部门的架构是如何支撑起高QPS下的?