话说 TP-LINK 联洲的秋招提前批已经开启很久了,6 月份就已经开启了,并且最近已经有人陆陆续续拿到口头 Offer 了,所以今天就来给大家介绍一下 TP-LINK 的面试流程和真题及答案解析。
秋招提前批投递地址
官网投递地址:https://hr.tp-link.com.cn/jobList
TP-LINK 面试流程
TP-LINK 整个面试流程如下:
- 技术面:两面或者是三面,普通 Offer 两面,SP Offer 三面。
- 心理评测
- 座谈会
- 签约
- 电话 OC
签订三方协议
面试问题
问题来源于某客,如下图所示:
问题链接:https://www.nowcoder.com/feed/main/detail/9af7b7989419489284b3cfce7aaae2bc
答案解析
1.说一下微服务架构?
微服务是一种软件开发架构风格,用于构建复杂应用程序。
它将大型应用程序拆分成一系列较小、独立的服务,每个服务专注于完成特定的业务功能。这些服务之间通过轻量级的通信机制(通常是基于 HTTP 或 RPC)进行交互,可以独立部署、扩展和管理。
微服务的主要特点包括:
- 单一责任:每个微服务专注于执行一个明确定义的业务功能。这使得开发人员可以更容易地理解和维护服务。
- 松耦合:微服务之间是独立的,它们可以使用不同的编程语言、技术堆栈和数据存储。这种松耦合使得开发团队能够独立地开发、测试和部署各个服务。
- 独立部署:每个微服务都可以独立地部署,这意味着当对一个服务进行更改时,不需要重新部署整个应用程序。这提高了开发和发布的速度,并允许快速迭代和灵活性。
- 弹性扩展:由于每个微服务是独立的,可以根据需要对它们进行独立的扩展。这使得应用程序能够更好地处理高负载情况,并具有更好的可伸缩性。
有限上下文:每个微服务维护自己的数据存储,这意味着它们可以使用不同类型的数据库或存储技术。这种隔离有助于减少整个系统的复杂性,并提高可靠性。
2.微服务优缺点
微服务架构具有以下优点:
- 松耦合:微服务架构使得各个服务之间的耦合度降低,每个服务都是独立的,可以使用不同的编程语言、技术堆栈和数据存储。这样可以提高团队的自治性,各个服务可以独立开发、测试和部署。
- 可伸缩性:由于微服务是独立的,可以根据需要对每个服务进行独立的扩展。这意味着可以根据流量和负载的需求,对具体的服务进行水平扩展,提高系统的性能和可用性。
- 独立部署:每个微服务都可以独立地部署,这样在更新或修复某个服务时,不需要重新部署整个应用程序。这样可以降低风险,并提高开发和发布的速度。
- 技术异构性:微服务架构允许不同的服务使用不同的技术和工具。这样可以选择最适合每个服务需求的技术,提高开发效率和灵活性。
- 易于理解和维护:微服务架构将复杂的应用程序拆分为小而独立的服务,每个服务专注于一个明确定义的业务功能。这样使得代码库更易于理解和维护。
然而,微服务架构也存在一些挑战和缺点:
- 分布式系统复杂性:微服务架构中的服务是分布式的,需要处理服务间通信、数据一致性、错误处理等问题。这增加了系统的复杂性,需要更多的设计和管理工作。
- 服务间通信开销:由于微服务架构中的服务通过网络通信进行交互,会增加一定的延迟和开销。此外,需要实现适当的通信机制和协议来确保可靠性和数据一致性。
- 运维复杂性:微服务架构中涉及多个独立的服务,每个服务都需要独立进行监控、日志记录和故障排除。这增加了运维的复杂性,需要适当的工具和自动化来管理和监控服务。
数据一致性:由于每个微服务维护自己的数据存储,确保数据一致性变得更加困难。在跨多个服务的业务操作中,需要采取适当的策略和技术来保证数据的一致性和完整性。
3.负载均衡的实现算法
负载均衡是指将网络流量或工作负载分配到多个服务器或计算资源上,以提高系统的性能、可靠性和可扩展性。在实现负载均衡时,通常会采用以下算法:
- 轮询(Round Robin):按照轮询的方式依次将请求分发给后端服务器。每个请求按照顺序依次分配给不同的服务器,循环往复。这种算法简单且均衡,适用于服务器性能相似且无状态的情况。
- 最少连接(Least Connection):根据当前连接数选择连接数最少的服务器来处理新的请求。这种算法可以有效地将负载均衡到连接数较少的服务器上,以保持各服务器的负载相对均衡。
- IP哈希(IP Hash):根据客户端的 IP 地址进行哈希计算,将同一个 IP 地址的请求发送到同一个后端服务器。这样可以确保同一个客户端的请求都发送到同一台服务器上,适用于需要保持会话一致性的场景。
- 加权轮询(Weighted Round Robin):给每个服务器分配一个权重值,根据权重值的比例来分配请求。具有较高权重的服务器会接收到更多的请求,适用于服务器性能不均衡的情况。
- 加权最少连接(Weighted Least Connection):根据服务器的当前连接数和权重值来选择服务器。连接数越少且权重值越高的服务器会被优先选择。
- 随机(Random):随机选择一个后端服务器来处理请求。这种算法简单且均衡,但无法保证每个服务器的负载一致。
- 响应时间加权(Response Time Weighted):根据服务器的平均响应时间或处理时间来分配请求。响应时间较短的服务器会得到更多的请求,以提高系统整体的响应速度。
这些算法可以单独使用,也可以结合使用,根据实际需求和场景进行选择和配置。另外,现代的负载均衡器通常会结合实时监测和自动调整策略,根据服务器的负载情况动态地调整请求分发策略,以实现更智能和自适应的负载均衡。
4.Redis集群部署方式?
Redis集群主要有以下三种模式:
- 主从复制(Master-Slave Replication):这是最简单的 Redis 集群部署方式。在主从复制中,一个节点作为主节点(master),负责处理写操作和读操作的部分负载;而其他节点作为从节点(slaves),复制主节点的数据,并负责读操作的负载。主节点负责写操作的原始数据,而从节点通过异步复制主节点的数据来提供读操作的负载均衡和高可用性。
- 哨兵模式(Sentinel):Sentinel 模式用于提供 Redis 的高可用性。在这种部署方式中,有多个 Redis 实例,其中一个充当主节点,负责处理写操作和读操作的部分负载。同时,还有多个 Sentinel 节点,它们监控主节点的状态,并在主节点故障时自动将从节点提升为新的主节点。这种方式可以实现故障切换和自动恢复。
Redis Cluster 模式:Redis Cluster 是 Redis 官方提供的分布式集群解决方案。它通过分区(sharding)将数据分布在多个节点上,每个节点负责一部分数据。Redis Cluster 使用哈希槽(hash slots)来管理数据分布,并在节点故障时进行自动迁移和重新分配。客户端可以直接连接到任何一个节点,节点会协调数据的读写操作。
5.MySQL主从复制?
MySQL 主从复制是一种常见的数据复制技术,用于实现 MySQL 数据库的高可用性、读写分离和数据备份等需求。在主从复制中,有一个主数据库(Master)和一个或多个从数据库(Slaves)。
MySQL 主从复制在确保了主服务器(Master)和从服务器(Slave)网络连接正常,可以互相访问的情况下,通过配置(主要是主服务器开启 bin log),从服务同步 bin log 的方式就可以实现主从复制了。
5.1 配置流程
主从复制的设置步骤如下:
- 配置主数据库:在主数据库上启用二进制日志,设置一个唯一的服务器ID,并在需要复制的数据库中创建一个专门用于复制的账户。
- 配置从数据库:在从数据库上设置一个唯一的服务器ID,并配置连接主数据库的相关参数,如主数据库的IP地址、账户信息等。
- 启动主从复制:在从数据库上启动复制进程,连接到主数据库并开始复制主数据库的数据。
一旦主从复制设置完成,主数据库上的写操作将自动复制到从数据库上,从而实现数据的同步复制。应用程序可以通过读写分离的方式,将读操作发送到从数据库上,以提高系统的读性能。
5.2 优缺点分析
主从复制具有以下优点:
- 高可用性:当主数据库发生故障时,可以快速切换到从数据库作为新的主数据库,实现故障切换,从而提高系统的可用性。
- 读写分离:可以将读操作分发到从数据库上,减轻主数据库的负载,提高整体的读性能。
- 数据备份:从数据库可以作为主数据库的备份,用于恢复数据和灾难恢复。
需要注意的是,主从复制并不适用于所有的场景,它具有一些限制和注意事项,如主从延迟、数据一致性、主数据库的单点故障等。因此,在使用主从复制时,需要仔细考虑系统需求和架构,并进行适当的监控和维护。
6.口头手撕快排
快速排序是一种分治算法,它通过将一个数组分成较小的子数组,然后递归地对子数组进行排序,最后将子数组的结果合并起来,从而达到整体有序的目的。
快速排序的实现步骤:
- 选择一个基准元素(pivot),通常是选择数组中的第一个元素或最后一个元素。
- 将数组分成两个子数组,小于等于基准元素的放在左边,大于基准元素的放在右边。
- 对左右两个子数组递归地应用快速排序算法。
- 将左子数组、基准元素和右子数组合并起来,得到最终的排序结果。
以下是 Java 实现的快排基本代码:
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5, 2, 9, 1, 7, 6, 3};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
7.队列实现栈和栈实现队列
7.1 队列实现栈
队列实现栈的基本思路是使用两个队列来模拟栈的行为,通过这种方式,可以实现栈的“先进后出”(Last In First Out,LIFO)的特性。
实现思路
- 初始化两个队列,记为 queue1 和 queue2。
- 当执行 push 操作时,将元素添加到 queue1 中。
- 当执行 pop 操作时,首先将 queue1 中的元素依次出队并入队到 queue2 中,直到 queue1 中只剩下一个元素。这个剩下的元素就是需要出栈的元素,将其移除并返回。
- 交换 queue1 和 queue2 的引用,使得 queue1 成为主队列,即 queue1 始终保持非空,而 queue2 作为辅助队列。
- top 操作则返回 queue1 中的最后一个元素,即栈顶元素。
empty 操作则判断 queue1 是否为空。
实现代码
import java.util.LinkedList; import java.util.Queue; public class StackUsingQueue { private Queue<Integer> queue1; private Queue<Integer> queue2; private int top; public StackUsingQueue() { queue1 = new LinkedList<>(); queue2 = new LinkedList<>(); } public void push(int x) { queue1.add(x); top = x; } public int pop() { while (queue1.size() > 1) { top = queue1.remove(); queue2.add(top); } int value = queue1.remove(); Queue<Integer> temp = queue1; queue1 = queue2; queue2 = temp; return value; } public int top() { return top; } public boolean empty() { return queue1.isEmpty(); } }
7.2 栈实现队列
栈是一种后进先出(LIFO)的数据结构,而队列是一种先进先出(FIFO)的数据结构。为了实现一个队列,我们可以使用两个栈来模拟。
实现思路
以下是使用两个栈实现队列的思路:
- 定义两个栈,分别称为"输入栈"(input stack)和"输出栈"(output stack)。
- 当有新元素进入队列时,将其压入输入栈。
- 当需要出队列时,如果输出栈为空,将输入栈中的所有元素弹出并依次压入输出栈。这样,输出栈的顶部元素即为队列的第一个元素,可以出队列。如果输出栈不为空,直接弹出输出栈的顶部元素。
- 当需要获取队列的第一个元素时,执行步骤 3 中的操作,保证输出栈的顶部元素为队列的第一个元素。
这种实现方式的思路是,使用输入栈来保存新进入队列的元素,而输出栈则负责提供队列的第一个元素和出队列操作。当输出栈为空时,需要将输入栈的元素转移到输出栈中,以保证队列的顺序。
实现代码
import java.util.Stack;
class MyQueue {
private Stack<Integer> stack1; // 用于入队操作
private Stack<Integer> stack2; // 用于出队操作
/** 初始化队列数据结构 */
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
/** 入队操作 */
public void push(int x) {
stack1.push(x);
}
/** 出队操作 */
public int pop() {
if (stack2.isEmpty()) {
// 将 stack1 中的元素依次弹出并压入 stack2
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
/** 获取队头元素 */
public int peek() {
if (stack2.isEmpty()) {
// 将 stack1 中的元素依次弹出并压入 stack2
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
/** 判断队列是否为空 */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
public class Main {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.push(3);
System.out.println(queue.pop()); // 输出:1
System.out.println(queue.peek()); // 输出:2
System.out.println(queue.empty()); // 输出:false
}
}
8.进程有几种状态?
进程在操作系统中可以处于以下几种状态:
- 创建(Created):进程正在被创建,但尚未开始执行。
- 就绪(Ready):进程已经创建并分配了所有必要的资源,等待被调度器选中并分配CPU资源开始执行。
- 运行(Running):被调度器选中的进程正在执行,并占用CPU资源。
- 阻塞(Blocked):进程由于某些原因无法继续执行,例如等待外部事件的发生(如输入/输出操作)或等待资源的释放。在此状态下,进程暂时停止执行,直到满足某些条件后才能切换到就绪状态。
终止(Terminated):进程执行完成或被操作系统终止,释放所有分配的资源。
9.Spring Boot Actuator?
Spring Boot Actuator 为 Spring Boot 框架提供了强大的功能,用于监控和管理 Spring Boot 应用程序。它提供了一系列的 REST API,可以让开发者通过 HTTP 请求来获取应用程序的运行时信息,如健康状况、内存使用情况、线程信息、日志等。同时,Actuator 还支持自定义的端点,可以根据项目需求添加自定义的监控和管理功能。通过 Actuator,开发者可以方便地监控和管理应用程序的运行状态,以及进行一些特定的操作,如动态修改日志级别、重新加载配置等。
Spring Boot Actuator 更多内容可访问:https://juejin.cn/post/7052857798530433031#heading-7
10.外键、主键和索引?
在数据库中,外键、主键和索引是三个不同的概念。
- 主键(Primary Key):主键是用来唯一标识一条记录的字段或字段组合。每张表只能有一个主键,主键的值不能重复且不能为空。主键的作用是保证数据的完整性和唯一性,加快数据检索速度。
- 外键(Foreign Key):外键是用来建立表与表之间的关联关系的字段。它指向另一张表的主键,用来保持数据完整性和一致性。外键可以确保数据之间的引用关系,并且在删除或更新操作时可以自动处理关联表中的数据。
- 索引(Index):索引是为了提高数据检索速度而创建的数据结构。它类似于书籍的目录,可以根据某个字段或字段组合快速定位到具体的数据记录。索引可以加快数据检索的速度,但会占用额外的存储空间,并且在插入、删除和更新操作时会有一定的性能影响。
但在实际开发中,因为性能的原因,所以我们很少用到真正的外键,也就是“物理外键”(使用 FOREIGN KEY 创建),而是在程序中使用逻辑外键来“建立”多张表的外键关系。
阿里巴巴《Java开发手册》中也明确规定禁止使用数据库的外键,如下图所示:
11.TCP和UDP区别?
TCP(传输控制协议)和 UDP(用户数据报协议)是两种常用的网络传输协议。
TCP 是一种面向连接的协议,它提供可靠的数据传输。在 TCP 通信中,数据被分成多个小片段,每个片段都会被编号和校验,确保数据完整性。TCP 使用确认机制,确保数据的可靠性,如果发送方没有收到确认信息,会重新发送数据。TCP还处理拥塞控制,根据网络条件动态调整数据传输的速率。TCP 适用于需要保证数据完整性和可靠性的应用,如文件传输、电子邮件等。
UDP 是一种面向无连接的协议,它提供不可靠的数据传输。在 UDP 通信中,数据被封装成数据包,直接发送给接收方,不需要建立连接。UDP 不提供数据校验、确认机制和拥塞控制,因此传输速度较快,但容易发生数据丢失。UDP 适用于实时传输要求较高的应用,如音频、视频流等。
所以,总结来说:TCP 是可靠的、有序的、面向连接的传输协议,而 UDP 是简单的、不可靠的、无连接的传输协议。选择 TCP 还是 UDP 要根据具体的应用需求来确定。
12.说一下哈西表?
哈希表(Hash Table),也称为散列表,是一种常用的数据结构,用于实现键值对的存储和快速检索。
哈希表的核心思想是通过哈希函数将键映射到一个固定大小的数组索引上,将键值对存储在该索引位置上。当需要查找或插入数据时,通过哈希函数计算出键对应的索引,然后在该位置上进行操作,从而实现快速的数据访问。
哈希表的优点是在平均情况下具有常数时间复杂度 O(1) 的查找、插入和删除操作。然而,在极端情况下,哈希冲突可能会导致性能下降,需要解决冲突的方法,如开放地址法(线性探测、二次探测等)或链表法(在冲突位置上使用链表存储多个键值对)。
哈希表广泛应用于各种编程场景中,如数据库索引、缓存系统、编译器中的符号表等,它提供了高效的数据访问和操作效率。
在 Java 中,哈希表的常见实现类有 Hashtable、HashMap 和 ConcurrentHashMap。
13.避免哈希冲突方法?
解决哈希冲突的常用方法有以下三种:链地址法、开放地址法和再哈希法。
- 链地址法(Separate Chaining):将哈希表中的每个桶都设置为一个链表,当发生哈希冲突时,将新的元素插入到链表的末尾。这种方法的优点是简单易懂,适用于元素数量较少的情况。缺点是当链表过长时,查询效率会降低。
- 开放地址法(Open Addressing):当发生哈希冲突时,通过一定的探测方法(如线性探测、二次探测、双重哈希等)在哈希表中寻找下一个可用的位置。这种方法的优点是不需要额外的存储空间,适用于元素数量较多的情况。缺点是容易产生聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。
- 再哈希法(Rehashing):当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将元素插入到对应的桶中。这种方法的优点是简单易懂,适用于元素数量较少的情况。缺点是需要额外的哈希函数,且当哈希函数不够随机时,容易产生聚集现象。
在 Java 中,HashMap 使用的是链地址法来解决哈希冲突的。
14.说一下JVM?
JVM(Java Virtual Machine,Java虚拟机)是 Java 程序的运行环境,它负责将 Java 字节码翻译成机器代码并执行。也就是说 Java 代码之所以能够运行,主要是依靠 JVM 来实现的。
JVM 整体的大概执行流程是这样的:
- 程序在执行之前先要把 Java 代码转换成字节码(class 文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中运行时数据区(Runtime Data Area);
- 但字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器,也就是 JVM 的执行引擎(Execution Engine)会将字节码翻译成底层系统指令再交由 CPU 去执行;
- 在执行的过程中,也需要调用其他语言的接口,如通过调用本地库接口(Native Interface) 来实现整个程序的运行。
JVM 具有以下特点:
- 平台无关性:JVM 使得 Java 程序可以在不同的操作系统和硬件平台上运行,而不需要重新编译和调整。
- 安全性:JVM 可以对 Java 程序进行安全管理,防止恶意代码的攻击和破坏。
- 内存管理:JVM 可以自动管理内存,包括分配和回收内存空间,以避免内存泄漏和崩溃。
- 字节码执行:JVM 可以执行 Java 字节码文件,而不需要解释器或编译器。
JVM 的实现有多种标准和非标准的方式,包括 HotSpot JVM、GraalVM、Jython 和 JRuby 等。不同的 JVM 实现有不同的性能和功能特性,需要根据具体的应用场景进行选择。
JVM 内存布局共有以下 5 部分:
- 程序计数器(Program Counter Register):用于记录当前线程执行的字节码指令地址,是线程私有的,线程切换不会影响程序计数器的值。
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。每个方法在执行时都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈等信息。
- 本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,用于存储本地方法的信息。
- Java 堆(Java Heap):用于存储对象实例和数组,是 JVM 中最大的一块内存区域,它是所有线程共享的。堆通常被划分为年轻代和老年代,以支持垃圾回收机制。
- 年轻代(Young Generation):用于存放新创建的对象。年轻代又分为 Eden 区和两个 Survivor 区(通常是一个 From 区和一个 To 区),对象首先被分配在 Eden 区,经过垃圾回收后存活的对象会被移到 Survivor 区,经过多次回收后仍然存活的对象会晋升到老年代。
- 老年代(Old Generation):用于存放存活时间较长的对象。老年代主要存放长时间存活的对象或从年轻代晋升过来的对象。
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享的。
15.项目中使用了哪些设计模式?
回答此问题,可以从一些常用的设计模式入手,比如以下这些:
- 单例模式:因为项目是 Spring Boot 项目,所以默认注入的所有对象都是单例模式,或者说项目中的某一个类就是通过双重效验锁的方式实现了单例模式。
- 工厂模式:项目中使用了线程池来实现一个接口的多个数据组装,之后再统一返回结果的,而线程池是通过默认的线程工厂实现的,所以也使用到了工厂模式。
- 观察者模式:如果项目中使用了 Spring Event 或者 Google Guava EventBus,那么就可以说你项目中使用了观察者模式,因为 Event(事件)本身是基于观察者模式实现的。
- 发布订阅者模式:如果你的项目中使用了消息中间件,比如 Kafka、RabbitMQ、RocketMQ 等,那么你就可以说你的项目中使用了发布、订阅者模式,因为消息队列本身就是发布订阅者模式。
- 策略模式:策略模式定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。比如用户登录包含了:账号密码登录、手机验证码登录和第三方登录等,我们把不同的登录方式封装成不同的登录策略,这就是策略模式。
模板方法模式:它在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。比如后台的数据上传功能,既支持 DOC 格式,又支持 CSV 格式,那么我们就在超类中定义执行的步骤,然后再实现各自类中重写读取方法,因为 DOC 和 CSV 的读写方法是不同的,这就是典型的模板方法模式。
16.什么是线程安全?
线程安全是指在多线程环境下,程序的行为不会被其他线程的干扰所影响,保证了多个线程同时访问共享资源时的正确性和可靠性。
在 Java 中,为了保证线程安全,可以使用 synchronized 关键字或者 Lock 接口来实现同步。synchronized 关键字可以保证同一时刻只有一个线程能够访问共享资源,而 Lock 接口则提供了更加灵活的控制方式。
小结
TP-LINK 总体面试难度一般,可能是因为面试时间的原因,所以很多知识的底层实现和细节问的比较少,这对于大部分应聘者来说是好事。所以机会比能力更重要,而投递简历的数量决定了机会的多少,所以抓紧投递简历吧。
本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。