大家好, 最近编程讨论群有位小伙伴去蚂蚁金服面试了,以下是面试的真题,跟大家一起来讨论怎么回答。
谈到事务,我们就会想到数据库事务,很容易就想到原子性、一致性、持久性、隔离性。
分布式事务跟数据库事务有点不一样,它是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。
分布式事务需要需要知道CAP理论和BASE理论。
一个分布式系统中,CAP理论它只能同时满足(一致性、可用性、分区容错性)中的两点。
BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。
业界目前使用本地消息表这种方案是比较多的,它的核心思想就是将分布式事务拆分成本地事务进行处理。可以看一下基本的实现流程图吧:
对于消息发送方:
消息消费方:
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
JDK 6 新特性
JDK 7的新特性
JDK8 的新特性
volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。
volatile是如何保证可见性的呢?我们先来看下java内存模型(jmm)
volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。
指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。volatile是如何禁止指令重排的?在Java语言中,有一个先行发生原则(happens-before)
实际上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指令相当于一个内存屏障,它保证以下这几点:
第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内存模型采取以下的保守策略
有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:
内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈~
计算机网路体系结构有三层:OSI七层模型、TCP/IP四层模型、五层体系结构,如图:
七层模型,亦称OSI(Open System Interconnection),国际标准化组织(International Organization for Standardization)制定的一个用于计算机或通信系统间互联的标准体系。
面试官如果要我们讲下线程池工作原理的话,大家讲下以下这个流程图就可以啦:
为了形象描述线程池执行,加深大家的理解,我打个比喻:
高可用,即High Availability,是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。单机部署谈不上高可用,因为单点故障问题。高可用都是多个节点的,我们在考虑MySQL数据库的高可用的架构时,需要考虑这几个方面:
用双节点数据库,搭建单向或者双向的半同步复制。架构如下:
通常会和proxy、keepalived等第三方软件同时使用,即可以用来监控数据库的健康,又可以执行一系列管理命令。如果主库发生故障,切换到备库后仍然可以继续使用数据库。
这种方案优点是架构、部署比较简单,主机宕机直接切换即可。缺点是完全依赖于半同步复制,半同步复制退化为异步复制,无法保证数据一致性;另外,还需要额外考虑haproxy、keepalived的高可用机制。
半同步复制机制是可靠的,可以保证数据一致性的。但是如果网络发生波动,半同步复制发生超时会切换为异步复制,异复制是无法保证数据的一致性的。因此,可以在半同复制的基础上优化一下,尽可能保证半同复制。如双通道复制方案
保证高可用,可以把主从双节点数据库扩展为数据库集群。Zookeeper可以作为集群管理,它使用分布式算法保证集群数据的一致性,可以较好的避免网络分区现象的产生。
★共享存储实现了数据库服务器和存储设备的解耦,不同数据库之间的数据同步不再依赖于MySQL的原生复制功能,而是通过磁盘数据同步的手段,来保证数据的一致性。
”
DRBD磁盘复制
DRBD是一个用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案。主要用于对服务器之间的磁盘、分区、逻辑卷等进行数据镜像,当用户将数据写入本地磁盘时,还会将数据发送到网络中另一台主机的磁盘上,这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。常用架构如下:
当本地主机出现问题,远程主机上还保留着一份相同的数据,即可以继续使用,保证了数据的安全。
分布式协议可以很好解决数据一致性问题。常见的部署方案就是MySQL cluster,它是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。如下:
数据库读写分离,主要解决高并发时,提高系统的吞吐量。来看下读写分离数据库模型:
在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。实际上可以使用缓存标记法。
这个方案,解决了数据不一致问题,但是每次请求都要先跟缓存打交道,会影响系统吞吐。
MySQL这种关系型数据库,是日志先行策略(Write-Ahead Logging),只要binlog和redo log日志能保证持久化到磁盘,我们就能确保MySQL异常重启后,数据不丢失。
binlog,又称为二进制日志,它会记录数据库执行更改的所有操作,但是不包括查询select等操作。一般用于恢复、复制等功能。它的格式有三种:statement、mixed和row。
binlog 的写入机制是怎样的呢?
★事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把binlog cache写到binlog文件中 。
”
系统为每个客户端线程分配一个binlog cache,其大小值控制参数是binlog_cache_size。如果binlog cache的值超过阀值,就会临时持久化到磁盘。当事务提交的时候,再将 binlog cache中完整的事务持久化到磁盘中,并且清空binlog cache。
binlog写文件
binlog写文件分write和fsync两个过程:
write和fsync的写入时机,是由变量sync_binlog控制的:
如果IO出现性能瓶颈,可以将sync_binlog设置成一个较大的值。比如设置为(100~1000)。但是,会存在数据丢失的风险,当主机异常重启时,会丢失N个最近提交的事务binlog。
redo log,又称为重做日志文件,只记录事务对数据页做了哪些修改,它记录的是数据修改之后的值。redo 有三种状态
日志写到redo log buffer是很快的;wirte到page cache也很快,但是持久化到磁盘的速度就慢多了。
为了控制redo log的写入策略,Innodb根据innodb_flush_log_at_trx_commit参数不同的取值采用不同的策略,它有三种不同的取值:
★三种模式下,0的性能最好,但是不安全,MySQL进程一旦崩溃会导致丢失一秒的数据。1的安全性最高,但是对性能影响最大,2的话主要由操作系统自行控制刷磁盘的时间,如果仅仅是MySQL宕机,对数据不会产生影响,如果是主机异常宕机了,同样会丢失数据。
”
设计一个秒杀系统,需要考虑这些问题:
如何解决这些问题呢?
页面静态化
秒杀活动的页面,大多数内容都是固定不变的,如商品名称,商品图片等等,可以对活动页面做静态化处理,减少访问服务端的请求。秒杀用户会分布在全国各地,有的在上海,有的在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到活动页面,可以使用CDN(Content Delivery Network,内容分发网络)。CDN可以让用户就近获取所需内容。
按钮至灰控制
秒杀活动开始前,按钮一般需要置灰的。只有时间到了,才能变得可以点击。这是防止,秒杀用户在时间快到的前几秒,疯狂请求服务器,然后秒杀时间点还没到,服务器就自己挂了。
服务单一职责
我们都知道微服务设计思想,也就是把各个功能模块拆分,功能那个类似的放一起,再用分布式的部署方式。
★如用户登录相关的,就设计个用户服务,订单相关的就搞个订单服务,再到礼物相关的就搞个礼物服务等等。那么,秒杀相关的业务逻辑也可以放到一起,搞个秒杀服务,单独给它搞个秒杀数据库。
”
服务单一职责有个好处:如果秒杀没抗住高并发的压力,秒杀库崩了,服务挂了,也不会影响到系统的其他服务。
秒杀链接加盐
链接如果明文暴露的话,会有人获取到请求Url,提前秒杀了。因此,需要给秒杀链接加盐。可以把URL动态化,如通过MD5加密算法加密随机的字符串去做url。
限流
一般有两种方式限流:nginx限流和redis限流。
分布式锁
可以使用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异步处理
如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来的时候,先放到消息队列,再拿出来消费。
限流&降级&熔断
[1]五大常见的MySQL高可用方案: https://zhuanlan.zhihu.com/p/25960208
[2]读写分离数据库如何保持数据一致性: https://blog.csdn.net/baidu_36161424/article/details/107712388
[3]《我们一起进大厂》系列-秒杀系统设计: https://juejin.cn/post/6844903999083151374#heading-11
[4]《极客时间:MySQL45讲实战》: http://gk.link/a/10vPr
[5]MySQL是如何保证不丢数据的(一): https://cloud.tencent.com/developer/article/1674625
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8