阅读 8718

蚂蚁金服一面:十道经典面试题解析

前言

大家好,我是捡田螺的小男孩。最近编程讨论群有位小伙伴去蚂蚁金服面试了,以下是面试的真题,跟大家一起来讨论怎么回答。

1. 用到分布式事务嘛?为什么用这种方案,有其他方案嘛?

什么是分布式事务

谈到事务,我们就会想到数据库事务,很容易就想到原子性、一致性、持久性、隔离性

分布式事务跟数据库事务有点不一样,它是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。

分布式事务基础

分布式事务需要需要知道CAP理论BASE理论

CAP理论

  • 一致性(C:Consistency):一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。
  • 可用性(A:Availability):可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。
  • 分区容错性(P:Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。

一个分布式系统中,CAP理论它只能同时满足(一致性、可用性、分区容错性)中的两点。

BASE 理论

BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。

  • 基本可用是指,通过支持局部故障而不是系统全局故障来实现的;
  • Soft State表示状态可以有一段时间不同步;
  • 最终一致,最终数据是一致的就可以了,而不是实时保持强一致。

分布式事务的几种解决方案

  • 2PC(二阶段提交)方案,事务的提交分为两个阶段:准备阶段和提交执行方案。
  • TCC(即Try、Confirm、Cancel),它采用了补偿机制,核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
  • 本地消息表,它的核心思想就是将分布式事务拆分成本地事务进行处理。
  • 最大努力通知,实现最大努力通知,可以采用MQ的ack机制。
  • Saga事务,它的核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

业界目前使用本地消息表这种方案是比较多的,它的核心思想就是将分布式事务拆分成本地事务进行处理。可以看一下基本的实现流程图吧:

对于消息发送方:

  • 首先需要有一个消息表,记录着消息状态相关信息。
  • 业务数据和消息表在同一个数据库,即要保证它俩在同一个本地事务。
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到MQ消息队列。
  • 消息会发到消息消费方,如果发送失败,即进行重试。

消息消费方:

  • 处理消息队列中的消息,完成自己的业务逻辑。
  • 此时如果本地事务处理成功,则表明已经处理成功了。
  • 如果本地事务处理失败,那么就会重试执行。
  • 如果是业务上面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

2.JDK6、7、8分别提供了哪些新特性

JDK 6 新特性

  • Desktop类(它允许一个Java应用程序启动本地的另一个应用程序去处理URI或文件请求)
  • 使用JAXB2来实现对象与XML之间的映射
  • 轻量级 Http Server API
  • 插入式注解处理API(lombok框架基于这个特性实现)
  • STAX(是JDK6中一种处理XML文档的API)

JDK 7的新特性

  • switch 支持String字符串类型
  • try-with-resources,资源自动关闭
  • 整数类型如(byte,short,int,long)能够用二进制来表示
  • 数字常量支持下划线
  • 泛型实例化类型自动推断,即”<>”
  • 一个catch中捕获多个异常类型,用(|)分隔开
  • 增强的文件系统
  • Fork/join 框架

JDK8 的新特性

  • lambada表达式
  • 函数式接口
  • 方法引用
  • 默认方法
  • Stream API
  • Optional
  • Date Time API(如LocalDate)
  • 重复注解
  • Base64
  • JVM的新特性(如元空间Metaspace代替持久代)

3. https原理,工作流程

  • HTTPS = HTTP + SSL/TLS,即用SSL/TLS对数据进行加密和解密,Http进行传输。
  • SSL,即Secure Sockets Layer(安全套接层协议),是网络通信提供安全及数据完整性的一种安全协议。
  • TLS,即Transport Layer Security(安全传输层协议),它是SSL3.0的后续版本。

  1. 客户端发起Https请求,连接到服务器的443端口。
  2. 服务器必须要有一套数字证书(证书内容有公钥、证书颁发机构、失效日期等)。
  3. 服务器将自己的数字证书发送给客户端(公钥在证书里面,私钥由服务器持有)。
  4. 客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会生成一个随机的对称密钥,用证书的公钥加密。
  5. 客户端将公钥加密后的密钥发送到服务器。
  6. 服务器接收到客户端发来的密文密钥之后,用自己之前保留的私钥对其进行非对称解密,解密之后就得到客户端的密钥,然后用客户端密钥对返回数据进行对称加密,酱紫传输的数据都是密文啦。
  7. 服务器将加密后的密文返回到客户端。
  8. 客户端收到后,用自己的密钥对其进行对称解密,得到服务器返回的数据。

4. 讲讲java jmm volatile的实现原理

volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。

volatile是如何保证可见性的呢?我们先来看下java内存模型(jmm)

  • Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
  • 为了更好的执行性能,java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存打交道,也没有限制编译器进行调整代码顺序优化。所以Java内存模型会存在缓存一致性问题和指令重排序问题的。
  • Java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。
  • 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作操作主内存。并且每个线程不能访问其他线程的工作内存。

Java内存模型

volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性

指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。volatile是如何禁止指令重排的?在Java语言中,有一个先行发生原则(happens-before)

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
  • 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

实际上volatile保证可见性和禁止指令重排都跟内存屏障有关。我们来看一段volatile使用的demo代码

public class Singleton {  
    private volatile static Singleton instance;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    if (instance == null) {  
        synchronized (Singleton.class) {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        }  
    }  
    return instance;  
    }  
}  
复制代码

