知乎回答
消息重传
当配置Kafka集群的时候,Followe中有一个ISR集合,当leader发生故障的时候,新选举出来的leader会从ISR中选出来,配置参数ack为-1的时候,生产者没发送一条信息除了发送给leader还会保证所有的ISR也会收到。
在一条消息在处理之后,消费者恰好宕机了,那会因为没有更新消费进度,所以当消费者重启之后,还会重复的消费这条消息。
- 消息生产的幂等,
- 消费端的幂等性会复杂些,可以从通用层和业务层进行考虑。
在通用层面,可以在消息生产的时候,使用信号发生器给它生成一个全局唯一的ID,消息被处理之后,ID存储在数据库,在处理下一条消息之前,先从数据库查询这个ID是否被消费,如果被消费过就不再消费。不过这样会有一个问题:要保证消息的处理和写入数据库具有事务性。
在业务层面,可以采用乐观锁的方式,具体操作如下:给每个人账户数据加一个版本号字段,在生产消息时先查询这个账户版本号,并且将这个版本号连同消息一起发送给消息队列。消费端在拿到消息和版本号后,在执行更新账户余额SQL的时候带上版本号。
希望你能在后续的分布式系统的开发中,不仅掌握流量削峰、延迟响应、体验降级、过载保护 这 4 板斧,更能理解这 4 板斧背后的妥协折中,从而灵活地处理不可预知的突发问 题。
- 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
- 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
- 数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。
读热点加缓存,写热点加缓冲,例如用消息队列,写热点也可以合并写。
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。 例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题。
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。
单表数据拆分有两种方式:垂直分表和水平分表。
垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。 例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
水品分表
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
路由水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。常见的路由算法有:
范围路由: 选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。以最常见的用户 ID 为例,路由算法可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到数据库 1 的表中,1000000 ~ 1999999 放到数据库 2 的表中,以此类推。范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。
Hash路由: Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
配置路由: 配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户 ID 为例,我们新增一张 user_router 表,这个表包含 user_id 和 table_id 两列,根据 user_id 就可以查询对应的 table_id。
如何做到不迁移数据库和避免热点问题
从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。
所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。通常,这些节点可以表示为一个系统或者一个新的 Socket 连接(比如代理服务器只是创建 一个新的 Socket 连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。
一点是提高单次请求的效率, 一点是减少没必要的请求。
“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。 也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据 中是否含有和访问者相关的个性化数据。
分离出动态内容之后,如何组织这些内容页就变得非常关键了。
热点分为热点操作和热点数据。 对系统来说,这些操作可以抽象为 “读请求”和“写请求”,这两种热点请求的处理方式大相径庭,读请求的优化空间要大一些,而 写请求的瓶颈一般都在存储层,优化的思路就是根据 CAP 理论做平衡,这个内容我在“减库存”一文再详细介绍。
发现静态热点数据:
发现动态热点数据:
服务器的处理资源是恒定的,你用或者不用它的处理能力都是一样的,所以出现峰值的话,很容易导致忙到处理不过来,闲的时候却又没有什么要处理。但是由于要保证服务质量,我们的很多处理资源只能按照忙的时候来预估,而这会导致资源的一个浪费。削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。
- 利用线程池加锁等待也是一种常用的排队方式;
- 先进先出、先进后出等常用的内存排队算法的实现方式;
- 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制) 来恢复请求等方式。
第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊。
第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用
- 将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读
- 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
- 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
- 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
- 对写数据进行强一致性校验,只保留最后有效的数据
总 QPS =(1000ms / 响应时间)× 线 程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求 的线程数。真正对性能有影响的是 CPU 的执行时间。
线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量
对缓存系统而言,制约它的是内 存,而对存储型系统来说 I/O 更容易是瓶颈。这个定位的场景是秒杀,它的瓶颈更多地发生在 CPU 上。
一个办法就是看当 QPS 达到极限时,你的服务器的 CPU 使用率是不是超过了 95%,如果没有超过,那么表示 CPU 还有提升的空间,要么是有锁限制,要么是有过多的本地 I/O 等待发生。
减少编码、减少序列化、Java 极致优化、并发读优化
千万不要超卖,这是大前提。
下单时直接通过数据库的事务机制控制商品库 存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也 就可能出现下单成功数远远超过真正库存数的情况
下单时先预扣,在规定时间内不付款再释放库存,针对 恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。例如,给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给 某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复 下单不付款的操作进行次数限制等。
说一下秒杀场景下减库存的极致优化思路,包括如何在缓存中减库存以及如何在数据库中减库存。
秒杀商品和普通商品的减库存还是有些差异的,例如商品数量比较少,交易时间段也比较短,因此这里有一个大胆的假设,即能否把秒杀商品减库存直接放到缓存系统中实现,也就是直接在缓存中减库存或者在一个带有持久化功能的缓存系统(如 Redis)中完成呢?
如果你的秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关 系的话,我觉得完全可以。但是如果有比较复杂的减库存逻辑,或者需要使用事务,你还是必须在数据库中完成减库存。
单个热点商品会影响整个数据库的性能,导致 0.01% 的商品影响 99.99% 的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍 的原则进行隔离,把热点商品放到单独的热点库中。 但是这无疑会带来维护上的麻烦,比如 要做热点数据的动态迁移以及单独的数据库等。
而分离热点商品到单独的数据库还是没有解决并发锁的问题,我们应该怎么办呢?要解决并 发锁的问题,有两种办法
应用层排队、数据库层排队
说到系统的高可用建设,它其实是一个系统工程。降级、限流和 拒绝服务。
其实我这里所说的排队,更多地是说在服务端的服务调用之间采用排队的策略。
服务端接受请求本身就是按照请求顺序处理的,而且这个处理在 Web 层是实时同步的,处理的结果也会立马就返回给用户。但是我前面也说了,整个请求的处理涉及很多服务调用也涉及很多其他的系统,也会有部分的处理需要排队,所以可能有部分先到的请求由于后面的一些排队的服务拖慢,导致最终整个请求处理完成的时间反而比较后面的 请求慢的情况。
一是页面中采用轮询的方式定时主动去服务端查询结果,例如每秒请求一次服务端看看有 没有处理结果(现在很多支付页面都采用了这种策略),这种方式的缺点是服务端的请求 数会增加不少。
二是采用主动 push 的方式,这种就要求服务端和客户端保持连接了,服务端处理完请求 主动 push 给客户端,这种方式的缺点是服务端的连接数会比较多。
比如阿里的双十一并发下单支持10w的QPS,虽然是 的10w但是落到实际的数据库层多个库的多台机器上,因为我们可以根据用户请求的商品ID进行分库分表,这样可以大大减少并发度。
秒杀系统的架构分析与实战
1、秒杀页面的展示
2、倒计时
产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求; JS层面,限制用户在x秒之内只能提交一次请求
3、 站点层设计
前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面;同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面。
用户请求预处理模块
经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可,不必再进一步发送事务了。
服务+数据库+缓存一套”的方式提供数据访问,用cache提高读性能。不管采用主从的方式扩展读性能,还是缓存的方式扩展读性能,数据都要复制多份(主+从,db+cache),一定会引发一致性问题。
4. 直接面向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入。当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。
5. 重启与过载保护
6. 超卖
- 虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。
全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。
乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。
当对象间存在一对多关系时,则使用观察者模式(Observer
Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
zookeeper的watch机制就是观者设计模式的很好体现。
观察者设模式详解
使用场景:
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
SLF4J是简单的日志外观模式框架,抽象了各种日志框架例如Logback、Log4j、Commons-logging和JDK自带的logging实现接口。它使得用户可以在部署时使用自己想要的日志框架。SLF4J是轻量级的,在性能方面几乎是零消耗的。
详解适配器模式
HBase系统架构
很多分布式锁
Redis分布式锁的正确实现方式
- 内存切换以使用新的地址空间
- 切换内核栈和硬件上下文
- 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
- 线程上下文切换和进程上下文切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
- 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
调度算法
- 吞吐量与响应时间的矛盾
响应时间少 — 切换次数多 — 系统内耗多 — 》吞吐量小- 前台任务和后台任务的关注点不同
前台任务关注响应时间,后台任务关注周转时间- IO约束型任务和CPU约束型任务
IO任务优先级应该高些
具体算法
浅谈Linux零拷贝
什么是操作系统中的虚拟内存?
Epoll原理介绍
进程线程区别
(信号量:是操作系统提供的一种协调共享资源访问的方法、
管程是一种用于多线程互斥访问共享资源的程序结构)
进程间的软件中断通知和处理机制;
不足:传送的信息量小,只有一个信号类型
进程间基于内存文件的通信机制、间接通信
管道是一种半双工的通信方式,数据只能单项流动,并且只能在具有亲缘关系的进程间流动,进程的亲缘关系通常是父子进程。
命名管道也是半双工的通信方式,它允许无亲缘关系的进程间进行通信。
进程不知道(或不关心)另一端,可能从键盘、文件、程序读取,可能写入到终端、文件、程序。
消息队列是由操作系统维护的以字节序列为基本单位的间接通信机制
可以实现两个生命周期不同的进程进行通信。
允许任意进程通过共享消息队列来实现进程间通信.并由系统调用函数来实现消息发送和接收之间的同步.从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题.使用方便,但是信息的复制需要额外消耗CPU的时间.不适宜于信息量大或操作频繁的场合。
共享内存是把同一个物理内存区域同时共映射到多个进程的内存地址空间的通信机制。
每个进程都私有内存地址空间,每个进程的内存地址空间必须明确设置共享内存段,而同一进程中的线程总是共享相同的内存地址空间。
优点:快速分表,没有数据复制、没用系统调用干预。
缺点:必须用额外的同步机制来协调数据的访问。
产生死锁的原因及解决方案:
互斥:把互斥的共享资源封装成可同时访问
持有并等待:请求资源时,要求它不持有任何资源;仅允许进程开始执行时,一次请求所有需要的资源。(资源利用率低)
非抢占:如进程请求资源不能立即分配的资源,责释放已占有的资源。
循环等待:对资源排序,要求进程按顺序请求资源。
死锁避免:(银行家算法)
当进程请求资源时,系统判断分配后是否处于安全状态。
级别低的域写在最左面,级别高的域名写在最右面。
类似树的结构,根root,根的下一级是顶级域名,顶级域名可划分为子域。
HTTP协议格式详解
HTTP缓存机制
HTTP缓存补充
面试官问你Http2.0
HTTP中Get Post Put区别
可靠传输的工作原理:停止等待协议和连续ARQ
- 发送窗口表示:发送方A,在没有收到B的确认下,A可以连续把窗口内的数据都发送出去。凡是已发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传使用。
- 发送窗口的位置由窗口前沿和后沿的位置共同确定。
- 接收窗口:对于不按序到达的数据该如何处理,TCP标准并无明确确定,如果接收方直接丢弃,管理简单但是会浪费网络资源。所以大多是对不按序到达的数据先临时保存在接收窗口,等到字节流中缺少字节补充后,交给应用层。
- TCP要求接收方必须有累计确认功能。
报文段的往返时间RTT,超时重传时间RTO,这里面设计一种自适应算法,
例如发出一个报文段,设定重传时间到了,还没收到确认,那么会重发,经过一段时间,收到确认的报文,不好区分是确认哪个报文的。所以对算法进行修正,报文段重传一次,就把超时重传时间RTO增大一些。典型做法是取新的重传时间为2倍的旧重传时间,当不发生报文重传时,重新计算超时重传时间。
RFC2018有明确规定,并没有指明发送方应当怎样响应SACK。
解决tcp time wait过多
关于TCP/IP常见知识点
以上补充
关于IP分片了解下
UDP变可靠
Time Wait过多解决办法
JDK排序
关闭线程池_1
关闭线程池_2
shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。
调用完shutdownNow和shuwdown方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。
threadPool.shutdown(); // Disable new tasks from being submitted
// 设定最大重试次数
try {
// 等待 60 s
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 调用 shutdownNow 取消正在执行的任务
threadPool.shutdownNow();
// 再次等待 60 s,如果还未结束,可以再次尝试,或者直接放弃
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("线程池任务未正常执行结束");
}
} catch (InterruptedException ie) {
// 重新调用 shutdownNow
threadPool.shutdownNow();
}
public SynchronousQueue(boolean fair) {
//公平模式下使用队列,实现先进先出,非公平模式下使用栈,先进后出
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
SynchronousQueue是一个无空间的队列即不可以通过peek来获取数据或者contain判断数据是否在队列中。
如果新请求与队列tail节点的模式相同,则将请求加入队列,模式不同,则可进行消费从队列中移除节点。
- SynchronousQueue不存储数据,只存储请求
- 当生产或消费请求到达时,如果队列中没有互补的请求,则将会此请求加入队列中,线程进入阻塞等待互补的请求到达。
- 若是互补的请求到达时,则唤醒队列中的线程,消费请求使用生产请求中的数据内容。
AQS实现原理
final域的重排序规则:
Collections 继承 Iterable
CopyOnWriteArrayList
Set — SortedSet — TreeSet
ConcurrentSkipListSet
ArrayDeque是Deque的实现类,可以作为栈来使用,效率高于Stack;也可以作为队列来使用,效率高于LinkedList。需要注意的是,ArrayDeque不支持null值。
ConcurrentHashMap transfer 源码分析
深入理解HashMap+ConcurrentHashMap的扩容策略
Java 8系列之重新认识HashMap
ConcurrentHashMap
ConcurrentHashMap size 改为mappingCount
内部类:
1、内部类中的变量和方法不能声明为静态的。
2、内部类实例化:B是A的内部类,实例化B:A.B b = new A().new B()。
3、内部类可以引用外部类的静态或者非静态属性及方法。
静态内部类:
1、静态内部类属性和方法可以声明为静态的或者非静态的。
2、实例化静态内部类:B是A的静态内部类,A.B b = new A.B()。
3、静态内部类只能引用外部类的静态的属性及方法。
inner classes——内部类
static nested classes——静态嵌套类
其实人家不叫静态内部类,只是叫习惯了,从字面就很容易理解了。
内部类依靠外部类的存在为前提,而静态嵌套类则可以完全独立,明白了这点就很好理解了。
原理介绍
使用场景
创建线程的当前线程就是新线程的父线程,新线程的一些资源来自于这个父线程,借助于当前正在运行的线程,对新创建线程进行一些必要的赋值与初始化。
InheritableThreadLocal继承了ThreadLocal,
application/json 四种常见的 POST 提交数据方式
代理
AOP细节
抽象类可以有构造方法,接口中不能有构造方法。
抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
抽象类中可以包含静态方法,接口中不能包含静态方法
抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
一个类可以实现多个接口,但只能继承一个抽象类
都不能被实例化
接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
Java1.8接口新增 只能定义default和static类型的方法。
1、首先初始化上下文,生成ClassPathXmlApplicationContext对象,在获取resourcePatternResolver对象将xml解析成Resource对象。
2、利用1生成的context、resource初始化工厂,并将resource解析成beandefinition,再将beandefinition注册到beanfactory中。
具体描述(个人总结):
applicationContext.xml, 这是个资源文件,由于我们的bean都在里边进行配置定义,那Spring总得对这个文件进行读取并解析。
- Resource表示资源的抽象(策略模式)
- ResourceLoader组件,该组件负责对Spring资源的加载,资源指的是xml、properties等文件资源,返回一个对应类型的Resource对象。
- ApplicationContext,AbstractApplication是实现了ResourceLoader的,这说明什么呢?说明我们的应用上下文ApplicationContext拥有加载资源的能力,这也说明了为什么可以通过传入一个String resource path给ClassPathXmlApplicationContext(“applicationContext.xml”)就能获得xml文件资源的原因了。
- 我们拥有了加载器ResourceLoader,也拥有了对资源的描述Resource,但是我们在xml文件中声明的bean/>标签在Spring又是怎么表示的呢?注意这里只是说对bean的定义,而不是说如何将bean/>转换为bean对象。于是就引入一个叫BeanDefinition的组件。
- 我们的Resource资源是怎么转成我们的BeanDefinition的呢?因此就引入了BeanDefinitionReader组件,。
- 你有了BeanDefinition后,你还必须将它们注册到工厂中去,所以当你使用getBean()方法时工厂才知道返回什么给你。还有一个问题既然要保存注册这些bean,那肯定要有个数据结构充当容器吧!没错,就是一个Map。BeanDefinitionRegistry
- ApplicationContext上下文基本直接或间接贯穿所有的部分,因此我们一般称之为容器,除此之外,ApplicationContext还拥有除了bean容器这种角色外,还包括了获取整个程序运行的环境参数等信息(比如JDK版本,jre等),其实这部分Spring也做了对应的封装,称之为Enviroment。
- 调用super(parent)方法为容器设置好Bean资源加载器,该方法最终会调用到AbstractApplicationContext的无参构造方法
- setConfigLocations(configLocations)设置Bean定义资源文件的定位路径
- ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();。这句代码的作用是告诉子类启动refreshBeanFactory方法以及通过getBeanFactory获得beanFactory。
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { this.refreshBeanFactory(); return this.getBeanFactory(); }
- refreshBeanFactory: 创建一个新的bean工厂(createBeanFactory())->将所有BeanDefinition载入beanFactory中,此处依旧是模板方法,具体由子类实现(loadBeanDefinitions(beanFactory); )
仔细描述
IOC控制反转可以理解为面向对象的一种补充。IOC主要实现策略依赖查找、依赖注入。
透彻的掌握 Spring 中@transactional 的使用
BeanFactory和FactoryBean
递归回溯的模板
动态规划-背包问题
最短路径算法
int[][] dp = new int[n + 1][n + 1];
for (int len = 2; len <= n; len++) { //区间长度
for (int i = 1; i <= n - len + 1; i++) { //区间起点
int j = i + len - 1; //区间终点
for (int k = i; k < j; k++) {
dp[i][j] = Math.min(dp[i][j],
dp[i][k] + dp[k + 1][j] +
preSum[j] - preSum[i - 1]);
}
}
}
System.out.println(dp[1][n]); //dp[1][n]
MVCC最大的优势:读不加锁,读写不冲突。在读多写少的场景下,读写不冲突是非常重要的,极大的增加了系统的并发性能
- InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)。
- 形成undo log链
- 实现一致性读 —— ReadView,解决版本链中哪些版本对当前事务可见。
RC、RR 两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RC、RR 这两个隔离级别的一个很大不同就是生成 ReadView 的时间点不同,RC 在每一次 SELECT 语句前都会生成一个 ReadView,事务期间会更新,因此在其他事务提交前后所得到的 m_ids 列表可能发生变化,使得先前不可见的版本后续又突然可见了。而 RR 只在事务的第一个 SELECT 语句时生成一个 ReadView,事务操作期间不更新。
缓存更新的套路
MySQL索引
MySQL引起CPU过大,怎么办?
死锁和死锁检测:
在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。
这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
淘宝Mysql记录
【mysql】关于ICP、MRR、BKA等特性
普通索引和唯一索引的区别:change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。
- 业务正确性优先。“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本篇文章的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,可以给你多提供一个排查思路。
业务场景- 对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
- 假设一个业务的更新模式是写入之后马上会做查询,增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。
- 调整key的大小
- 调整页的大小
字段函数计算、where month(t_modified) =7;
隐式类型转换、字符串数字之间相互转换
隐式字符编码转换、
第一种方式(即:直接执行 delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。
第三种方式(即:在 20 个连接中同时执行 delete from T limit 500),会人为造成锁冲突。
第二种方式,即:在一个连接中循环执行 20 次 delete from T limit 500。
Java heap leaks(java堆泄漏), Native memory leaks(本机内存泄漏):与Java堆之外的任何不断增长的内存利用率相关联,例如由JNI代码,驱动程序甚至JVM分配。
如何发现??
如何避免??
Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。
对象的内存分配,往大方向上说就是在堆上分配,对象主要分配在新生代的Eden区。
虚拟机提供一个 -XX:PretenureSizeThreshold参数,令大于这个参数的值直接在老年代分配。
如果在Survivor空间中相同年龄所有对象大小大于Survivor空间一半,年龄大等于这个的直接进入老年代。
美团关于GC优化的案例
单次Minor GC时间由以下两部分组成:T1(扫描新生代)和 T2(复制存活对象到Survivor区)。当Eden区较小时,new threshold = 2(动态年龄判断,对象的晋升年龄阈值为2),对象仅经历2次Minor GC后就晋升到老年代,这样老年代会迅速被填满,直接导致了频繁的Major GC。 2. Major GC后老年代使用空间为300M+,意味着此时绝大多数(86% = 2G/2.3G)的对象已经不再存活,也就是说生命周期长的对象占比很小。
CMS是一种获取最短回收停顿时间为目标的收集器,尤其重视响应速度。基于标记清除算法实现。
仅仅是标记一下 GC Roots能直接关联到的对象,速度很快。
并发标记阶段就是进行GC Roots Tracing的过程。
为了修正并发标记期间,因用户程序继续运作导致标记产生的变动的那一部分对象的标记记录。
CMS收集器默认启动回收线程数是(CPU数量+3)/ 4,也就是4个CPU以上时,并发回收收集器最多不占用超过25%的CPU资源,但是如果CPU数量不足4个的时候,CMS对用户程序就很大,会降低程序运行速度。为了解决这种情况,虚拟机提出一种增量式并发收集器,让GC线程和用户线程交替运行,尽量减少GC线程的独占资源的时间。
由于CMS并发清理阶段用户线程依然运行,伴随程序运行垃圾不断产生,这一部分垃圾只能留在下次回收,就是“浮动垃圾”。CMS收集器需要留一部分空间给并发收集时用户程序使用,会有个参数-XX:CMSInitiatingOccupancyFraction,用来调节触发比。要是CMS运行期间预留内存无法满足程序运行需要,那就会进行SerialOld GC。
基于标记-清除算法。
基于标记-整理算法实现的收集器,也就是说不会产生空间碎片。
G1垃圾收集器可以实现基本不牺牲吞吐量的前提下,完成低停顿的内存回收,这是由于它能够极力避免全区域的垃圾收集,之前的垃圾收集器收集的范围时整个新生代或老年代,而G1将整个Java堆划分多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。
美团技术团队关于ZGC的分析
ZGC是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:
1. ZGC为什么会出现,比CMS、G1垃圾收集哪里强?
- 标记阶段,即从GC Roots集合开始,标记活跃对象;
- 转移阶段,即把活跃对象复制到新的内存地址上;
- 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。
2. ZGC对该算法做了重大改进
CMS垃圾收集器
深入浅出G1垃收集器
深入浅出ZGC
Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
(6)CMS垃圾回收器两个参数控制FullGC。
JVM调优
Java调优经验
数组动态扩容导致频繁GC
JVM源码分析之堆外内存完全解读
FullGC案例一
FullGC案例二
FullGC案例三
Java故障排查
GC问题诊断
top 查看占用cpu的进程 pid
top -Hp pid 查看进程中占用cpu过高的线程id tid
printf ‘%x/n’ tid 转化为十六进制
jstack pid |grep tid的十六进制 -A 30 查看堆栈信息定位
top查看占用cpu高的进程
jstat -gcutil pid 时间间隔 查看gc状况
jmap -dump:format=b,file=name.dump pid 导出dump文件
用visualVM分析dump文件
常量池
jdk变化下常量池变化
在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。
一个线程不能start多次
ReentrantLock如何实现公平和非公平
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面(hasQueuedProcessors)。
从ReentrantLock看AQS的原理
AQS详解
Java中的join方法原理详解
TomcatClassLoader
zookeeper使用场景
zookeeper在选举leader时,会停止服务,直到选举成功之后才会再次对外提供服务,这个时候就说明了服务不可用,但是在选举成功之后,因为一主多从的结构,zookeeper在这时还是一个高可用注册中心,只是在优先保证一致性的前提下,zookeeper才会顾及到可用性。但是在在实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性,这是注册中心设计应该遵循的铁律!
在服务发现和健康监测场景下,随着服务规模的增大,无论是应用频繁发布时的服务注册带来的写请求,还是刷毫秒级的服务健康状态带来的写请求,还是恨不能整个数据中心的机器或者容器皆与注册中心有长连接带来的连接压力上,ZooKeeper 很快就会力不从心,而 ZooKeeper 的写并不是可扩展的,不可以通过加节点解决水平扩展性问题。
我们知道 ZooKeeper 的 ZAB 协议对每一个写请求,会在每个ZooKeeper节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,以及宕机之后的数据可恢复,这是非常好的特性,但是我们要问,在服务发现场景中,其最核心的数据-实时的健康的服务的地址列表真的需要数据持久化么?对于这份数据,答案是否定的。
使用 ZooKeeper 作为服务注册中心时,服务的健康检测常利用 ZooKeeper 的 Session 活性 Track机制 以及结合 Ephemeral ZNode的机制,简单而言,就是将服务的健康监测绑定在了 ZooKeeper 对于 Session 的健康监测上,或者说绑定在TCP长链接活性探测上了。这在很多时候也会造成致命的问题,ZK 与服务提供者机器之间的TCP长链接活性探测正常的时候,该服务就是健康的么?答案当然是否定的!注册中心应该提供更丰富的健康监测方案,服务的健康与否的逻辑应该开放给服务提供方自己定义,而不是一刀切搞成了 TCP 活性检测!
在实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性,那么在可用性上,一个本质的问题,如果注册中心(Registry)本身完全宕机了,svcA 调用 svcB链路应该受到影响么?是的,不应该受到影响。
服务调用(请求响应流)链路应该是弱依赖注册中心,必须仅在服务发布,机器上下线,服务扩缩容等必要时才依赖注册中心。
这需要注册中心仔细的设计自己提供的客户端,客户端中应该有针对注册中心服务完全不可用时做容灾的手段,例如设计客户端缓存数据机制(我们称之为 client snapshot)就是行之有效的手段。另外,注册中心的 health check 机制也要仔细设计以便在这种情况不会出现诸如推空等情况的出现。
ZooKeeper的原生客户端并没有这种能力,所以利用 ZooKeeper 实现注册中心的时候我们一定要问自己,如果把 ZooKeeper 所有节点全干掉,你生产上的所有服务调用链路能不受任何影响么?而且应该定期就这一点做故障演练。
Hystrix断路器 、服务降级、接近实时的监控,是一个用于处理分布式系统的延迟和容错的开源库。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。当某个服务单元发生故障之后,通过断路器的监控,向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证服务调用方的线程不会被长时间、不必要的占用,从而避免了故障咋分布式系统的蔓延。乃至雪崩。
OceanBase系统内部按照时间线将数据划分为基线数据和增量数据,基线数据是只读的,所有的修改更新到增量数据中,系统内部通过合并操作定期将增量数据融合到基线数据中。
从模块划分的角度看,OceanBase可以划分为四个模块:主控服务器RootServer、更新服务器UpdateServer、基线数据服务器ChunkServer以及合并服务器MergeServer。
RootServer:管理集群中的所有服务器,子表(tablet)数据分布以及副本管理。 RootServer一般为一主一备,主备之间数据强同步。
RootServer管理集群中的所有MergeServer、ChunkServer以及UpdateServer。每个集群由各自的RootServer负责数据划分、负载均衡、集群服务器管理等操作。每个集群内部同一时刻只允许一个UpdateServer提供写服务,这个UpdateServer成为主UpdateServer。这种方式通过牺牲一定的可用性获取了强一致性。RootServer通过租约(Lease)机制选择唯一的主UpdateServer,当原先的主UpdateServer发生故障后,RootServer能够在原先的租约失效后选择一台新的UpdateServer作为主UpdateServer。另外,RootServer与MergeServer&ChunkServer之间保持心跳(heartbeat),从而能够感知到在线和已经下线的MergeServer&ChunkServer机器列表。
UpdateServer:UpdateServer是集群中唯一能够接受写入的模块,每个集群中只有一个主Update-Server。UpdateServer中的更新操作首先写入到内存表,当内存表的数据量超过一定值时,可以生成快照文件并转储到SSD中。UpdateServer一般为一主一备。OceanBase支持强一致性和跨行跨表事务。
OceanBase所有写事务最终都落到UpdateServer,而UpdateServer逻辑上是一个单点,支持跨行跨表事务,实现上借鉴了传统关系数据库的做法。
ChunkServer:存储OceanBase系统的基线数据。基线数据一般存储两份或者三份,可配置。ChunkServer的功能包括:存储多个子表,提供读取服务,执行定期合并以及数据分发。
MergeServer:接收并解析用户的SQL请求,经过词法分析、语法分析、查询优化等一系列操作后转发给相应的ChunkServer或者UpdateServer。如果请求的数据分布在多台ChunkServer上,MergeServer还需要对多台ChunkServer返回的结果进行合并。客户端和MergeServer之间采用原生的MySQL通信协议,MySQL客户端可以直接访问MergeServer。
结合业务特点:(读多写少)OceanBase决定采用单台更新服务器来记录最近一段时间的修改增量,而以前的数据保持不变,以前的数据称为基线数据。
基线数据以类似分布式文件系统的方式存储于多台基线数据服务器中,每次查询都需要把基线数据和增量数据融合后返回给客户端。这样,写事务都集中在单台更新服务器上(这台服务器配置要相对较好),避免了复杂的分布式事务,高效地实现了跨行跨表事务;
更新服务器上的修改增量能够定期分发到多台基线数据服务器中,避免成为瓶颈,实现了良好的扩展性。
UpdateServer单点,这个问题限制了OceanBase集群的整体读写性能。
Redis数据结构以及内部实现
跳跃表SkipList
1)Redis提供了两种持久化方式:RDB和AOF。
2)RDB使用一次性生成内存快照的方式,产生的文件紧凑压缩比更高,因此读取RDB恢复速度更快。由于每次生成RDB开销较大,无法做到实时持久化,一般用于数据冷备和复制传输。
3)save命令会阻塞主线程不建议使用,bgsave命令通过fork操作创建子进程生成RDB避免阻塞。
4)AOF通过追加写命令到文件实现持久化,通过appendfsync参数可以控制实时/秒级持久化。因为需要不断追加写命令,所以AOF文件体积逐渐变大,需要定期执行重写操作来降低文件体积。
5)AOF重写可以通过auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数控制自动触发,也可以使用bgrewriteaof命令手动触发。
6)子进程执行期间使用copy-on-write机制与父进程共享内存,避免内存消耗翻倍。AOF重写期间还需要维护重写缓冲区,保存新的写入命令避免数据丢失。
7)持久化阻塞主线程场景有:fork阻塞和AOF追加阻塞。fork阻塞时间跟内存量和系统有关,AOF追加阻塞说明硬盘资源紧张。
8)单机下部署多个实例时,为了防止出现多个子进程执行重写操作,建议做隔离控制,避免CPU和IO资源竞争。
- 当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创建子进程,对于大多数操作系统来说fork是个重量级错误。虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。
- 控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB以内。
- 合理配置Linux内存分配策略,避免物理内存不足导致fork失败,具体细节见12.1节“Linux配置优化”。
- 降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。
不要和其他CPU密集型服务部署在一起,造成CPU过度竞争。
有写时复制机制(copy-on-write),父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。
当开启AOF功能的Redis用于高流量写入场景时,如果使用普通机械磁盘,写入吞吐一般在100MB/s左右,这时Redis实例的瓶颈主要在AOF同步硬盘上。配置no-appendfsync-on-rewrite=yes时,在极端情况下可能丢失整个AOF重写期间的数据,需要根据数据安全性决定是否配置。
1)主线程负责写入AOF缓冲区。
2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
3)主线程负责对比上次AOF同步时间:
- 如果距上次同步成功时间在2秒内,主线程直接返回。
- 如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。
通过对AOF阻塞流程可以发现两个问题:
1)everysec配置最多可能丢失2秒数据,不是1秒。
2)如果系统fsync缓慢,将会导致Redis主线程阻塞影响效率。
Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。当Redis用于高并发场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于我们的应用来说都是噩梦。导致阻塞问题的场景大致分为内在原因和外在原因:
不合理地使用API或数据结构 Redis原生提供慢查询统计功能,执行slowlog get{n}命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。如果命令执行时间在毫秒级,则实例实际QPS只有1000左右。慢查询队列长度默认128,可适当调大。(慢查询本身只记录了命令执行时间,不包括数据网络传输时间和命令排队时间,因此客户端发生阻塞异常后,可能不是当前命令缓慢,而是在等待其他命令执行。)
CPU饱和虽然采用ziplist编码后hash结构内存占用会变小,但是操作变得更慢且更消耗CPU。ziplist压缩编码是Redis用来平衡空间和效率的优化手段,不可过度使用。
持久化阻塞
Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS)。
字符串之所以采用预分配的方式是防止修改操作需要不断重分配内存和字节数据拷贝。但同样也会造成内存的浪费。
尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化。
同样的数据使用ziplist编码的hash类型存储比string类型节约内存。
hash-ziplist类型比string类型写入耗时,但随着value空间的减少,耗时逐渐降低。使用hash重构后节省内存量效果非常明显,特别对于存储小对象的场景,内存只有不到原来的1/5。
hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。
ziplist长度需要控制在1000以内,否则由于存取操作时间复杂度在O(n)到O(n2)之间,长列表会导致CPU消耗严重,得不偿失。
1.删除过期键对象
2.内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。
1)noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)。
2)volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
3)volatile-random:随机删除过期键,直到腾出足够空间为止
4)allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
4)allkeys-random:随机删除所有键,直到腾出足够空间为止。
6)volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
- Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
- Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
- Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面我介绍的不可查的 Error,是 Throwable 不是 Exception。
不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
性能角度来审视一下 Java 的异常处理机制,这里有两个可能会相对昂贵的地方:
- try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
- Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
一个Kafka主题由多个分区,一个分区对应一个Log对象,比如创建主题test-topic,那么kafka会创建两个子目录test-topic-0、test-topic-1,Kafka 日志对象由多个日志段对象(LogSegment)组成,而每个日志段对象会在磁盘上创建一组文件。
(log对象)日志是日志段的容器,里面定义了很多管理日志段的操作。
事务,其实是包含一系列操作的、一个有边界的工作序列,有明确的开始和结束标志,且要么被完全执行,要么完全失败,即 all or nothing。通常情况下,我们所说的事务指的都是本地事务,也就是在单机上的事务。
分布式事务就是在分布式系统中运行的事务,由多个本地事务组合而成。
ACID
分布式事务基本能够满足 ACID,其中的 C 是强一致性,也就是所有操作均执行成功,才提交最终结果,以保证数据一致性或完整性。但随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了 BASE 理论,该理论的一个关键点就是采用最终一致性代替强一致性。
两阶段提交协议的执行过程,分为投票(voting)和提交(commit)两个阶段。
- 协调者(Coordinator,即事务管理器)会向事务的参与者发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,记录日志信息但不提交,待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。
- 当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了提交阶段。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令。
- 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。
- 单点故障问题:基于 XA 的二阶段提交算法类似于集中式算法,一旦事务管理器发生故 障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理 器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
- 数据不一致问题:在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生 了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分 参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法 执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
对二阶段提交(2PC)的 改进, 为了解决两阶段提交的同步阻塞和数据不一致问题三阶段提交引入了超时机制和准备阶段。这样三阶段提交协 议就有 CanCommit、PreCommit、DoCommit 三个阶段。
2PC 和 3PC 这两种方法,有两个共同的缺点,一是都需要锁定资源,降低系统性能;二 是,没有解决数据不一致的问题。
在 eBay 的分布式系统架构中,架构师解决一致性问题的核心思想就是:将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队 列中,再通过业务规则进行失败重试。这个案例,就是使用基于分布式消息的最终一致性方 案解决了分布式事务的问题。
通过牺牲强一致性,保证最终一致性,来获得高可用性,是对 ACID 原则的弱化。这三种分布式事务实现方式,二阶段提交、三阶段提交方法,遵循的是 ACID 原则,而消息最终一致性方案遵循的就是 BASE 理论。
CAP阐述
一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)
BASE 理论是对 CAP 中一致性和可用性权衡的结果, 它来源于对大规模互联网分布式系统实践的总结,是基于 CAP 定理逐步演化而来的。它的核心思想是,如果不是必须的话,不推荐实现事务或强一致性,鼓励可用性和性能优先,根据业务的场景特点,来实现非常弹性的基本可用,以及实现数据的终一致性。
一个是 Basic Paxos 算法,描述的是多节点之间如何就某个值(提案 Value)达成共识; 另一个是 Multi-Paxos思想,描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。说白了,Multi-Paxos 就是多执行几次 Basic Paxos。 在 Basic Paxos 中,有提议者(Proposer)、接受者(Acceptor)、学习者(Learner) 三种角色。
准备(Prepare)阶段
接受(Accept)阶段
Basic Paxos 只能就单个值(Value)达成共识,一旦遇到为一系列的值实现共识的时候,它就不管用了。
而如果我们直接通过多次执行 Basic Paxos 实例,来实现一系列值的共识,就会存在这样 几个问题:1. 提案冲突 2. 2轮RPC通讯延迟大
那么如何解决上面的 2 个问题呢?可以通过引入领导者和优化 Basic Paxos 执行来解决, 咱们首先聊一聊领导者。
从本质上说,Raft 算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致
Raft 算法支持领导者(Leader)、跟随者 (Follower)和候选人(Candidate) 3 种状态。
Raft 算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。集群中没有领导者,而节点 A 的等待 超时时间小(150ms),它会先因为没有等到领导者的心跳信息,发生超时。这个时候,节点 A 就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选 票,然后向其他节点发送请求投票 RPC 消息,请它们选举自己为领导者。如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。节点 A 当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举,篡权。
在一次选举中,每一个服务器节点多会对一个任期编号投出一张选票,并且按照“先 来先服务”的原则进行投票。比如节点 C 的任期编号为 3,先收到了 1 个包含任期编号 为 4 的投票请求(来自节点 A),然后又收到了 1 个包含任期编号为 4 的投票请求(来 自节点 B)。那么节点 C 将会把唯一一张选票投给节点 A,当再收到节点 B 的投票请求 RPC 消息时,对于编号为 4 的任期,已没有选票可投了。
在 Raft 算法中,服务器节点间的沟通联络采用的是远程过程调用(RPC),在领导者选举 中,需要用到这样两类的 RPC:
其实在选举中,除了选举规则外,我们还需要避免一些会导致选举失败的情况,比如同一任 期内,多个候选人同时发起选举,导致选票被瓜分,选举失败。那么在 Raft 算法中,如何 避免这个问题呢?答案就是随机超时时间。
Raft 算法和兰伯特的 Multi-Paxos 不同之处,主要有 2 点
在 Raft 算法中,副本数据是以日志的形式存在的,领导者接收到来自客户端写请求后,处理写请求的过程就是一个复制和提交日志项的过程。
日志是由日志项组成,日志项究竟是什么样子呢?其实,日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),
还包含一些附加信息,比如索引值(Log index)、任期编号(Term)。
你可以把 Raft 的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段), 减少了一半的往返消息,也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢?
在日常工作中,集群中的服务器数量是会发生变化的。Raft 是共识算法,对集群成员进行变更时(比如增加 2 台服务器),会不会因为集群分裂,出现 2 个领导者呢?”
关于成员变更,不仅是 Raft 算法中比较难理解的一部分,非常重要,也是 Raft 算法中唯一被优化和改进的部分。比如,初实现成员变更的是联合共识(Joint Consensus), 但这个方法实现起来难,后来 Raft 的作者就提出了一种改进后的方法,单节点变更 (single-server changes)。
Gossip 的三板斧分别是:直接邮寄(Direct Mail)、反熵(Anti-entropy)和谣言传播 (Rumor mongering)。
兰伯特的 Multi-Paxos,虽然能保证达成共识后的值不再改变,但它不管关 心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。
区块链通过工作量证明(Proof of Work)增加了坏人作恶的成本,以此防止坏人作恶。比如,如果坏人要发起 51% 攻击,需要控制现网 51% 的算力,成本是非常高昂的。为啥呢?因为根据 Cryptoslate 估算,对比特币进行 51% 算力攻击需要上百亿人民币!
区块链是通过执行哈希运算,然后通过运算后的结果值,证明自己做过了相关工作。区块链也是通过 SHA256 来执行哈希运算的,通过计算出符合指定条件的哈希值,来证明工作量的。因为在区块链中,PoW 算法是基于区块链中的区块信息,进行哈希运算的。
拜占庭容错算法(比如 PoW 算法、PBFT 算法),能容忍一定比例的作恶行为,所以它在相对开放的场景中应用广泛,比如公链、联盟链。非拜占庭容错算法 (比如 Raft)无法对作恶行为进行容错,主要用于封闭、绝对可信的场景中,比如私链、 公司内网的 DevOps 环境。我希望你能准确理解 2 类算法之间的差异,根据场景特点,选择合适的算法,保障业务高效、稳定的运行。