编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个「内存屏障」

lock指令相当于一个内存屏障,它保证以下这几点:

  • 1.重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 2.将本处理器的缓存写入内存
  • 3.如果是写入动作,会导致其他处理器中对应的缓存无效。

第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排列的体现。内存屏障又是什么呢?

内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)

内存屏障类型抽象场景描述
LoadLoad屏障Load1; LoadLoad; Load2在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障Store1; StoreStore; Store2在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障Load1; LoadStore; Store2在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障Store1; StoreLoad; Load2在Load2读取操作执行前,保证Store1的写入对所有处理器可见。

为了实现volatile的内存语义,Java内存模型采取以下的保守策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:

内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈~

5. 讲一讲7层网络模型,tcp的为什么要三次握手

计算机网路体系结构有三层:OSI七层模型、TCP/IP四层模型、五层体系结构,如图:

七层模型,亦称OSI(Open System Interconnection),国际标准化组织(International Organization for Standardization)制定的一个用于计算机或通信系统间互联的标准体系。

  • 应用层:网络服务与最终用户的一个接口,常见的协议有:HTTP FTP SMTP SNMP DNS.
  • 表示层:数据的表示、安全、压缩。,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
  • 会话层:建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话.
  • 传输层:定义传输数据的协议端口号,以及流控和差错校验,协议有TCP UDP.
  • 网络层:进行逻辑地址寻址,实现不同网络之间的路径选择,协议有ICMP IGMP IP等.
  • 数据链路层:在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路。
  • 物理层:建立、维护、断开物理连接。

6.说说线程池的工作原理

面试官如果要我们讲下线程池工作原理的话,大家讲下以下这个流程图就可以啦:

为了形象描述线程池执行,加深大家的理解,我打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

  • 当产品提个需求,正式员工(核心线程)先接需求(执行任务)
  • 如果正式员工都有需求在做,即核心线程数已满),产品就把需求先放需求池(阻塞队列)。
  • 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。
  • 如果所有员工(最大线程数也满了)都有需求在做了,那就执行拒绝策略。
  • 如果外包员工把需求做完了,它经过一段(keepAliveTime)空闲时间,就离开公司了。

7.你们数据库的高可用是怎么实现的?

高可用,即High Availability,是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。单机部署谈不上高可用,因为单点故障问题。高可用都是多个节点的,我们在考虑MySQL数据库的高可用的架构时,需要考虑这几个方面:

  • 如果数据库节点宕机,需要尽快回复,保证业务不受宕机影响。
  • 从数据库节点的数据,尽可能跟主节点数据实时保持一致,至少保证最终一致性。
  • 数据库节点切换时,数据不能缺失。

7.1 主从或主主半同步复制

用双节点数据库,搭建单向或者双向的半同步复制。架构如下:

通常会和proxy、keepalived等第三方软件同时使用,即可以用来监控数据库的健康,又可以执行一系列管理命令。如果主库发生故障,切换到备库后仍然可以继续使用数据库。

这种方案优点是架构、部署比较简单,主机宕机直接切换即可。缺点是完全依赖于半同步复制,半同步复制退化为异步复制,无法保证数据一致性;另外,还需要额外考虑haproxy、keepalived的高可用机制。

7.2 半同步复制优化

半同步复制机制是可靠的,可以保证数据一致性的。但是如果网络发生波动,半同步复制发生超时会切换为异步复制,异复制是无法保证数据的一致性的。因此,可以在半同复制的基础上优化一下,尽可能保证半同复制。如双通道复制方案

  • 优点:这种方案架构、部署也比较简单,主机宕机也是直接切换即可。比方案1的半同步复制,更能保证数据的一致性。
  • 缺点:需要修改内核源码或者使用mysql通信协议,没有从根本上解决数据一致性问题。

7.3 高可用架构优化

保证高可用,可以把主从双节点数据库扩展为数据库集群。Zookeeper可以作为集群管理,它使用分布式算法保证集群数据的一致性,可以较好的避免网络分区现象的产生。

  • 优点:保证了整个系统的高可用性,扩展性也较好,可以扩展为大规模集群。
  • 缺点:数据一致性仍然依赖于原生的mysql半同步复制;引入Zookeeper使系统逻辑更复杂。

7.4 共享存储

共享存储实现了数据库服务器和存储设备的解耦,不同数据库之间的数据同步不再依赖于MySQL的原生复制功能,而是通过磁盘数据同步的手段,来保证数据的一致性。

DRBD磁盘复制

DRBD是一个用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案。主要用于对服务器之间的磁盘、分区、逻辑卷等进行数据镜像,当用户将数据写入本地磁盘时,还会将数据发送到网络中另一台主机的磁盘上,这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。常用架构如下:

当本地主机出现问题,远程主机上还保留着一份相同的数据,即可以继续使用,保证了数据的安全。

  • 优点:部署简单,价格合适,保证数据的强一致性
  • 缺点:对IO性能影响较大,从库不提供读操作

7.5 分布式协议

分布式协议可以很好解决数据一致性问题。常见的部署方案就是MySQL cluster,它是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。如下:

  • 优点:不依赖于第三方软件,可以实现数据的强一致性;
  • 缺点:配置较复杂;需要使用NDB储存引擎;至少三节点;

8. 读写分离的场景下,怎么保证从数据库读到最新的数据?

数据库读写分离,主要解决高并发时,提高系统的吞吐量。来看下读写分离数据库模型:

  • 写请求是直接写主库,然后同步数据到从库
  • 读请求一般直接读从库,除飞强制读主库

在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。实际上可以使用缓存标记法

  • A发起写请求,更新主库数据,并在缓存中设置一个标记,表示数据已更新,标记格式为:userId+业务Id。
  • 设置此标记,设置过期时间(估值为主库和从库同步延迟的时间)
  • B发起读请求,先判断此请求,在缓存中有没有更新标记。
  • 如果存在标记,走主库;如果没有,请求走从库。

这个方案,解决了数据不一致问题,但是每次请求都要先跟缓存打交道,会影响系统吞吐。

9. 如何保证MySQL数据不丢?

MySQL这种关系型数据库,是日志先行策略(Write-Ahead Logging),只要binlog和redo log日志能保证持久化到磁盘,我们就能确保MySQL异常重启后,数据不丢失。

binlog日志

binlog,又称为二进制日志,它会记录数据库执行更改的所有操作,但是不包括查询select等操作。一般用于恢复、复制等功能。它的格式有三种:statement、mixed和row

  • statement:每一条会修改数据的sql都会记录到binlog中,不建议使用。
  • row:基于行的变更情况记录,会记录行更改前后的内容,推荐使用
  • mixed:混合statement和row两个模式,不建议使用。

binlog 的写入机制是怎样的呢?

事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把binlog cache写到binlog文件中

系统为每个客户端线程分配一个binlog cache,其大小值控制参数是binlog_cache_size。如果binlog cache的值超过阀值,就会临时持久化到磁盘。当事务提交的时候,再将 binlog cache中完整的事务持久化到磁盘中,并且清空binlog cache。

binlog写文件

binlog写文件分write和fsync两个过程:

  • write:指把日志写到文件系统的page cache,并没有把数据持久化到磁盘,因此速度较快。
  • fsync,实际的写盘操作,即把数据持久化到磁盘。

write和fsync的写入时机,是由变量sync_binlog控制的:

如果IO出现性能瓶颈,可以将sync_binlog设置成一个较大的值。比如设置为(100~1000)。但是,会存在数据丢失的风险,当主机异常重启时,会丢失N个最近提交的事务binlog

redo log日志

redo log,又称为重做日志文件,只记录事务对数据页做了哪些修改,它记录的是数据修改之后的值。redo 有三种状态

  • 物理上是在MySQL进程内存中,存在redo log buffer中,
  • 物理上在文件系统的page cache里,写到磁盘 (write),但是还没有持久化(fsync)。
  • 存在hard disk,已经持久化到磁盘。

日志写到redo log buffer是很快的;wirte到page cache也很快,但是持久化到磁盘的速度就慢多了。

为了控制redo log的写入策略,Innodb根据innodb_flush_log_at_trx_commit参数不同的取值采用不同的策略,它有三种不同的取值:

    1. 设置为0时,表示每次事务提交时都只是把redo log留在redo log buffer 中 ;
    1. 设置为1时,表示每次事务提交时都将 redo log 直接持久化到磁盘;
    1. 设置为2时,表示每次事务提交时都只是把redo log 写到page cache。

三种模式下,0的性能最好,但是不安全,MySQL进程一旦崩溃会导致丢失一秒的数据。1的安全性最高,但是对性能影响最大,2的话主要由操作系统自行控制刷磁盘的时间,如果仅仅是MySQL宕机,对数据不会产生影响,如果是主机异常宕机了,同样会丢失数据。

10. 高并发下如何设计秒杀系统?

设计一个秒杀系统,需要考虑这些问题:

如何解决这些问题呢?

  • 页面静态化
  • 按钮至灰控制
  • 服务单一职责
  • 秒杀链接加盐
  • 限流
  • 分布式锁
  • MQ异步处理
  • 限流&降级&熔断

页面静态化

秒杀活动的页面,大多数内容都是固定不变的,如商品名称,商品图片等等,可以对活动页面做静态化处理,减少访问服务端的请求。秒杀用户会分布在全国各地,有的在上海,有的在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到活动页面,可以使用CDN(Content Delivery Network,内容分发网络)。CDN可以让用户就近获取所需内容。

按钮至灰控制

秒杀活动开始前,按钮一般需要置灰的。只有时间到了,才能变得可以点击。这是防止,秒杀用户在时间快到的前几秒,疯狂请求服务器,然后秒杀时间点还没到,服务器就自己挂了。

服务单一职责

我们都知道微服务设计思想,也就是把各个功能模块拆分,功能那个类似的放一起,再用分布式的部署方式。

如用户登录相关的,就设计个用户服务,订单相关的就搞个订单服务,再到礼物相关的就搞个礼物服务等等。那么,秒杀相关的业务逻辑也可以放到一起,搞个秒杀服务,单独给它搞个秒杀数据库。

服务单一职责有个好处:如果秒杀没抗住高并发的压力,秒杀库崩了,服务挂了,也不会影响到系统的其他服务。

秒杀链接加盐

链接如果明文暴露的话,会有人获取到请求Url,提前秒杀了。因此,需要给秒杀链接加盐。可以把URL动态化,如通过MD5加密算法加密随机的字符串去做url。

限流

一般有两种方式限流:nginx限流和redis限流。

  • 为了防止某个用户请求过于频繁,我们可以对同一用户限流
  • 为了防止黄牛模拟几个用户请求,我们可以对某个IP进行限流
  • 为了防止有人使用代理,每次请求都更换IP请求,我们可以对接口进行限流
  • 为了防止瞬时过大的流量压垮系统,还可以使用阿里的Sentinel、Hystrix组件进行限流。

分布式锁

可以使用redis分布式锁解决超卖问题。

使用Redis的SET EX PX NX + 校验唯一随机值,再删除释放锁。

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}
复制代码

在这里,判断是不是当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

为了更严谨,一般也是用lua脚本代替。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;
复制代码

MQ异步处理

如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来的时候,先放到消息队列,再拿出来消费。

限流&降级&熔断

  • 限流,就是限制请求,防止过大的请求压垮服务器;
  • 降级,就是秒杀服务有问题了,就降级处理,不要影响别的服务;
  • 熔断,服务有问题就熔断,一般熔断降级是一起出现。

参考与感谢

文章分类
后端
文章标签