非八股文!Java面试刁钻夺命连环问汇总
2021-06-16 17:20:50 5 举报
AI智能生成
Java面试の灵魂拷问
作者其他创作
大纲/内容
Java 基础
hashCode() 和 equals() 的作用、区别、联系?
作用
两者在 Java 里都是用来对比两个对象是否相等一致。
区别
重写的 equals 里一般比较得比较全面比较复杂,效率就比较低;而利用 hashCode 进行对比,只要生成一个 hash 值进行比较就可以了,效率很高。但是 hashCode 并非完全可靠,存在 hash 冲突的问题。
equals() 相等的两个对象他们的 hashCode() 肯定相等,也就是用 equals() 对比肯是绝对可靠的。
hashCode() 相等的两个对象他们的 equals() 不一定相等,也就是 hashCode() 不是绝对可靠的。
联系
重写 equals() 方法时必须重写 hashCode() 方法
为性能考虑:首先用 hashCode()去对比,hashCode 不一样则肯定不相等;hashCode 相等再对比 equals。
为了保证同一个对象,保证在 equals 相同的情况下 hashCode 值必定相同,如果重写了 equals 而未重写 hashCode,可能就会出现两个没有关系的对象 equals 相同(因为 equals 都是根据对象的特征进行重写的),但 hashCode 确实不相同的情况。
hashCode 和 equals 都是基本类 Object 里的方法,和 equals 一样,Object 里 hashCode 只是返回当前对象的地址,如果是这样的话,相同的一个类 new 两个对象,由于他们在内存里的地址不同,则他们的 hashCode 不同,所以这显然不是我们想要的。所以我们必须重写类的 hashCode 方法,即一个类,在 hashCode 里面返回唯一的一个 hash 值。
HashMap
数据结构
容量与扩容如何实现
HashMap 为什么是线程不安全的?
底层源码
Java 反射机制
Java 中 Class.forName() 和 ClassLoader 有什么区别?
Java 8
你还在 new 对象吗?Java8 通用 Builder 了解?
Java 中 for、foreach、stream 哪个处理效率更高?
ConcurrentHashMap
数据结构
蛋疼系列
a==1 && a==2 && a==3 是 true 还是 false?
集合
遍历 Map 集合有几种方式?效率如何?
ArrayList 使用 forEach 遍历时删除元素会报错吗?
网络传输对象为什么要实现序列化接口
只实现序列化但是不定义序列化 ID 会不会有什么问题?
如果不定义这个序列化 ID,下次在改了这个类的结构之后,可能会报序列化错误。
字节流跟序列化 ID 做一个绑定关系,去验证这个版本是不是一致的,类结构有没有做变化。
JVM
Java 中 new 一个对象的过程中发生了什么?
JIT
内核态/用户态
操作系统权限安全
JMM
JMM 基础计算机原理
物理内存模型带来的问题
缓存一致性
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence),在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory),当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
伪共享
伪共享会影响应用的性能
Java 内存模型 JMM
从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java 内存模型带来的问题
可见性问题
竞争问题
重排序
重排序类型
数据依赖性
as-if-serial
控制依赖性
内存屏障
临界区
happens-before
定义
加深理解
Happens-Before 规则
volatile 详解
volatile 特性
volatile 的内存语义
volatile 内存语义的实现
volatile 重排序规则表
volatile 的内存屏障
volatile 写
volatile 读
volatile 的实现原理
为何 volatile 不是线程安全的?
final 的内存语义
final 的两个重排序规则
final 域为引用类型
final 引用不能从构造函数内逃逸
final 语义的实现
锁的内存语义
synchronized 的实现原理
了解各种锁
并发编程
保证
1. 原子性
2. 可见性
volatile/synchronized/Lock
3. 有序性
volatile
可见性
总线 MESI 缓存一致性协议
主内存-总线-CPU 工作内存,CPU 总线嗅探机制,某线程对主内存数据的修改(lock,其他线程无法读取修改),将工作内存中相同的内容失效。
禁止指令重排
内存屏障
lock 前缀指令
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
Store Barrier:sfence
Load Barrier:ifence
Full Barrier:mfence
Java 内存模型中 volatile 变量在写操作之后会插入一个 store 屏障,在读操作之前会插入一个 load 屏障。一个类的 final 字段会在初始化后插入一个 store 屏障,来确保 final 字段在构造函数初始化完成并可被使用时可见。
原子指令和 Software Locks
原子指令,如 x86 上的 “lock …” 指令是一个 Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个 CPU。Software Locks 通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
内存屏障的性能影响
内存屏障阻碍了 CPU 采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器可以按单元执行任务,然后在任务单元的边界放上所有需要的内存屏障。采用这个方法可以让处理器不受限的执行一个任务单元。合理的内存屏障组合还有一个好处是:缓冲区在第一次被刷后开销会减少,因为再填充改缓冲区不需要额外工作了。
问题
总线风暴
在 Java 中使用 unsafe 实现 CAS,而其底层由 CPP 调用汇编指令实现的,如果是多核 CPU 是使用 lock cmpxchg 指令,单核 CPU 使用 compxch 指令。
如果在短时间内产生大量的 CAS 操作再加上 volatile 的嗅探机制则会不断地占用总线带宽,导致总线流量激增,就会产生总线风暴。
总之,就是因为 volatile 和 CAS 的操作导致 BUS 总线缓存一致性流量激增所造成的影响。
如果在短时间内产生大量的 CAS 操作再加上 volatile 的嗅探机制则会不断地占用总线带宽,导致总线流量激增,就会产生总线风暴。
总之,就是因为 volatile 和 CAS 的操作导致 BUS 总线缓存一致性流量激增所造成的影响。
1、总线锁
在早期处理器提供一个 LOCK# 信号,CPU1 在操作共享变量的时候会预先对总线加锁,此时 CPU2 就不能通过总线来读取内存中的数据了,但这无疑会大大降低 CPU 的执行效率。
在早期处理器提供一个 LOCK# 信号,CPU1 在操作共享变量的时候会预先对总线加锁,此时 CPU2 就不能通过总线来读取内存中的数据了,但这无疑会大大降低 CPU 的执行效率。
2、缓存一致性协议
由于总线锁的效率太低所以就出现了缓存一致性协议,Intel 的 MESI 协议就是其中一个佼佼者。MESI 协议保证了每个缓存变量中使用的共享变量的副本都是一致的。
由于总线锁的效率太低所以就出现了缓存一致性协议,Intel 的 MESI 协议就是其中一个佼佼者。MESI 协议保证了每个缓存变量中使用的共享变量的副本都是一致的。
3、MESI 的核心思想
modified(修改)、exclusive(互斥)、share(共享)、invalid(无效)
CPU1 使用共享数据时会先数据拷贝到 CPU1 缓存中,然后置为独占状态(E),这时 CPU2 也使用了共享数据,也会拷贝也到 CPU2 缓存中。
通过总线嗅探机制,当该 CPU1 监听总线中其他 CPU 对内存进行操作,此时共享变量在 CPU1 和 CPU2 两个缓存中的状态会被标记为共享状态(S);
若 CPU1 将变量通过缓存回写到主存中,需要先锁住缓存行,此时状态切换为(M),向总线发消息告诉其他在嗅探的 CPU 该变量已经被 CPU1 改变并回写到主存中。
接收到消息的其他 CPU 会将共享变量状态从(S)改成无效状态(I),缓存行失效。若其他 CPU 需要再次操作共享变量则需要重新从内存读取。
modified(修改)、exclusive(互斥)、share(共享)、invalid(无效)
CPU1 使用共享数据时会先数据拷贝到 CPU1 缓存中,然后置为独占状态(E),这时 CPU2 也使用了共享数据,也会拷贝也到 CPU2 缓存中。
通过总线嗅探机制,当该 CPU1 监听总线中其他 CPU 对内存进行操作,此时共享变量在 CPU1 和 CPU2 两个缓存中的状态会被标记为共享状态(S);
若 CPU1 将变量通过缓存回写到主存中,需要先锁住缓存行,此时状态切换为(M),向总线发消息告诉其他在嗅探的 CPU 该变量已经被 CPU1 改变并回写到主存中。
接收到消息的其他 CPU 会将共享变量状态从(S)改成无效状态(I),缓存行失效。若其他 CPU 需要再次操作共享变量则需要重新从内存读取。
缓存一致性协议失效的情况:
共享变量大于缓存行大小,MESI 无法进行缓存行加锁;
CPU 并不支持缓存一致性协议
共享变量大于缓存行大小,MESI 无法进行缓存行加锁;
CPU 并不支持缓存一致性协议
4、嗅探机制
每个处理器会通过嗅探器来监控总线上的数据来检查自己缓存内的数据是否过期,如果发现自己缓存行对应的地址被修改了,就会将此缓存行置为无效。当处理器对此数据进行操作时,就会重新从主内存中读取数据到缓存行。
每个处理器会通过嗅探器来监控总线上的数据来检查自己缓存内的数据是否过期,如果发现自己缓存行对应的地址被修改了,就会将此缓存行置为无效。当处理器对此数据进行操作时,就会重新从主内存中读取数据到缓存行。
5、缓存一致性流量
通过前面都知道了缓存一致性协议,比如 MESI 会触发嗅探器进行数据传播。当有大量的 volatile 和 CAS 进行数据修改的时候就会产大量嗅探消息。
通过前面都知道了缓存一致性协议,比如 MESI 会触发嗅探器进行数据传播。当有大量的 volatile 和 CAS 进行数据修改的时候就会产大量嗅探消息。
volatile VS synchronized
synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile 关键字在某些情况下性能要优于 synchronized,但是要注意 volatile 关键字是无法替代 synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。
通常来说,使用 volatile 必须具备以下 2 个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的 2 个条件需要保证操作是原子性操作,才能保证使用 volatile 关键字的程序在并发时能够正确执行。
通常来说,使用 volatile 必须具备以下 2 个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的 2 个条件需要保证操作是原子性操作,才能保证使用 volatile 关键字的程序在并发时能够正确执行。
synchronized
synchronized 加到 static 方法前面是给 Class 加锁,即类锁;synchronized 加到非静态方法前面是给对象上锁。
什么时候使用多线程
CPU 多核
CPU 是性能瓶颈的情况下,多线程才能实现提升性能的目的。
瓶颈在于 IO 操作的话,拆分到两个线程中执行没什么卵用。
任务具有并发性,即任务可以拆分为多个子任务并发执行。
比如其中某些子任务(外部调用、数据库查询、大量计算)比较耗时,并且结果没有那么重要的。
一些后台线程。
例如定期执行一些特殊任务,或者允许延后处理的任务。
后台定时任务,例如:定时向大量用户(100w 以上)发送邮件。
异步处理,例如:发微博、记录日志等。
分布式计算
使用多线程的主要目的
吞吐量
做 Web,容器帮你做了多线程,但是它只能帮你做请求层面的。简单地说,可能就是一个请求一个线程,或多个请求一个线程。如果是单线程,那同时只能处理一个用户的请求。
伸缩性
你可以通过增加 CPU 核数来提升性能。如果是单线程,那程序执行到死也就利用了单核,肯定没办法通过增加 CPU 核数来提升性能。
并发编程优化
锁优化
线程池优化
并发编程基础
线程基础入门
什么是进程和线程
进程是程序运行资源分配的最小单位
线城是 CPU 调度的最小单位,必须依赖于进程而存在
线程无处不在
CPU 核心数和线程数的关系
CPU 时间片轮转机制
并发和并行
高并发编程的意义、优点和注意事项
意义
由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越受重视和关注。
优点
充分利用 CPU 资源
加快用户响应时间
使代码模块化、异步化、简单化
注意事项
线程之间的安全性
线程之间的死锁
多线程程序注意事项
Java 程序天生就是多线程的
线程的启动与中止
深入理解 run() 和 start()
线程的其他相关方法
join 方法
线程的优先级
守护线程
线程之间的共享和协作
线程间的共享
synchronized、volatile、ThreadLocal 如何实现线程共享
线程间的协作
wait/notify/notifyAll 如何实现线程之间的协作
ThreadLocal 辨析
项目常用实战
并发工具类实战
CountDownLatch
CyclicBarrier
Fork/Join
分而治之
归并排序
原理
工作密取
即当前线程的 Task 已经全部被执行完毕,则自动取到其他线程的 Task 池中取出 Task 继续执行。
ForkJoinPool 中维护着多个线程(一般为 CPU 核数)在不断地执行 Task,每个线程除了执行自己职务内的 Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的 Task,如此一来就能够减少线程阻塞或是闲置的时间,提高 CPU 利用率。
Fork/Join 框架
在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行汇总(join)
实战
Fork/Join 使用的标准范式
我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 fork 和 join 的操作机制,通常我们不直接集成 ForkJoinTask 类,只需要直接继承其子类。
1. RecursiveAction,用于没有返回结果的任务
2. RecursiveTask,用于有返回值的任务
Task 要通过 ForkJoinPool 来执行,使用 submit 或 invoke 提交,两者的区别是:invoke 是同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit 是异步执行。
join() 和 get() 方法当任务完成的时候返回计算结果。
1. RecursiveAction,用于没有返回结果的任务
2. RecursiveTask,用于有返回值的任务
Task 要通过 ForkJoinPool 来执行,使用 submit 或 invoke 提交,两者的区别是:invoke 是同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit 是异步执行。
join() 和 get() 方法当任务完成的时候返回计算结果。
流程图
在我们自己实现的 compute 方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务;如果不足够小,就必须分割成两个子任务,每个子任务在调用 invokeAll 方法时,又会进入 compute 方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用 join 方法会等待子任务执行完并得到其结果。
在我们自己实现的 compute 方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务;如果不足够小,就必须分割成两个子任务,每个子任务在调用 invokeAll 方法时,又会进入 compute 方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用 join 方法会等待子任务执行完并得到其结果。
Fork/Join 的同步用法和异步用法
参见代码包 cn.enjoyedu.ch2.forkjoin 下
Callable、FutureTask、Semaphore、Exchange 场景实战
原子操作 CAS
CAS 原理
什么是原子操作?如何实现原子操作?
CAS 三大问题
ABA 问题
循环时间长开销大
只能保证一个共享变量的原子操作
原子操作类场景实战
AtomicInteger
AtomicIntegerArray
更新引用类型
AtomicReference
AtomicStampedReference
AtomicMarkableReference
原子更新字段类
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
显式锁解析
汇总
内置锁和显式锁
公平锁和非公平锁
锁的可重入
显式锁
Lock 的标准用法
Lock 的常用 API
ReentrantLock
锁的可重入
公平和非公平锁
读写锁 ReentrantReadWriteLock
Condition 接口
Condition 常用方法
Condition 适用范式
Condition 使用
了解 LockSupport
CLH 队列锁
扩展知识点
AbstractQueuedSynchronizer
学习 AQS 的必要性
AQS 使用方法和其中的设计模式
模板方法模式
深入源码
节点在同步队列中的增加和移除
共享式的同步工具类
了解 Condition 的实现
回头看 Lock 的实现
深度分析线程池内部机制
手写线程池实战
线程池扩展实战
Executor 框架解读实战
性能优化实战
手写并发任务执行框架实战
应用性能优化实战
源码
CountDownLatch、Semaphore、FutureTask、ForkJoin 源码解析
AQS 源码深度解读
AbstractQueuedSynchronizer 源码分析
ReentrantLock 底层源码及应用实战
手写实现自定义 ReentrantLock
架构师应该了解的并发安全解决方案
Java 内存模型 JMM 分析
线程池源码
阻塞队列源码
HashMap、ConcurrentHashMap 源码解读及应用实战
ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 版本对比
线程/纤程/协程 多维度对比
高性能本地队列 Disruptor 解析
新增原子类和并发流技术解析
并发容器
预备知识
Hash 算法
位运算
二进制
常用位运算
位运算应用场景
实战:权限控制
使用位运算的优劣势
为什么要使用 ConcurrentHashMap
1.7 中 HashMap 死循环分析
HashMap 扩容流程
原理
实例
并发下的扩容
总结
ConcurrentHashMap
使用
ConcurrentHashMap 实现分析
1.7 下的实现
构造方法和初始化
get 操作
put 操作
rehash 操作
remove 操作
ConcurrentHashMap 的弱一致性
size、containsValue
1.8 下的实现
改进
核心数据结构和属性
Node
ConcurrentSkipList 系列
了解什么是 SkipList
二分查找和 AVL 树查找
什么是跳表
ConcurrentLinkedQueue
写时复制容器
什么是写时复制容器?
写时复制容器相关问题
性能问题
数据一致性问题
阻塞队列 Blocking Queue
队列
什么是阻塞队列
常用阻塞队列
了解阻塞队列的实现原理
手写阻塞队列
线程池
为什么要使用线程池
ThreadPoolExecutor 的类关系
线程池的创建各个参数含义
扩展线程池
线程池的工作机制
提交任务
关闭线程池
合理地配置线程池
预定义线程池
CompletionService
并发安全
什么是线程安全
线程封闭
ad-hoc 线程封闭
栈封闭
无状态的类
让类不可变
volatile
加锁和 CAS
安全的发布
ThreadLocal
Servlet 辨析
死锁
概念
学术化的定义
现象、危害和解决
现象
简单顺序死锁
动态顺序死锁
危害
实际工作中的死锁
解决
定位
修正
其他安全问题
活锁
线程饥饿
并发下的性能
线程引入的开销
上下文切换
内存同步
同步操作的性能开销包括多个方面,在 synchronized 和 volatile 提供的可见性宝郑重可能会使用一些特殊指令,即内存栅栏(Memory Barrier)。
内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行管道。
内存栅栏可能同样会对性能带来间接影响,因为它们将抑制一些编译器优化操作,在内存栅栏中,大多数操作都是不能被重排序的。
内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行管道。
内存栅栏可能同样会对性能带来间接影响,因为它们将抑制一些编译器优化操作,在内存栅栏中,大多数操作都是不能被重排序的。
阻塞
引起阻塞的原因:包括阻塞 IO,等待获取发生竞争的锁,或者在条件变量上等待等。
阻塞会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作】。
很明显这个操作至少包含两次额外的上下文切换,还有相关的操作系统级的操作等。
阻塞会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作】。
很明显这个操作至少包含两次额外的上下文切换,还有相关的操作系统级的操作等。
如何减少锁的竞争
减少锁的粒度
使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象,但是如果有同时要持有多个锁的业务方法,要注意避免发生死锁。
减小锁的范围
对锁的持有实现快进快出,尽量缩短持有锁的时间。将一些与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作。
避免多余的锁
两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化——扩大锁的范围。
锁分段
参考 JDK1.7 下的 ConcurrentHashMap,典型的锁分段。
线程安全的单例模式
框架
MyBatis
流式查询替代MySQL分页查询
Spring
IoC,控制反转
DI,依赖注入
由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
循环依赖
单例的 setter 注入(能解决)
三级缓存
singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的 Bean 实例
earlySingletonObjects 二级缓存,用于保存实例化完成的 Bean 实例
singletonFactories 三级缓存,用于保存 Bean 创建工厂,以便于后面扩展有机会创建代理对象。
singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的 Bean 实例
earlySingletonObjects 二级缓存,用于保存实例化完成的 Bean 实例
singletonFactories 三级缓存,用于保存 Bean 创建工厂,以便于后面扩展有机会创建代理对象。
多例的 setter 注入(不能解决)
构造器注入(不能解决)
单例的代理对象 setter 注入(有可能解决)
DependsOn 循环依赖(不能解决)
如何一次注入所有类
AOP
实现原理
Spring 的 AOP 实现原理其实很简单,就是通过动态代理实现的。
如果我们为 Spring 的某个 Bean 配置了切面,那么 Spring 在创建这个 Bean 的时候,实际上创建的是这个 Bean 的一个代理对象,我们后续对 Bean 中方法的调用,实际上调用的是代理类重写的代理方法。而 Spring 的 AOP 使用了两种动态代理,分别是 JDK 的动态代理,以及 CGLib 的动态代理。
Spring 的 AOP 实现原理其实很简单,就是通过动态代理实现的。
如果我们为 Spring 的某个 Bean 配置了切面,那么 Spring 在创建这个 Bean 的时候,实际上创建的是这个 Bean 的一个代理对象,我们后续对 Bean 中方法的调用,实际上调用的是代理类重写的代理方法。而 Spring 的 AOP 使用了两种动态代理,分别是 JDK 的动态代理,以及 CGLib 的动态代理。
JDK 动态代理
Spring 默认使用 JDK 的动态代理实现 AOP,类如果实现了接口,Spring 就会使用这种方式实现动态代理。熟悉 Java 语言的小伙伴应该会对 JDK 动态代理有所了解。
JDK 实现动态代理需要两个组件,首先第一个就是 InvocationHandler 接口。我们在使用 JDK 的动态代理时,需要编写一个类,去实现这个接口,然后重写 invoke 方法,这个方法其实就是我们提供的代理方法。
JDK 动态代理需要使用的第二个组件就是 Proxy 这个类,我们可以通过这个类的 newProxyInstance 方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的 invoke 方法。
JDK 实现动态代理需要两个组件,首先第一个就是 InvocationHandler 接口。我们在使用 JDK 的动态代理时,需要编写一个类,去实现这个接口,然后重写 invoke 方法,这个方法其实就是我们提供的代理方法。
JDK 动态代理需要使用的第二个组件就是 Proxy 这个类,我们可以通过这个类的 newProxyInstance 方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的 invoke 方法。
实现原理
JDK 的动态代理是基于反射实现。JDK 通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的 InvocationHandler 接口的 invoke 方法。并且这个代理类是 Proxy 类的子类(记住这个结论,后面测试要用)。这就是 JDK 动态代理大致的实现方式。
优点
1. JDK 动态代理是 JDK 原生的,不需要任何依赖即可使用;
2. 通过反射机制生成代理类的速度要比 CGLib 操作字节码生成代理类的速度更快;
2. 通过反射机制生成代理类的速度要比 CGLib 操作字节码生成代理类的速度更快;
缺点
1. 如果要使用 JDK 动态代理,被代理的类必须实现了接口,否则无法代理。
2. JDK 动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring 仍然会使用 JDK 的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
3. JDK 动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。
2. JDK 动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring 仍然会使用 JDK 的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
3. JDK 动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。
CGLib 动态代理
JDK 动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时 JDK 动态代理将没有办法使用,于是 Spring 会使用 CGLib 的动态代理来生成代理对象。CGLib 直接操作字节码,生成类的子类,重写类的方法完成代理。
实现原理
CGLib 实现动态代理的原理是,底层采用了 ASM 字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为 Spring 中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
优点
1. 使用 CGLib 代理的类,不需要实现接口,因为 CGLib 生成的代理类是直接继承自需要被代理的类;
2. CGLib 生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
3. CGLib 生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以 CGLib 执行代理方法的效率要高于 JDK 的动态代理;
2. CGLib 生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
3. CGLib 生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以 CGLib 执行代理方法的效率要高于 JDK 的动态代理;
缺点
1. 由于 CGLib 的代理类使用的是继承,这也就意味着如果需要被代理的类是一个 final 类,则无法使用 CGLib 代理;
2. 由于 CGLib 实现代理方法的方式是重写父类的方法,所以无法对 final 方法,或者 private 方法进行代理,因为子类无法重写这些方法;
3. CGLib 生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比 JDK 通过反射生成代理类的速度更慢;
2. 由于 CGLib 实现代理方法的方式是重写父类的方法,所以无法对 final 方法,或者 private 方法进行代理,因为子类无法重写这些方法;
3. CGLib 生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比 JDK 通过反射生成代理类的速度更慢;
其他问题
事务处理 @Transactional(rollbackFor=Exception.class)
事务失效
@Transactional 用在非 public 修饰的方法上
同一个类中方法调用
@Transactional 注解属性 propagation 设置错误
@Transactional 注解属性 rollbackFor 设置错误
异常被 catch
Spring 中 @Transactional 是如何实现的?
RPC
惊群效应/羊群效应
具体问题:线上集群节点很多,每次部署少量的请求会报超时,当下游的应用在部署,上游调用会报一个通道关闭的错误,起初整个 QPS 比较低,所以大家可能会忽略;但是后面 QPS 上来之后,失败的数量也就上来了。Linux 源码中涉及到这些地方。
在通道关闭的时候,服务的提供方底层 Linux 是输出了洪水攻击的一个日志(类似于 DDoS),后来发现它是有一个半连接队列在里面,TCP 三次握手,第二次握手之后,对服务的消费方来说它其实已经 ready 了,已经 establish 建立连接了,但是服务提供方其实这边已经把它的请求给关闭掉了。因为半连接队列已经溢出了。这个时候上有真的来请求的时候其实会直接把它的请求给拒绝掉,所以上游就报一个通道关闭。
解决方案:改底层 Linux 源码两个参数,backlog,底层用的 netty,当时版本默认写死 50。
在通道关闭的时候,服务的提供方底层 Linux 是输出了洪水攻击的一个日志(类似于 DDoS),后来发现它是有一个半连接队列在里面,TCP 三次握手,第二次握手之后,对服务的消费方来说它其实已经 ready 了,已经 establish 建立连接了,但是服务提供方其实这边已经把它的请求给关闭掉了。因为半连接队列已经溢出了。这个时候上有真的来请求的时候其实会直接把它的请求给拒绝掉,所以上游就报一个通道关闭。
解决方案:改底层 Linux 源码两个参数,backlog,底层用的 netty,当时版本默认写死 50。
如何修改源码?
项目依赖的 xx 包,在工程下面创建一个同样的包名同样的类,然后进行相应的修改。
Java 线程在启动的时候,类加载会通过相应的顺序优先加载工程里的类。
项目依赖的 xx 包,在工程下面创建一个同样的包名同样的类,然后进行相应的修改。
Java 线程在启动的时候,类加载会通过相应的顺序优先加载工程里的类。
应用刚启动的时候,接口 RT 突然会比较高,大概是个什么原因?如何尽量避免?
JIT
Java 即时编译,字节码,JVM 检测到热点代码执行次数达到一定阈值之后,会编译成更底层的机器码,执行起来会更加高效。
为什么启动的时候不去把整个应用都编译成更底层的机器码?
主要考虑到性能开销,应用启动非常慢,文件大小也会很大。
绝大部分的代码其实符合二八定律的,80% 流量集中在 20% 的代码。所以动态识别热点代码对于 JVM 来说收益相对高一些。
为了避免 JIT 对线上部署的影响,启动的时候直接去回放流量,触发 JIT 机制,RT 稳定之后再去给它放入线上流量。
主要考虑到性能开销,应用启动非常慢,文件大小也会很大。
绝大部分的代码其实符合二八定律的,80% 流量集中在 20% 的代码。所以动态识别热点代码对于 JVM 来说收益相对高一些。
为了避免 JIT 对线上部署的影响,启动的时候直接去回放流量,触发 JIT 机制,RT 稳定之后再去给它放入线上流量。
网络抖动
缓存预热
数据库连接池
Dubbo
集群如何做到平滑部署?
服务发现
下线,服务发现,类似于 zk 主动推拉的一种机制,包括缓存如何进行刷新(zk 不是一种强一致性,而是一种顺序一致性,写入其实是一种过半机制,没写入的节点其实数据还是老数据,它就可能调到已经下线的机器上),一个请求过来,如何知道哪个请求是对应哪一个响应呢?
心跳监测
灰度发布
蓝绿发布
JIT
预热
用户线程如何回收的?
粘包怎么解决的?
高性能如何实现?
底层协议
TCP 这一层,解码、转码、序列化……
IO 模型,阻塞、非阻塞,异步、同步
主要从两个阶段来看,内核态到用户态、磁盘和内核态之间的传输。
零拷贝
如何降低拷贝数量
MySQL
索引
事务
A:原子性
undolog
实现一致性
undolog 主要为事务的回滚服务。在事务执行的过程中,除了记录 redolog,还会记录一定量的 undolog。undolog 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undolog 进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
undolog 记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录下表空间中的(共享表空间或者独享表空间)
undolog 主要为事务的回滚服务。在事务执行的过程中,除了记录 redolog,还会记录一定量的 undolog。undolog 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undolog 进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
undolog 记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录下表空间中的(共享表空间或者独享表空间)
undolog 是逻辑日志,根据每行记录进行记录
C:一致性
I:隔离性
隔离级别
READ UNCOMMITTED
问题:脏读、不可重复读、幻读
隔离级别太低,脏读不可忍受,不用。
READ COMMITTED
问题:不可重复读、幻读
如果使用 statement 这种 binlog 格式,在 RC 隔离级别下会出现问题。
REPEATABLE READ
问题:幻读
如果使用 statement 这种 binlog 格式,在 RR 隔离级别下会出现问题。
所以 MySQL 选择 RR 作为默认隔离级别,其实就是为了兼容历史上那种 statement 格式的 binlog。
在 RR 级别下,update、delete 当前读语句不但会加行锁,还会加 GAP 锁。
加 GAP 锁的前提:在 SQL 语句不走索引或者非唯一索引的当前读。
加 GAP 锁的前提:在 SQL 语句不走索引或者非唯一索引的当前读。
1. Record lock:单个⾏行行记录上的锁;
2. Gap lock:间隙锁,锁定⼀一个范围,不不包括记录本身;
3. Next-key lock:record+gap 锁定⼀一个范围,包含记录本身。
SERIALIZABLE
隔离级别太高,影响并发,不用。
MVCC
RR 级别下 MVCC 如何工作
SELECT:InnoDB 会根据以下两个条件检查每行记录:
- InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的
- 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除
只有符合上述两个条件的才会被查询出来
- InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的
- 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除
只有符合上述两个条件的才会被查询出来
INSERT:InnoDB 为新插入的每一行保存当前系统版本号作为行版本号
DELETE:InnoDB 为删除的每一行保存当前系统版本号作为行删除标识
UPDATE:InnoDB 为插入的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识
保存这两个额外系统版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且也能保证只会读取到符合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
MVCC 只在 COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作。
D:持久性
redolog
实现持久化和原子性
在 InnoDB 的存储引擎中,事务日志通过重做(redo)日志和 InnoDB 存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是 DBA 口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在 Buffer Pool 中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据 redolog 中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为 redolog 分配了一块连续的存储空间,以顺序追加的方式记录 redolog,通过顺序 IO 来改善性能。所有的事务共享 redolog 的存储空间,它们的 redolog 按语句的执行顺序,依次交替的记录在一起。
在 InnoDB 的存储引擎中,事务日志通过重做(redo)日志和 InnoDB 存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是 DBA 口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在 Buffer Pool 中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据 redolog 中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为 redolog 分配了一块连续的存储空间,以顺序追加的方式记录 redolog,通过顺序 IO 来改善性能。所有的事务共享 redolog 的存储空间,它们的 redolog 按语句的执行顺序,依次交替的记录在一起。
redolog 是物理日志,记录页的物理修改操作
集群
主从复制
主服务器提供写服务,从服务器提供读服务。
主从复制过程
数据的同步是通过 binlog 进行的。
两个线程:I/O 线程,SQL 线程
数据的同步是通过 binlog 进行的。
两个线程:I/O 线程,SQL 线程
binlog
三种格式
statement
binlog 里面记录的就是 SQL 语句的原文
row
statement 格式中存在很多问题,最明显的就是可能会导致主从数据库的数据不一致
mixed
分库分表
优化
group by 优化
分页查询优化
死锁
缓存
主要是 Redis
主要是 Redis
Redis 集群问题及方案
要么不出现,一旦出现就是致命性的问题,所以面试官必会问你
要么不出现,一旦出现就是致命性的问题,所以面试官必会问你
缓存穿透
对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
不同场景下的解决方式可如下:
1. 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
2. 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
3. 若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
1. 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
2. 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
3. 若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
缓存雪崩
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
事前:
1. Redis 高可用,主从+哨兵,Redis Cluster,避免全盘崩溃。
2. 热点值设置不同的过期时间
1. Redis 高可用,主从+哨兵,Redis Cluster,避免全盘崩溃。
2. 热点值设置不同的过期时间
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
限流&降级好处:
1. 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
2. 只要数据库不死,对用户来说,大多数请求都是可以被处理的。
3. 只要有请求可以被处理,就意味着你的系统没有死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来。
1. 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
2. 只要数据库不死,对用户来说,大多数请求都是可以被处理的。
3. 只要有请求可以被处理,就意味着你的系统没有死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来。
事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
基本原理
缓存分类
按访问位置
本地缓存
远程缓存
按存储介质
内存缓存
SSD 缓存
读写模式
经典问题
缓存协议
memcached
Redis
缓存 client
设计关键点
kv size
读写峰值
命中率
过期策略
穿透加载时间
缓存组件
memcached
访问协议
IO 模型
状态机
Hash 定位
LRU 剔除策略
内存分配及管理
pika
fatcache
Redis
访问协议
数据类型
持久化
复制
服务化
配置管理
注册中心
配置推送
proxy 路由
分布策略
一致性保障
集群管理
扩容
缩容
数据迁移
服务治理
服务监控
异常报警
故障转移
自动化运维
经典场景
电商秒杀
feed 聚合
热点推送
网络
HTTP/HTTPS
TCP/UDP
TCP/IP
IO/BIO/NIO/AIO
IO 多路复用
Netty
问题汇总
粘包怎么解决
分布式锁
数据库锁
乐观锁
悲观锁
Redis 锁
Redis(+Lua)
Redisson
Redlock
zk 锁
ETCD
分布式事务
产生背景
单体架构下,多个不同的业务逻辑使用的都是同一个数据源,单一事务管理器情况下,不存在事务问题。
跨数据库实例:单体架构偶尔也会存在多数据源事务管理,解决方案通常采用 JTA + Atomikos。
跨 JVM 进程:在分布式或者微服务架构中,每个服务都有自己的数据源,使用不同事务管理器,如果两个服务执行成功之后出现了异常,A 服务的事务会回滚,但是 B 服务的事务不会回滚,分布式事务就出现了。
多服务访问同一个数据库实例:通过远程调用协作,跨 JVM 进程,两个微服务持有了不同的数据库连接进行数据库操作,依然产生分布式事务。
BASE理论
BA:基本可用,Basically Available
鼓励通过架构设计,把可能影响全平台的严重问题,转化为只影响平台中的一部分数据或功能的非严重问题。
分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。
S:软状态,Soft State
允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
由于不要求强一致性,BASE 允许系统中存在中间状态(或者叫软状态),这个状态不影响系统可用性,如订单的“支付中”“数据同步中”等,待数据最终一致后状态改为“成功”。
E:最终一致性,Eventually Consistent
指数据在多个副本之间能否保持一致的特性,也是分布式事务要解决的终极问题。
分类
因果一致性(Causal Consistency)
读己之所写(Read your Writes)
会话一致性(Session Consistency)
单调读一致性(Monotonic Read Consistency)
单调写一致性(Monotonic Write Consistency)
指经过一点时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用;允许数据在一段时间内是不一致的,但最终达到一致状态。
满足 BASE 理论的事务,称之为“柔性事务”。
满足 BASE 理论的事务,称之为“柔性事务”。
基本设计原则
对业务无侵入:微服务化和分布式事务支持的引入,尽可能不要给业务带来额外的研发负担
高性能:引入分布式事务的业务基本保持在同一量级上,不能因为事务机制显著拖慢业务
解决方案
2PC,两阶段提交
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
情况1:
当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:
当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
问题
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
解决方案
XA 方案:基于 XA 协议的两阶段提交
XA 是一个分布式事务协议,XA 中大致分为两部分:事务管理器(TM)和本地资源管理器(RM)。
其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;
而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
XA 是一个分布式事务协议,XA 中大致分为两部分:事务管理器(TM)和本地资源管理器(RM)。
其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;
而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
涉及到三个角色:AP、RM、TM
AP:使用 2PC 分布式事务的应用程序
RM:资源管理器,控制分支事务
TM:事务管理器,控制着整个全局事务
AP:使用 2PC 分布式事务的应用程序
RM:资源管理器,控制分支事务
TM:事务管理器,控制着整个全局事务
MySQL 支持两阶段提交协议
1. 准备阶段(Prepare phase):事务管理器给每个参与者发送 prepare 消息,每个数据库参与者在本地执行事务,并写本地的 undo/redo 日志,此时事务没有提交。
2. 提交阶段(Commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的资源锁。注意:必须在最后阶段释放锁资源。
1. 准备阶段(Prepare phase):事务管理器给每个参与者发送 prepare 消息,每个数据库参与者在本地执行事务,并写本地的 undo/redo 日志,此时事务没有提交。
2. 提交阶段(Commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的资源锁。注意:必须在最后阶段释放锁资源。
1. 在准备阶段,RM 执行实际的业务操作,但不提交事务,资源锁定;
2. 在提交阶段 TM 会接受 RM 在准备阶段的执行回复,只要有任意 RM 执行失败,TM 会通知所有 RM 执行回滚操作;否则,TM 将会通知所有 RM 提交该事务。提交阶段结束,资源锁释放。
2. 在提交阶段 TM 会接受 RM 在准备阶段的执行回复,只要有任意 RM 执行失败,TM 会通知所有 RM 执行回滚操作;否则,TM 将会通知所有 RM 提交该事务。提交阶段结束,资源锁释放。
优点:
总的来说,XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低。
总的来说,XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低。
缺点:
1. 资源锁需要等到两个阶段结束才释放,性能较差:XA 有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景。
2. 需要本地数据库支持 XA 协议:XA 目前在商业数据库支持得比较理想,在 MySQL 数据库中支持得不太理想,MySQL 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。
1. 资源锁需要等到两个阶段结束才释放,性能较差:XA 有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景。
2. 需要本地数据库支持 XA 协议:XA 目前在商业数据库支持得比较理想,在 MySQL 数据库中支持得不太理想,MySQL 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。
Seata 方案:基于阿里开源分布式事务框架 Seata
Seata 解决了传统 2PC 问题,通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。
目前提供 AT 模式(即 2PC)和 TCC 模式的分布式事务解决方案。
TC:Transaction Coordinator,事务协调器,是独立的中间件,需要独立部署运行(Seata 服务端),它维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通信协调各分支事务的提交或回滚。
TM:Transaction Manager,事务管理器,TM 需要嵌入应用程序中工作(Seata 客户端,可以理解为一个 jar 包),它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。
RM:Resource Manager,控制分支事务(Seata 客户端,可以理解为一个 jar 包),负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。
Seata 解决了传统 2PC 问题,通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。
目前提供 AT 模式(即 2PC)和 TCC 模式的分布式事务解决方案。
TC:Transaction Coordinator,事务协调器,是独立的中间件,需要独立部署运行(Seata 服务端),它维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通信协调各分支事务的提交或回滚。
TM:Transaction Manager,事务管理器,TM 需要嵌入应用程序中工作(Seata 客户端,可以理解为一个 jar 包),它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。
RM:Resource Manager,控制分支事务(Seata 客户端,可以理解为一个 jar 包),负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。
优点:
1. 性能好,不长时间占用连接资源。
2. 以高效并且对业务 0 侵入的方式解决为服务场景下面临的分布式事务问题。
1. 性能好,不长时间占用连接资源。
2. 以高效并且对业务 0 侵入的方式解决为服务场景下面临的分布式事务问题。
设计思想:
对业务无侵入,在传统 2PC 基础上演进,解决传统 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。
对业务无侵入,在传统 2PC 基础上演进,解决传统 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。
Seata 实现 2PC 与传统 2PC 的区别:
架构层次方面,传统 2PC 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。
两阶段提交方面,传统 2PC 无论第二阶段的决议是 commit 还是 rollback,事务性资源的锁都要保持到 phase2 完成才释放,而 Seata 的做法是在 phase1 就将本地事务提交,这样可以省去 phase2 持锁时间,整体提高效率。
架构层次方面,传统 2PC 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。
两阶段提交方面,传统 2PC 无论第二阶段的决议是 commit 还是 rollback,事务性资源的锁都要保持到 phase2 完成才释放,而 Seata 的做法是在 phase1 就将本地事务提交,这样可以省去 phase2 持锁时间,整体提高效率。
以新用户注册送积分为例
具体执行流程如下:
1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2. 用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
3. 用户服务执行分支事务,向用户表插入一条记录。
4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 想 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
6. 用户服务分支事务执行完毕。
7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
其他说明:
1. 为啥积分服务没有 TM?因为用户服务负责开启这一事务,所以 TM 在用户服务而无需在积分服务也有这个东西。
具体执行流程如下:
1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2. 用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
3. 用户服务执行分支事务,向用户表插入一条记录。
4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 想 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
6. 用户服务分支事务执行完毕。
7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
其他说明:
1. 为啥积分服务没有 TM?因为用户服务负责开启这一事务,所以 TM 在用户服务而无需在积分服务也有这个东西。
但这样其实不好吧?总不能因为加不了积分,使得用户注册失败。
所以这种场景其实应该异步加积分。
所以这种场景其实应该异步加积分。
3PC,三阶段提交
三阶段提交是在二阶段提交上的改进版本,3PC 最关键要解决的就是协调者和参与者同时挂掉的问题,所以 3PC 把 2PC 的准备阶段再次一分为二,这样三阶段提交。
阶段一
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
协调者根据参与者响应情况,有以下两种可能。
情况1:
所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:
只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:
所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 doCommit 请求。
b) 参与者收到 doCommit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 doCommit 请求。
b) 参与者收到 doCommit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:
只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
柔性事务解决方案架构
TCC,补偿事务
TCC 事务补偿型方案
TCC 是 Try、Confirm、Cancel 三个词的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销 Cancel。
Try 操作做业务检查及资源预留,Confirm 做业务确认,Cancel 实现一个与 Try 相反的操作即回滚操作。
TM 首先发起所有的分支事务的 try 操作,任何一个分支事务的 try 操作执行失败,TM 将会发起所有分支事务的 cancel 操作;若 try 全部成功,TM 将会发起所有分支事务的 confirm 操作,其中 confirm/cancel 操作若执行失败,TM 会进行重试。
TCC 分为三个阶段,具体如下:
1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的 Confirm 一起才能真正构成一个完整的业务逻辑。
2. Confirm 阶段是做确认提交,Try 阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 try 成功,Confirm 一定成功。若 Confirm 阶段真的出错了,需引入重试机制或人工介入处理。
3. Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用 TCC 则认为 Cancel 阶段一定是成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工介入处理。
4. TM 事务管理器,可以实现为独立的服务,也可以让全局事务发起方充当 TM 的角色,TM 独立出来是为了成为公用组件,是为了考虑系统结构和软件复用。TM 在发起全局事务时生成全局事务记录 XID,XID 贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于 Confirm 和 Cancel 失败需进行重试,因此需要实现幂等。
TCC 是 Try、Confirm、Cancel 三个词的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销 Cancel。
Try 操作做业务检查及资源预留,Confirm 做业务确认,Cancel 实现一个与 Try 相反的操作即回滚操作。
TM 首先发起所有的分支事务的 try 操作,任何一个分支事务的 try 操作执行失败,TM 将会发起所有分支事务的 cancel 操作;若 try 全部成功,TM 将会发起所有分支事务的 confirm 操作,其中 confirm/cancel 操作若执行失败,TM 会进行重试。
TCC 分为三个阶段,具体如下:
1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的 Confirm 一起才能真正构成一个完整的业务逻辑。
2. Confirm 阶段是做确认提交,Try 阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 try 成功,Confirm 一定成功。若 Confirm 阶段真的出错了,需引入重试机制或人工介入处理。
3. Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用 TCC 则认为 Cancel 阶段一定是成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工介入处理。
4. TM 事务管理器,可以实现为独立的服务,也可以让全局事务发起方充当 TM 的角色,TM 独立出来是为了成为公用组件,是为了考虑系统结构和软件复用。TM 在发起全局事务时生成全局事务记录 XID,XID 贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于 Confirm 和 Cancel 失败需进行重试,因此需要实现幂等。
实现:一个完整的业务活动由一个主业务服务于若干的从业务服务组成。主业务服务负责发起并完成整个业务活动。从业务服务提供TCC型业务操作。业务活动管理器控制业务活动的一致性,它登记业务活动的操作,并在业务活动提交时确认所有的TCC型操作的Confirm操作,在业务活动取消时调用所有TCC型操作的Cancel操作。
分支事务成功的情况
分支事务失败的情况
成本:实现TCC操作的成本较高,业务活动结束的时候Confirm和Cancel操作的执行成本。业务活动的日志成本。
使用范围:强隔离性,严格一致性要求的业务活动。适用于执行时间较短的业务,比如处理账户或者收费等等。
特点:不与具体的服务框架耦合,位于业务服务层,而不是资源层,可以灵活的选择业务资源的锁定粒度。TCC里对每个服务资源操作的是本地事务,数据被锁住的时间短,可扩展性好,可以说是为独立部署的SOA服务而设计的。
TCC 框架
tcc-transaction
Hmily
ByteTCC
EasyTransaction
Seata 也支持 TCC,但 Seata 的 TCC 模式对 Spring Cloud 并没有提供支持。
tcc-transaction
Hmily
ByteTCC
EasyTransaction
Seata 也支持 TCC,但 Seata 的 TCC 模式对 Spring Cloud 并没有提供支持。
TCC 三种异常需要处理
空回滚
在没有调用 TCC 资源 try 方法的情况下,调用了二阶段的 cancel 方法,cancel 方法需要识别出这是一个空回滚,然后直接返回成功。
出现原因是当一个分支事务所在服务宕机或者网络异常,分支事务调用记录为失败,这个时候其实是没有执行 try 阶段,当故障恢复后,分布式事务进行回滚会调用二阶段的 cancel 方法,从而形成空回滚。
解决思路的关键是要识别出这个空回滚。思路很简单,就是需要知道一阶段是否执行,如果执行了,那么就是正常回滚;如果没执行,就空回滚。前面已经说过 TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 try 方法里会差一条记录,表示一阶段执行了。cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则空回滚。
出现原因是当一个分支事务所在服务宕机或者网络异常,分支事务调用记录为失败,这个时候其实是没有执行 try 阶段,当故障恢复后,分布式事务进行回滚会调用二阶段的 cancel 方法,从而形成空回滚。
解决思路的关键是要识别出这个空回滚。思路很简单,就是需要知道一阶段是否执行,如果执行了,那么就是正常回滚;如果没执行,就空回滚。前面已经说过 TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 try 方法里会差一条记录,表示一阶段执行了。cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则空回滚。
幂等
通过前面介绍已经了解到,为保证 TCC 二阶段提交重试机制不会引发数据不一致,要求 TCC 二阶段 try、confirm、cancel 接口保证幂等,这样不会重复使用或释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
悬挂
悬挂就是对于一个分布式事务,其二阶段 cancel 接口比 try 接口先执行。
出现原因是在 RPC 调用分支事务 try 时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM 就会通知 RM 回滚该分布式事务,可能会滚完成后,RPC 请求才到达参与者真正执行,而一个 try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,就称为悬挂,及业务资源预留后没法继续处理。
出现原因是在 RPC 调用分支事务 try 时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM 就会通知 RM 回滚该分布式事务,可能会滚完成后,RPC 请求才到达参与者真正执行,而一个 try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,就称为悬挂,及业务资源预留后没法继续处理。
优化方案,以账户 A 给账户 B 转账为例
账户 A
try:
try 幂等校验
try 悬挂处理
检查余额是否够 100 元
扣减 100 元
try 幂等校验
try 悬挂处理
检查余额是否够 100 元
扣减 100 元
confirm:
空
空
cancel:
cancel 幂等校验
cancel 空回滚处理
增加可用余额 100 元
cancel 幂等校验
cancel 空回滚处理
增加可用余额 100 元
账户 B
try:
空
空
confirm:
confirm 幂等校验
正式增加 100 元
confirm 幂等校验
正式增加 100 元
cancel:
空
空
基于可靠消息的最终一致性方案
可靠消息最终一致性方案
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
实现:可靠消息最终一致性方案是指当事务发起方执行完成本地事务后发出一条消息,事务参与方(消息消费者)一定能够接受消息并处理事务成功。此方案强调的是只要消息发给事务参与方,最终事务要达成一致。
此方案是利用消息中间件完成。
事务发起方(消息生产者)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费者)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。
此方案是利用消息中间件完成。
事务发起方(消息生产者)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费者)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。
分布式事务问题
1. 本地事务与消息发送的原子性问题
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。
begin transaction;
1. 发送 MQ;
2. 本地事务执行;
commit transaction;
1. 发送 MQ;
2. 本地事务执行;
commit transaction;
可能发送消息成功,本地事务执行失败。
begin transaction;
1. 本地事务执行;
2. 发送 MQ;
commit transaction;
1. 本地事务执行;
2. 发送 MQ;
commit transaction;
如果发送 MQ 消息失败,就会抛出异常,导致数据库事务回滚。
如果是发送 MQ 超时异常,数据库回滚,但 MQ 其实已经正常发送了,会导致不一样。
如果是发送 MQ 超时异常,数据库回滚,但 MQ 其实已经正常发送了,会导致不一样。
2. 事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重新接收消息。
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重新接收消息。
3. 消息重复消费的问题
由于网络 2 的存在,若某一个消息节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。
要解决消息重复消费的问题,就要实现事务参与方的方法幂等性。
由于网络 2 的存在,若某一个消息节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。
要解决消息重复消费的问题,就要实现事务参与方的方法幂等性。
消息:业务处理服务在业务事务回滚后,向实时消息服务取消发送。消息发送状态确认系统定期找到未确认发送或者回滚发送的消息,向业务处理服务询问消息状态,业务处理服务根据消息ID或者消息内容确认该消息是否有效。被动方的处理结果不会影响主动方的处理结果,被动方的消息处理操作是幂等操作。
成本:可靠的消息系统建设成本,一次消息发送需要两次请求,业务处理服务需要实现消息状态回查接口。
优点:消息数据独立存储,独立伸缩,降低业务系统和消息系统之间的耦合。对最终一致性时间敏感度较高,降低业务被动方的实现成本。兼容所有实现JMS标准的MQ中间件,确保业务数据可靠的前提下,实现业务的最终一致性,理想状态下是准实时的一致性。
方案详解
消息发送一致性
消息中间件在分布式系统中的核心作用就是异步通讯、应用解耦和并发缓冲(流量削峰)。在分布式环境下,需要通过网络进行通讯,就引入了数据传输的不确定性,也就是 CAP 理论中的分区容错性。
消息发送一致性是指产生消息的业务动作与消息发送一致,也就是说如果业务操作成功,那么由这个业务操作产生的消息一定要发送出去,否则就丢失。
消息发送一致性是指产生消息的业务动作与消息发送一致,也就是说如果业务操作成功,那么由这个业务操作产生的消息一定要发送出去,否则就丢失。
保证消息一致的变通做法
1. 发送消息:主动方应用把消息发送给消息中间件,消息状态标记为“待确认”状态。
2. 消息中间件接收到消息后,把消息持久化到消息存储中,但并不影响被动方投递消息。
3. 消息中间件返回消息持久化结果,主动方根据返回结果进行判断如何进行业务操作处理:
3.1 失败:放弃执行业务操作处理,结束,必要时向上游返回处理结果;
3.2 成功:执行业务操作处理;
4. 业务操作完成后,把业务操作结果返回给消息中间件。
5. 消息中间件收到业务操作结果后,根据业务结果进行处理:
5.1 失败:删除消息存储中的消息,结束;
5.2 成功:更新消息存储中的消息状态为“待发送”,然后执行消息投递;
6. 前面的正向流程都成功后,向北东方应用投递消息。
在上面的处理流程中,任何一个环节都有可能出现问题。
2. 消息中间件接收到消息后,把消息持久化到消息存储中,但并不影响被动方投递消息。
3. 消息中间件返回消息持久化结果,主动方根据返回结果进行判断如何进行业务操作处理:
3.1 失败:放弃执行业务操作处理,结束,必要时向上游返回处理结果;
3.2 成功:执行业务操作处理;
4. 业务操作完成后,把业务操作结果返回给消息中间件。
5. 消息中间件收到业务操作结果后,根据业务结果进行处理:
5.1 失败:删除消息存储中的消息,结束;
5.2 成功:更新消息存储中的消息状态为“待发送”,然后执行消息投递;
6. 前面的正向流程都成功后,向北东方应用投递消息。
在上面的处理流程中,任何一个环节都有可能出现问题。
常规 MQ 消息处理流程和特点
常规的 MQ 处理流程无法实现消息的一致性。
投递消息的本质就是消息消费,可以细化。
投递消息的本质就是消息消费,可以细化。
消息重复发送问题和业务接口幂等性设计
对于未确认的消息,采用按规则重新投递的方式进行处理。对于以上流程,消息重复发送会导致业务处理接口出现重复调用的问题。消息消费过程中消息重复发送的主要原因就是消费者成功接收处理完消息后,消息中间件没有及时更新投递状态导致的。如果允许消息重复发送,那么消费方应该实现业务接口的幂等性设计。
可靠消息最终一致性方案
本地消息表
本地消息表方案:基于本地数据库 + MQ,维护本地状态(进行中),通过 MQ 调用服务,完成后响应一条消息回调,将状态改为完成。需要配合定时任务扫表、重新发送消息调用服务,需要保证幂等性。
其核心思想是将分布式事务拆分成本地事务进行处理。
方案通过在消费者额外新建事务消息表,消费者处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,提供者基于消息中间件消费事务消息表中的事务。
其核心思想是将分布式事务拆分成本地事务进行处理。
方案通过在消费者额外新建事务消息表,消费者处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,提供者基于消息中间件消费事务消息表中的事务。
实现思路:
1. 主动方应用系统通过业务操作完成对业务数据的操作,在准备发送消息的时候将消息存储在主动方应用系统一份,另一份发送到实时消息服务。
2. 被动方应用系统监听实时消息系统中的消息,当被动方完成消息处理后通过调用主动方接口完成消息确认。
3. 主动方接收到消息确认以后删除消息数据。
4. 通过消息查询服务查询到消息被接收之后在规定的时间内没有返回 ack 确认消息就通过消息恢复系统重新发送消息。
1. 主动方应用系统通过业务操作完成对业务数据的操作,在准备发送消息的时候将消息存储在主动方应用系统一份,另一份发送到实时消息服务。
2. 被动方应用系统监听实时消息系统中的消息,当被动方完成消息处理后通过调用主动方接口完成消息确认。
3. 主动方接收到消息确认以后删除消息数据。
4. 通过消息查询服务查询到消息被接收之后在规定的时间内没有返回 ack 确认消息就通过消息恢复系统重新发送消息。
条件:
服务消费者需要创建一张消息表,用来记录消息状态。
服务消费者和提供者需要支持幂等。
需要补偿逻辑。
每个节点上起定时线程,检查未处理完成或发出失败的消息,重新发出消息,即重试机制和幂等性机制。
服务消费者需要创建一张消息表,用来记录消息状态。
服务消费者和提供者需要支持幂等。
需要补偿逻辑。
每个节点上起定时线程,检查未处理完成或发出失败的消息,重新发出消息,即重试机制和幂等性机制。
处理流程:
1. 服务消费者把业务数据和消息一同提交,发起事务。
2. 消息经过MQ发送到服务提供方,服务消费者等待处理结果。
3. 服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。
容错处理情况如下:
当步骤1处理出错,事务回滚,相当于什么都没有发生。
当步骤2、3处理出错,由于消息保存在消费者表中,可以重新发送到MQ进行重试。
如果步骤3处理出错,且是业务上的失败,服务提供者发送消息通知消费者事务失败,且此时变为消费者发起回滚事务进行回滚逻辑。
1. 服务消费者把业务数据和消息一同提交,发起事务。
2. 消息经过MQ发送到服务提供方,服务消费者等待处理结果。
3. 服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。
容错处理情况如下:
当步骤1处理出错,事务回滚,相当于什么都没有发生。
当步骤2、3处理出错,由于消息保存在消费者表中,可以重新发送到MQ进行重试。
如果步骤3处理出错,且是业务上的失败,服务提供者发送消息通知消费者事务失败,且此时变为消费者发起回滚事务进行回滚逻辑。
优点:
1. 消息的时效性比较高。
2. 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
3. 方案轻量级,容易实现。
1. 消息的时效性比较高。
2. 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
3. 方案轻量级,容易实现。
缺点:
1. 与具体的业务场景绑定,耦合性强,不可公用。
2. 消息数据与业务数据同库,占用业务系统资源。
3. 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
1. 与具体的业务场景绑定,耦合性强,不可公用。
2. 消息数据与业务数据同库,占用业务系统资源。
3. 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
本地消息表方案,以新用户注册送积分为例
本地消息表方案最初是 eBay 提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
本地消息表方案最初是 eBay 提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
1. 用户注册
用户服务在本地事务新增用户和增加“积分消息日志”(用户表和消息表通过本地事务保证一致)
begin transaction;
1. 新增用户
2. 存储积分消息日志
commit transaction;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
用户服务在本地事务新增用户和增加“积分消息日志”(用户表和消息表通过本地事务保证一致)
begin transaction;
1. 新增用户
2. 存储积分消息日志
commit transaction;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
2. 定时任务扫描日志
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日至,否则等待定时任务下一周期重试。
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日至,否则等待定时任务下一周期重试。
3. 消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用 MQ 的 ack(消息确认)机制,消费者监听 MQ,如果消费者接收到消息并且业务处理完成后向 MQ 发送 ack(消息确认),此时说明消费者正常消费消息完成,MQ 将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后向消息中间件回应 ack,否则消息中间件将重复投递此消息。
由于消息会重复投递,积分服务“增加积分”功能需要实现幂等。
如何保证消费者一定能消费到消息呢?
这里可以使用 MQ 的 ack(消息确认)机制,消费者监听 MQ,如果消费者接收到消息并且业务处理完成后向 MQ 发送 ack(消息确认),此时说明消费者正常消费消息完成,MQ 将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后向消息中间件回应 ack,否则消息中间件将重复投递此消息。
由于消息会重复投递,积分服务“增加积分”功能需要实现幂等。
MQ 事务消息
MQ 事务消息方案
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。
对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作;
步骤二出错,则整个事务失败,不会执行A的本地操作;
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息;
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。
对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作;
步骤二出错,则整个事务失败,不会执行A的本地操作;
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息;
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
支持事务消息的MQ,其支持事务消息的方式采用类似于二阶段提交。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
支持事务消息的MQ,其支持事务消息的方式采用类似于二阶段提交。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
条件:
a) 需要补偿逻辑
b) 业务处理逻辑需要幂等
a) 需要补偿逻辑
b) 业务处理逻辑需要幂等
处理流程:
c) 消费者向MQ发送half消息。
d) MQ Server将消息持久化后,向发送方ack确认消息发送成功。
e) 消费者开始执行事务逻辑。
f) 消费者根据本地事务执行结果向MQ Server提交二次确认或者回滚。
g) MQ Server收到commit状态则将half消息标记可投递状态。
h) 服务提供者收到该消息,执行本地业务逻辑。返回处理结果。
c) 消费者向MQ发送half消息。
d) MQ Server将消息持久化后,向发送方ack确认消息发送成功。
e) 消费者开始执行事务逻辑。
f) 消费者根据本地事务执行结果向MQ Server提交二次确认或者回滚。
g) MQ Server收到commit状态则将half消息标记可投递状态。
h) 服务提供者收到该消息,执行本地业务逻辑。返回处理结果。
优点:
消息数据独立存储,降低业务系统与消息系统之间的耦合。
吞吐量优于本地消息表方案。
消息数据独立存储,降低业务系统与消息系统之间的耦合。
吞吐量优于本地消息表方案。
缺点:
一次消息发送需要两次网络请求(half消息 + commit/rollback)。需要实现消息回查接口。
一次消息发送需要两次网络请求(half消息 + commit/rollback)。需要实现消息回查接口。
RocketMQ 事务消息方案
RocketMQ 事务消息设计主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题。
RocketMQ 的设计中,broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;
RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;
RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在 RocketMQ4.3 后实现了完整的事务消息,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。
RocketMQ 事务消息设计主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题。
RocketMQ 的设计中,broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;
RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;
RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在 RocketMQ4.3 后实现了完整的事务消息,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。
执行流程如下:
为了方便理解,还以注册送积分的例子来描述整个流程。
Producer 即 MQ 发送方,本例中是用户服务,负责新增用户;MQ 订阅方即消息消费方,本例中是积分服务,负责新增积分。
为了方便理解,还以注册送积分的例子来描述整个流程。
Producer 即 MQ 发送方,本例中是用户服务,负责新增用户;MQ 订阅方即消息消费方,本例中是积分服务,负责新增积分。
1. Producer 发送事务消息
Producer 发送事务消息至 MQ Server,MQ Server 将消息状态标记为 Prepared(预备状态),注意此时这条消息消费者(MQ 订阅方)是无法消费到的。
本例中,Producer 发送“增加积分消息”到 MQ Server。
Producer 发送事务消息至 MQ Server,MQ Server 将消息状态标记为 Prepared(预备状态),注意此时这条消息消费者(MQ 订阅方)是无法消费到的。
本例中,Producer 发送“增加积分消息”到 MQ Server。
2. MQ Server 回应消息发送成功
MQ Server 接收到 Producer 发送的消息则回应发送成功,表示 MQ 已接收到消息。
MQ Server 接收到 Producer 发送的消息则回应发送成功,表示 MQ 已接收到消息。
3. Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer 执行添加用户操作。
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer 执行添加用户操作。
4. 消息投递
若 Producer 本地事务执行成功则自动向 MQ Server 发送 commit 消息,MQ Server 接收到 commit 消息后将“增加积分消息”状态标记为可消费,此时 MQ 订阅方(消息消费者,积分服务)即正常消费消息;
若 Producer 本地事务执行失败则自动向 MQ Server 发送 rollback 消息,MQ Server 接收到 rollback 消息后将删除“增加积分消息”。
MQ 订阅方(积分服务)消费消息,消费成功则向 MQ 回应 ack,否则将重复接收消息。这里 ack 默认自动回应,即程序执行正常则自动回应 ack。
若 Producer 本地事务执行成功则自动向 MQ Server 发送 commit 消息,MQ Server 接收到 commit 消息后将“增加积分消息”状态标记为可消费,此时 MQ 订阅方(消息消费者,积分服务)即正常消费消息;
若 Producer 本地事务执行失败则自动向 MQ Server 发送 rollback 消息,MQ Server 接收到 rollback 消息后将删除“增加积分消息”。
MQ 订阅方(积分服务)消费消息,消费成功则向 MQ 回应 ack,否则将重复接收消息。这里 ack 默认自动回应,即程序执行正常则自动回应 ack。
5. 事务回查
如果执行 Producer 端本地事务过程中,执行端挂掉、或者超时,MQ Server 将会不停询问侗族的其他 Producer 来获取事务执行状态。这个过程叫“事务回查”。MQ Server 会根据事务会查结果来决定是否投递消息。
如果执行 Producer 端本地事务过程中,执行端挂掉、或者超时,MQ Server 将会不停询问侗族的其他 Producer 来获取事务执行状态。这个过程叫“事务回查”。MQ Server 会根据事务会查结果来决定是否投递消息。
以上主干流程已由 RocketMQ 实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
RocketMQ 提供 RocketMQLocalTransactionListener 接口
发送事务消息:以上是 RocketMQ 提供用于发送事务消息的 API
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性。
本案例使用了 RocketMQ 作为消息中间件,RocketMQ 主要解决了两个功能:
1. 本地事务与消息发送的原子性问题;
2. 事务参与方接收消息的可靠性。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
本案例使用了 RocketMQ 作为消息中间件,RocketMQ 主要解决了两个功能:
1. 本地事务与消息发送的原子性问题;
2. 事务参与方接收消息的可靠性。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
独立消息服务方案
实现思路:
1. 预发送消息:主动方应用系统预发送消息,由消息服务子系统存储消息。如果存储失败,那么也就无法进行业务操作;如果返回存储成功,然后执行业务操作。
2. 执行业务操作:执行业务操作如果成功,将业务操作执行成功的状态发送到消息服务子系统。消息服务子系统修改消息标识为“可发送”状态。
3. 发送消息到实时消息服务:当消息的状态发生改变时,立刻将消息发送到实时消息服务中。之后,消息将会被消息业务的消费端监听到,然后被消费。
4. 消息状态子系统:相当于定时任务系统,在消息服务子系统中定时查找确认超时的消息,在主动方应用系统中也去定时查找没有处理成功的任务,进行相应的处理。
5. 消息消费:当消息被消费的时候,向实时消息服务发送 ack,然后实时消息服务删除消息。同时调用消息服务子系统修改消息为“被消费”状态。
6. 消息恢复子系统:当消费方返回消息的时候,由于网络中断等其他原因导致消息没有及时确认,那么需要消息恢复子系统定时查找出在消息服务子系统中没有确认的消息。将没有被确认的消息放到实时消息服务中,进行重做,因为被动方应用系统的接口是幂等的。
1. 预发送消息:主动方应用系统预发送消息,由消息服务子系统存储消息。如果存储失败,那么也就无法进行业务操作;如果返回存储成功,然后执行业务操作。
2. 执行业务操作:执行业务操作如果成功,将业务操作执行成功的状态发送到消息服务子系统。消息服务子系统修改消息标识为“可发送”状态。
3. 发送消息到实时消息服务:当消息的状态发生改变时,立刻将消息发送到实时消息服务中。之后,消息将会被消息业务的消费端监听到,然后被消费。
4. 消息状态子系统:相当于定时任务系统,在消息服务子系统中定时查找确认超时的消息,在主动方应用系统中也去定时查找没有处理成功的任务,进行相应的处理。
5. 消息消费:当消息被消费的时候,向实时消息服务发送 ack,然后实时消息服务删除消息。同时调用消息服务子系统修改消息为“被消费”状态。
6. 消息恢复子系统:当消费方返回消息的时候,由于网络中断等其他原因导致消息没有及时确认,那么需要消息恢复子系统定时查找出在消息服务子系统中没有确认的消息。将没有被确认的消息放到实时消息服务中,进行重做,因为被动方应用系统的接口是幂等的。
优点:
1. 消息服务独立部署,独立维护,独立伸缩。
2. 消息存储可以按需选择不同的数据库来集成实现。
3. 消息服务可以被相同的的使用场景使用,降低重复建设服务的成本。
4. 从分布式服务应用设计开发角度实现了消息数据的可靠性,消息数据的可靠性不依赖于MQ中间件,弱化了对MQ中间件特性的依赖。
5. 降低了业务系统与消息系统之间的耦合,有利于系统的扩展维护。
1. 消息服务独立部署,独立维护,独立伸缩。
2. 消息存储可以按需选择不同的数据库来集成实现。
3. 消息服务可以被相同的的使用场景使用,降低重复建设服务的成本。
4. 从分布式服务应用设计开发角度实现了消息数据的可靠性,消息数据的可靠性不依赖于MQ中间件,弱化了对MQ中间件特性的依赖。
5. 降低了业务系统与消息系统之间的耦合,有利于系统的扩展维护。
缺点:
1. 一次消息发送需要两次请求。
2. 主动方应用系统需要实现业务操作状态的校验与查询接口。
1. 一次消息发送需要两次请求。
2. 主动方应用系统需要实现业务操作状态的校验与查询接口。
最大努力通知
最大努力通知型方案
实现:
业务活动的主动方在完成处理之后向业务活动的被动方发送消息,允许消息丢失。
业务活动的被动方根据定时策略,向业务活动的主动方查询,恢复丢失的业务消息。
通过对最大努力通知的理解,采用 MQ 的 ack 机制就可以实现最大努力通知方案。
业务活动的主动方在完成处理之后向业务活动的被动方发送消息,允许消息丢失。
业务活动的被动方根据定时策略,向业务活动的主动方查询,恢复丢失的业务消息。
通过对最大努力通知的理解,采用 MQ 的 ack 机制就可以实现最大努力通知方案。
方案一:本方案是利用 MQ 的 ack 机制由 MQ 向接收通知方发送通知,流程如下:
1. 发起通知方将通知发给 MQ。
使用普通消息机制将通知发给 MQ。
注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。
2. 接收通知方监听 MQ。
3. 接收方接收消息,业务处理完成回应 ack。
4. 接收方若没有回应 ack 则 MQ 会重复通知。
MQ 会按照间隔 1min、5min、10min、30min、1h、2h、5h、10h 的方式,逐步拉大通知间隔(如果 MQ 采用 RocketMQ,在 broker 中可以进行配置),直到达到通知要求的时间窗口上限。
5. 接收方可通过消息校对接口来校对消息的一致性。
1. 发起通知方将通知发给 MQ。
使用普通消息机制将通知发给 MQ。
注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。
2. 接收通知方监听 MQ。
3. 接收方接收消息,业务处理完成回应 ack。
4. 接收方若没有回应 ack 则 MQ 会重复通知。
MQ 会按照间隔 1min、5min、10min、30min、1h、2h、5h、10h 的方式,逐步拉大通知间隔(如果 MQ 采用 RocketMQ,在 broker 中可以进行配置),直到达到通知要求的时间窗口上限。
5. 接收方可通过消息校对接口来校对消息的一致性。
方案二:也利用 MQ 的 ack 机制,与方案一不同的是应用程序向接收通知方发送通知,流程如下:
1. 发起通知方将通知发给 MQ。
使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知结果先发给 MQ。
2. 通知程序监听 MQ,接收 MQ 的消息。
方案一中接收方直接监听 MQ,方案二中由通知程序监听 MQ。
通知程序若没有回应 ack 则 MQ 会重复通知。
3. 通知程序通过互联网接口协议(如 HTTP、WebService)调用接收通知方接口,完成通知。
通知程序调用接收通知方接口成功就表示通知成功,即消费 MQ 消息成功,MQ 将不再向通知程序投递通知消息。
4. 接收方可通过消息校对接口来校对消息的一致性。
1. 发起通知方将通知发给 MQ。
使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知结果先发给 MQ。
2. 通知程序监听 MQ,接收 MQ 的消息。
方案一中接收方直接监听 MQ,方案二中由通知程序监听 MQ。
通知程序若没有回应 ack 则 MQ 会重复通知。
3. 通知程序通过互联网接口协议(如 HTTP、WebService)调用接收通知方接口,完成通知。
通知程序调用接收通知方接口成功就表示通知成功,即消费 MQ 消息成功,MQ 将不再向通知程序投递通知消息。
4. 接收方可通过消息校对接口来校对消息的一致性。
方案一和方案二的不同点:
1. 方案一中接收方与 MQ 接口,即接收方监听 MQ,此方案主要应用于内部应用之间的通知。
2. 方案二中由通知程序与 MQ 接口,通知程序监听 MQ,收到 MQ 的消息后由通知程序通过 RPC 调用接收方。此方案主要应用于外部应用之间的通知,例如支付宝、微信支付结果通知。
1. 方案一中接收方与 MQ 接口,即接收方监听 MQ,此方案主要应用于内部应用之间的通知。
2. 方案二中由通知程序与 MQ 接口,通知程序监听 MQ,收到 MQ 的消息后由通知程序通过 RPC 调用接收方。此方案主要应用于外部应用之间的通知,例如支付宝、微信支付结果通知。
约束:被动方的处理结果不影响主动方的处理结果。
成本:业务查询与校对系统的建设成本。
使用范围:对业务最终一致性的时间敏感度低。跨企业的业务活动。
特点:业务活动的主动方在完成业务处理之后,向业务活动的被动方发送通知消息。主动方可以设置时间阶梯通知规则,在通知失败后按规则重复通知,知道通知N次后不再通知。主动方提供校对查询接口给被动方按需校对查询,用户恢复丢失的业务消息。
适用范围:银行通知,商户通知。
最大努力通知与可靠消息最终一致性有什么不同?
1. 解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发送出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
可靠消息一致性,发起通知方需要保证将消息发送出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
2. 业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠地通知出去。
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠地通知出去。
3. 技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制,即尽最大努力将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制,即尽最大努力将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
最大努力通知方案,以充值场景为例
交互流程:
1. 账户系统调用充值系统接口
2. 充值系统完成支付处理想账户系统发起充值结果通知;若通知失败,则充值系统按策略进行重复通知。
3. 账户系统接收到充值结果通知修改充值状态。
4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
1. 账户系统调用充值系统接口
2. 充值系统完成支付处理想账户系统发起充值结果通知;若通知失败,则充值系统按策略进行重复通知。
3. 账户系统接收到充值结果通知修改充值状态。
4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
具体包括:
1. 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动想通知方查询消息信息来满足需求。
具体包括:
1. 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动想通知方查询消息信息来满足需求。
Sagas 事务模型,最终一致性
对比
2PC:强一致性;
3PC:相对于2PC引入超时机制;
TCC:业务层面的补偿型分布式事务;
本地消息表:利用各系统本地事务实现分布式事务;
消息事务:以RocketMQ为代表,通过中间件实现;
最大努力通知:柔性事务思想。
2PC:强一致性;
3PC:相对于2PC引入超时机制;
TCC:业务层面的补偿型分布式事务;
本地消息表:利用各系统本地事务实现分布式事务;
消息事务:以RocketMQ为代表,通过中间件实现;
最大努力通知:柔性事务思想。
总结
高并发易落地的分布式事务是我们所追求的,而分布式事务产生的场景较为复杂,比如跨 JVM 进程、跨数据库实例、多服务访问等,对于复杂问题的解决思路往往比结论更为重要。
当然不同业务要求不同,一个好的分布式事务需要适配自身业务特点,找到更合适的结合点。该如何进行权衡、怎样选择,是一个优秀开发者或者架构师的必修课。
一致性保证:XA > TCC = SAGA > 事务消息
业务友好性:XA > 事务消息 > SAGA > TCC
性能损耗:XA > TCC > SAGA = 事务消息
高并发易落地的分布式事务是我们所追求的,而分布式事务产生的场景较为复杂,比如跨 JVM 进程、跨数据库实例、多服务访问等,对于复杂问题的解决思路往往比结论更为重要。
当然不同业务要求不同,一个好的分布式事务需要适配自身业务特点,找到更合适的结合点。该如何进行权衡、怎样选择,是一个优秀开发者或者架构师的必修课。
一致性保证:XA > TCC = SAGA > 事务消息
业务友好性:XA > 事务消息 > SAGA > TCC
性能损耗:XA > TCC > SAGA = 事务消息
简单总结
在分布式系统、微服务架构大行其道的今天,服务间互相调用出现失败已经成为常态。如何处理异常,如何保证数据一致性,成为微服务设计过程中,绕不开的一个难题。在不同的业务场景下,解决方案会有所差异,常见的方式有:
1. 阻塞式重试
在微服务架构中,阻塞式重试是比较常见的一种方式。伪代码
如上,当请求 B 服务的 API 失败后,发起最多三次重试。如果三次还是失败,就打印日志,继续执行下或向上层抛出错误。这种方式会带来以下问题
调用 B 服务成功,但由于网络超时原因,当前服务认为其失败了,继续重试,这样 B 服务会产生 2 条一样的数据。
通过让 B 服务的 API 支持幂等性来解决。
调用 B 服务失败,由于 B 服务不可用,重试 3 次依然失败,当前服务在前面代码中插入到 DB 的一条记录,就变成了脏数据。
可以通过后台定时脚步去修正数据,但这并不是一个很好的办法。
重试会增加上游对本次调用的延迟,如果下游负载较大,重试会放大下游服务的压力。
这是通过阻塞式重试提高一致性、可用性,必不可少的牺牲。
阻塞式重试适用于业务对一致性要求不敏感的场景下。如果对数据一致性有要求的话,就必须要引入额外的机制来解决。
2. 2PC、3PC 传统事务
3. 使用队列,后台异步处理
在解决方案演化的过程中,引入队列是个比较常见也较好的方式。伪代码
在当前服务将数据写入 DB 后,推送一条消息给 MQ,由独立的服务去消费 MQ 处理业务逻辑。和阻塞式重试相比,虽然 MQ 在稳定性上远高于普通的业务服务,但在推送消息到 MQ 中的调用,还是会有失败的可能性,比如网络问题、当前服务宕机等。这样还是会遇到阻塞式重试相同的问题,即 DB 写入成功了,但推送失败了。
理论上来讲,分布式系统下,涉及多个服务调用的代码都存在这样的情况,在长期运行中,调用失败的情况一定会出现。这也是分布式系统设计的难点之一。
4. TCC 补偿事务
在对事务有要求,且不方便解耦的情况下,TCC 补偿式事务是个较好的选择。
TCC 把调用每个服务都分成 2 个阶段、 3 个操作:
阶段一、Try 操作:对业务资源做检测、资源预留,比如对库存的检查、预扣。
阶段二、Confirm 操作:提交确认 Try 操作的资源预留。比如把库存预扣更新为扣除。
阶段二、Cancel 操作:Try 操作失败后,释放其预扣的资源。比如把库存预扣的加回去。
TCC 要求每个服务都实现上面 3 个操作的 API,服务接入 TCC 事务前一次调用就完成的操作,现在需要分 2 阶段完成、三次操作来完成。
比如一个商城应用需要调用 A 库存服务、B 金额服务、C 积分服务,如下伪代码:
代码中分别调用 A、B、C 服务 API 检查并保留资源,都返回成功了再提交确认(Confirm)操作;如果 C 服务 Try 操作失败后,则分别调用 A、B、C 的 Cancel API 释放其保留的资源。
TCC 在业务上解决了分布式系统下,跨多个服务、跨多个数据库的数据一致性问题。但 TCC 方式依然存在一些问题,实际使用中需要注意,包括上面章节提到的调用失败的情况。
空释放
上面代码中如果 C.Try() 是真正调用失败,那下面多余的 C.Cancel() 调用会出现释放并没有锁定资源的行为。这是因为当前服务无法判断调用失败是不是真的锁定 C 资源了。如果不调用,实际上成功了,但由于网络原因返回失败了,这会导致 C 的资源被锁定,一直得不到释放。
空释放在生产环境经常出现,服务在实现 TCC 事务 API 时,应支持空释放的执行。
时序
上面代码中如果 C.Try() 失败,接着调用 C.Cancel() 操作。因为网络原因,有可能会出现 C.Cancel() 请求会先到 C 服务,C.Try() 请求后到,这会导致空释放问题,同时引起 C 的资源被锁定,一直得不到释放。
所以 C 服务应拒绝释放资源之后的 Try() 操作。具体实现上,可以用唯一事务ID来区分第一次 Try() 还是释放后的 Try()。
调用失败
Cancel 、Confirm 在调用过程中,还是会存在失败的情况,比如常见的网络原因。
Cancel() 或 Confirm() 操作失败都会导致资源被锁定,一直得不到释放。这种情况常见解决方案有:
阻塞式重试。但有同样的问题,比如宕机、一直失败的情况。
写入日志、队列,然后有单独的异步服务自动或人工介入处理。但一样会有问题,写日志或队列时,会存在失败的情况。
理论上来讲非原子性、事务性的二段代码,都会存在中间态,有中间态就会有失败的可能性。
5. 本地消息表(异步确保)
本地消息表最初是 ebay 提出的,它让本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来满足事务特性。
具体做法是在本地事务中插入业务数据时,也插入一条消息数据。然后在做后续操作,如果其他操作成功,则删除该消息;如果失败则不删除,异步监听这个消息,不断重试。
本地消息表是一个很好的思路,可以有多种使用方式:
具体做法是在本地事务中插入业务数据时,也插入一条消息数据。然后在做后续操作,如果其他操作成功,则删除该消息;如果失败则不删除,异步监听这个消息,不断重试。
本地消息表是一个很好的思路,可以有多种使用方式:
配合 MQ
配合服务调用
消息过期
独立消息服务
6. MQ 事务
有些 MQ 的实现支持事务,比如 RocketMQ 。MQ 的事务可以看作独立消息服务的一种具体实现,逻辑完全一致。
所有操作之前先在 MQ 投递个消息,后续操作成功则 Confirm 确认提交消息,失败则Cancel删除消息。MQ 事务也会存在 prepare状态,需要 MQ 的消费处理逻辑来确认业务是否成功。
7. 最大努力通知
总结:从分布式系统实践中来看,要保障数据一致性的场景,必然要引入额外的机制处理。
TCC 的优点是作用于业务服务层,不依赖某个具体数据库、不与具体框架耦合、资源锁的粒度比较灵活,非常适用于微服务场景下。缺点是每个服务都要实现 3 个 API,对于业务侵入和改动较大,要处理各种失败异常。开发者很难完整处理各种情况,找个成熟的框架可以大大降低成本,比如阿里的 Fescar。
本地消息表的优点是简单、不依赖其他服务的改造、可以很好的配合服务调用和 MQ 一起使用,在大多业务场景下都比较实用。缺点是本地数据库多了消息表,和业务表耦合在一起。文中本地消息表方式的示例,来源于作者写的一个库,有兴趣的同学可以参考下 https://github.com/mushroomsir/tcc
MQ 事务和独立消息服务的优点是抽离出一个公共的服务来解决事务问题,避免每个服务都有消息表和服务耦合在一起,增加服务自身的处理复杂性。缺点是支持事务的 MQ 很少;且每次操作前都先调用 API 添加个消息,会增加整体调用的延迟,在绝大多数正常响应的业务场景下,是一种多余的开销。
来源:cnblogs.com/mushroom/p/13788039.html
来源:cnblogs.com/mushroom/p/13788039.html
场景设计
本地执行一个事务,然后有一个远程调用,如何保证两个事务同时成功同时失败?
阻塞式分布式事务
事务消息,半提交,实际上是阻塞式的,整个系统的吞吐量其实是比较低的。
分布式事务最好的解决方案就是避免分布式事务。
一个比较不错的解决方案:调远端进行事务的提交,两种情况:
调用超时
比如远端有一个慢查询 SQL,或者出现网络抖动、阻塞等。分布式系统下,调用的话可能会调用多个节点,中间万一又有超时怎么办?
如果调用超时,就发一个消息,发消息的话可以不断地去重试,保证消息能够发成功;下游保证幂等。
消息监听的话,不管有多少个节点,都去监听这个消息,只要跟本地事务保持数据一致的话,所有节点都去监听这个消息,然后每个节点根据这个消息去进行,事务到底是不是要回滚。但这里有个前提,每一个相关的节点,去提交事务的时候都会要记录一个流水号。因为超时的事务,远端节点不知道事务到底有没有提交,就根据消息里面的这个事务流水号,这个消息到底有没有消费,远程调用到底有没有执行成功。如果没有执行成功,那直接忽略掉消息就行了,如果有执行成功,才去把事务进行回滚,这样就达到一个最终一致性。
这样整个过程并没有阻塞,并没有锁定什么资源,所以整体性能是比较高的,并且能达到最终一致性的效果。
但是也会可能出现类似于数据库脏读的这种情况,因为每个节点都是直接提交了本地事务,所以去做回滚的时候可能会失败的这种极端情况。这样的情况出现的话,只能是去加告警之类的,人工介入解决。
这样整个过程并没有阻塞,并没有锁定什么资源,所以整体性能是比较高的,并且能达到最终一致性的效果。
但是也会可能出现类似于数据库脏读的这种情况,因为每个节点都是直接提交了本地事务,所以去做回滚的时候可能会失败的这种极端情况。这样的情况出现的话,只能是去加告警之类的,人工介入解决。
提交失败
远端提交失败,本地事务就不用提交,两边数据是一致的。
中间件
消息队列
为什么需要消息队列?使用消息队列有什么好处?
缓存
如何正确访问 Redis 中的海量数据,服务才不会挂掉?
搜索
分布式数据库
设计模式
原则
23 个设计模式
单例模式
单例模式双重检查锁为什么要两次校验?
第一次校验:
也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。
也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。
第二次校验:
也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。
也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。
volatile必不可少
禁止指令重排
singleton = new Singleton();
1. 为 singleton 分配内存空间;
2. 初始化 singleton;
3. 将 singleton 指向分配的内存空间。
由于JVM具有指令重排的特性,执行顺序有可能变成1-3-2。指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用getInstance()后发现singleton 不为空,因此返回singleton,但是此时的singleton 还没有被初始化。
保证可见性
架构设计
限流
限流分类
合法性验证限流
比如验证码、IP 黑名单等,这些手段可以有效的防止恶意攻击和爬虫采集;
容器限流
比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以设置最大线程数(maxThreads),当并发超过最大线程数会排队等待执行;而 Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数;
Tomcat 限流
Nginx 限流
服务端限流
比如我们在服务器端通过限流算法实现限流,此为本模块重点。
服务端限流需要配合限流的算法来执行,而算法相当于执行限流的“大脑”,用于指导限制方案的实现。
有人看到「算法」两个字可能就晕了,觉得很深奥,其实并不是。算法就相当于操作某个事务的具体实现步骤汇总,其实并不难懂,不要被它的表象给吓到哦~
限流的常见算法有以下三种:
服务端限流需要配合限流的算法来执行,而算法相当于执行限流的“大脑”,用于指导限制方案的实现。
有人看到「算法」两个字可能就晕了,觉得很深奥,其实并不是。算法就相当于操作某个事务的具体实现步骤汇总,其实并不难懂,不要被它的表象给吓到哦~
限流的常见算法有以下三种:
时间窗口算法
漏桶算法
令牌算法
依赖框架解决
Spring Cloud Alibaba | Sentinel
总结
本模块提供了 6 种具体的实现限流的手段,分别是:
1. Tomcat 使用 maxThreads 来实现限流;
2. Nginx 提供了两种限流方式:
一是通过 limit_req_zone 和 burst 来实现速率限流;
二是通过 limit_conn_zone 和 limit_conn 两个指令控制并发连接的总数。
3. 时间窗口算法借助 Redis 的有序集合可以实现。
4. 漏桶算法可以使用 Redis-Cell 来实现。
5. 令牌算法可以解决 Google 的 guava 包来实现。
6. 借助 Spring Cloud Alibaba Sentinel 组件实现。
需要注意的是借助 Redis 实现的限流方案可用于分布式系统,而 guava 实现的限流只能应用于单机环境。
如果你嫌弃服务器端限流麻烦,甚至可以在不改代码的情况下直接使用容器限流(Nginx 或 Tomcat),但前提是能满足你的业务需求。
1. Tomcat 使用 maxThreads 来实现限流;
2. Nginx 提供了两种限流方式:
一是通过 limit_req_zone 和 burst 来实现速率限流;
二是通过 limit_conn_zone 和 limit_conn 两个指令控制并发连接的总数。
3. 时间窗口算法借助 Redis 的有序集合可以实现。
4. 漏桶算法可以使用 Redis-Cell 来实现。
5. 令牌算法可以解决 Google 的 guava 包来实现。
6. 借助 Spring Cloud Alibaba Sentinel 组件实现。
需要注意的是借助 Redis 实现的限流方案可用于分布式系统,而 guava 实现的限流只能应用于单机环境。
如果你嫌弃服务器端限流麻烦,甚至可以在不改代码的情况下直接使用容器限流(Nginx 或 Tomcat),但前提是能满足你的业务需求。
负载均衡
DNS 轮询
CDN
IP 负载均衡
硬件负载均衡
F5
主要特性
1. 多链路的负载均衡和冗余
2. 防火墙负载均衡
3. 服务器负载均衡
4. 高可用
5. 安全性
6. 易于管理
7. 其他辅助功能
LVS
VS/NAT
VS/TUN
VS/DR
VS/FULLNAT
软件负载均衡
负载均衡算法
淘汰机制
LRU
LFU
数据一致性
集群主从复制
配置
异常场景
集群方案
缓存 & 数据库
MySQL与Redis缓存实现最终一致性的四种方案
最主流的一种方案就是删除缓存并更新数据库,cache aside。99.9% OK,但还是在极端情况下会出现问题,如何解决?
其实不要去想如何解决,只能说设置一个超时时间,让它这个不一致的时间尽量缩短,当然设置这个时间也要保证它不会雪崩。
真要解决的话,只能加个分布式锁,然后去更新数据库然后再塞缓存,系统的性能肯定是不能容忍的。
真要解决的话,只能加个分布式锁,然后去更新数据库然后再塞缓存,系统的性能肯定是不能容忍的。
上下游系统
分布式事务
高并发/高性能/高可用
缓存
缓存分类
本地缓存
简单场景,不需要考虑缓存一致性、过期时间、清空策略等问题
分布式缓存
对缓存服务做水平扩展,引入缓存集群。数据分片存储在不同机器,采用一致性 Hash 算法,能够保证在缓存集群动态调整时,客户端访问时依然能够根据 key 访问数据。
使用指南
适合缓存的场景
读多写少
计算耗时大,且实时性不高
不适合缓存的场景
写多读少,频繁更新
对数据一致性要求严格
数据访问完全随机
更新策略
Cache-Aside
1. 判断数据是否在缓存
2. 如果不在则从数据源读取
3. 更新缓存
Cache-As-SoR
SoR 即 System Of Record,记录系统,表示数据源,一般就是指数据库
把缓存作为数据源,一切读写操作都是针对 Cache 的,由 Cache 内部自己维护和数据源的一致性。
Read Through
Write Through
直写式,在将数据写入缓存的同时,缓存也去更新后面的数据源,并且必须等到数据源被更新成功后才可返回。
保证了缓存和数据库里的数据一致性。
Write Back
回写式,数据写入缓存即可返回,缓存内部会异步更新数据源。
优点:
1. 写操作特别快,因为只需要更新缓存;
2. 缓存内部可以合并对相同数据项的多次更新。
1. 写操作特别快,因为只需要更新缓存;
2. 缓存内部可以合并对相同数据项的多次更新。
缺点:
数据不一致,可能发生写丢失。
数据不一致,可能发生写丢失。
缓存有效原理
池化
内存池
C/C++中,常使用 malloc、new 等 API 动态申请内存。由于申请的内存块大小不一,如果频繁申请、释放会导致大量的内存碎片,并且这些 API 底层依赖系统调用,会有额外的开销。
内存池就是在使用内存钱,先向系统申请一块空间留做备用,使用者需要内存时向内存池申请,用完后还回来。
内存池就是在使用内存钱,先向系统申请一块空间留做备用,使用者需要内存时向内存池申请,用完后还回来。
如何快速分配内存
降低内存碎片率
维护内存池所需的额外空间尽量少
线程池
线程就是程序执行的实体。在服务端开发领域,我们经常会为每个请求分配一个线程去处理,但是线程的创建销毁、调度都会带来额外的开销,线程太多也会导致系统整体性能下降。在这种场景下,我们通常会提前创建若干个线程,通过线程池来进行管理。当请求到来时,只需从线程池选一个线程去执行处理任务即可。
线程池常常和队列一起使用来实现任务调度,主线程收到请求后将创建对应的任务,然后放到队列里,线程池中的工作线程等待队列里的任务。
线程池常常和队列一起使用来实现任务调度,主线程收到请求后将创建对应的任务,然后放到队列里,线程池中的工作线程等待队列里的任务。
线程池实现上一般有四个核心组成部分
管理器,Manager,用于创建并管理线程池。
工作线程,Worker,执行任务的线程。
任务接口,Task,每个具体的任务必须实现任务接口,工作线程将调用该接口来完成具体的任务。
任务队列,TaskQueue,存放还未执行的任务。
连接池
MySQL 连接池
HTTP 长连接
对象池
消息队列
服务解耦
如果直接发起调用下游服务的方式,如果新增一个下游服务又得改动上游服务,违背依赖倒置原则,即上层服务不应该依赖下层服务。
引入消息队列作为中间层,下游服务订阅上游发布的事件;新增下游服务只需要订阅该事件即可,完全不用改动发布服务,完成系统解耦。
异步处理
有些业务涉及到的处理流程非常多,但是很多步骤并不要求实时性。那么我们就可以通过消息队列异步处理。只要核心功能成功,那么就可以返回结果通知用户了。后续的功能,可以通过消息队列发送给下游服务异步处理。大大提高了系统响应速度。
优点:
1. 服务解耦
2. 提高系统并发,将非核心操作异步处理,不会阻塞住主流程
1. 服务解耦
2. 提高系统并发,将非核心操作异步处理,不会阻塞住主流程
缺点:
1. 降低了数据一致性,从强一致性变为最终一致性
2. 有消息丢失的风险,比如宕机,需要有容灾机制
1. 降低了数据一致性,从强一致性变为最终一致性
2. 有消息丢失的风险,比如宕机,需要有容灾机制
流量控制,削峰填谷
短时间海量请求,一般超过后端服务器的处理能力,那么就可以在接入层将请求放到消息队列里,后端根据自己的处理能力不断从队列里取出请求进行业务处理。
数据库
SQL 调优
索引优化
索引提高查找效率,因为索引一般而言是一个排序列表,排序意味着可以基于二分思想进行查找,将查询时间复杂度做到 O(log(N)),快速支持等值查询和范围查询。
二叉搜索树查询效率无疑是最高的,因为平均来说每次比较都能缩小一般的搜索范围,但是一般在数据库索引的实现上却会选择 B+Tree 而不用二叉搜索树。
二叉搜索树查询效率无疑是最高的,因为平均来说每次比较都能缩小一般的搜索范围,但是一般在数据库索引的实现上却会选择 B+Tree 而不用二叉搜索树。
这涉及到数据库的存储介质了。数据库的数据和索引都是存放在磁盘,并且 InnoDB 引擎是以页为基本单位管理磁盘的,一页一般为 16KB。AVL 或红黑树搜索效率虽然非常高,但是同样数据项,它也会比 B+Tree 更高,高就意味着平均来说会访问更多节点,即磁盘 IO 次数。
所以表面上来看我们使用 B+Tree 没有二叉查找树效率高,但是实际上由于 B+Tree 降低了树高,减少了磁盘 IO 次数,反而大大提升了速度。
没有绝对的快和慢,系统分析要抓主要矛盾,先分析出决定系统瓶颈的到底是什么,然后才是针对瓶颈的优化。
其他问题
主键索引和普通索引
最左前缀匹配原则
索引下推
覆盖索引、联合索引
分库分表
垂直拆分
1. 根据业务关联性强弱,将一个库中的很多表分到不同的数据库。比如订单库、商家库、用户库、支付库……
2. 对一些大表进行垂直分表,将一个表按照字段分成多表,每个表存储其中一部分字段。将常用字段合理创建表,将不常访问的字段单独拆一些表。
水平拆分
由于垂直分库已经按照业务关联切分到了最小粒度,数据量仍然非常大,可以考虑水平分库,比如把订单库分为订单 1 库、订单 2 库……
考虑队逐渐通过哈希算法计算放在哪个库。
分完库,单表数据量依然庞大,查询起来非常慢,也可以参考按日或按月或者按 ID 之类的将订单分表,日表、月表……
带来问题
平时单库单表使用的主键自增特性将作废,因为某个分区库表的主键无法保证全局唯一,这就需要引入全局 UUID 服务了。
读写分离
读多写少,读操作首先成为数据库瓶颈,消除读写锁冲突从而提升数据库整体的读写能力。
采用读写分离的数据库集群方式,一主多从,主库会同步数据到从库。写操作都到主库,读操作都去从库。
读写分离之后就避免了读写锁争用。
读写分离之后就避免了读写锁争用。
排它锁(X 锁):事务 T 对数据 A 加上 X 锁时,只允许事务 T 读取和修改数据 A。
共享锁(S 锁):事务 T 对数据 A 加上 S 锁时,其他事务只能再对数据 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。
带来问题:主库和从库数据不一致
MySQL 主从同步依赖于 binlog,一种二进制日志,独立于具体的存储引擎。它主要存储对数据库更新的 SQL 语句,由于记录了完整的 SQL 更新信息,所以 binlog 是可以用来数据恢复和主从同步复制的。
从库从主库拉取 binlog 然后依次执行其中的 SQL 即可达到复制主库的目的,由于从库拉取 binlog 存在网络延迟等,所以主从数据存在延迟同步问题。那么这里就要看业务是否允许短时间内的数据不一致,如果不能容忍,那么可以通过如果读从库没有获取到数据就去主库读一次来解决。
IO/CPU
零拷贝
高性能的服务期应当避免不必要的数据复制,特别是在用户空间和内核空间之间的数据复制。
Linux IO,传统方式:磁盘 -DMA-> 页缓存(内核空间)-CPU拷贝-> 应用缓存(用户空间)-CPU 拷贝-> socket 缓存 -DMA-> 网卡
DMA:数据传送在 IO 设备与内存之间直接进行的
内核空间和用户空间之间数据拷贝需要 CPU 亲自完成,但是对于这类“数据不需要在用户空间进行处理”的程序来说,两次拷贝显然是浪费。如果能够直接将数据在内核缓存之间移动,那么除了减少拷贝次数以外,还能避免内核态和用户态之间的上下文切换。
而这正是零拷贝(Zero Copy)做的事情。利用各种零拷贝技术,减少不必要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,专注于别的服务。
而这正是零拷贝(Zero Copy)做的事情。利用各种零拷贝技术,减少不必要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,专注于别的服务。
mmap
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
sendfile
Linux2.1 版本提供,数据不经过用户态,直接从页缓存拷贝到 socket 缓存,同时由于和用户态完全无关,就减少了一次上下文切换。Linux2.4 版本进行了优化,直接通过 DMA 将磁盘文件数据读取到 socket 缓存,真正实现了零拷贝。
(mmap 和 2.1sendfile 只是消除了用户空间和内核空间之间的拷贝,而页缓存和 socket 缓存之间的拷贝依然存在)
(mmap 和 2.1sendfile 只是消除了用户空间和内核空间之间的拷贝,而页缓存和 socket 缓存之间的拷贝依然存在)
IO 复用
高效序列化
无锁编程
CAS
多线程
缓存友好
批量处理
请求合并
在涉及到网络连接、IO 等情况时,将操作批量进行处理能够有效提高系统的传输速率和吞吐量。
在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。
在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。
比如后台 RPC 框架,经常有更新数据的需求,而有的数据更新的接口往往只接受一项,这个时候我们往往会优化下更新接口,使其能够接受批量更新的请求,这样可以将批量的数据一次性发送,大大缩短网络 RPC 调用耗时。
预处理与延后处理
预先处理
例如预先将一些调用外部的名单存到缓存中。
通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大地提高了系统的吞吐量。
预处理在 CPU 和操作系统中也广泛使用。
CPU 基于历史访存信息,将内存中的指令和数据预读到 Cache 中,大大提高 Cache 命中率。
Linux 文件系统中,预读算法会预测即将访问的 page,然后批量加载比当前读更多的数据缓存在 page cache 中,下次读请求到来时可以直接从 cache 中返回,大大减少了访问磁盘的时间。
延迟处理
比如支付宝集五福,“将存入余额,稍后到账”
系统间调用,比如转账,几个动作必须一起成功或者一起都不成功,不能只成功一般,这是保证数据一致性。
保证两个操作同时成功或者失败就需要用到事务。
如果去实时的做到账,那么大概率数据库 TPS 会是瓶颈。
COW,Copy On Write,写时复制。
Linux 创建进程的系统调用 fork,fork 产生的子进程只会创建虚拟地址空间,而不会分配真正的物理内存,子进程共享父进程的屋里空间,只有当某个进程需要写入的时候,才会真正分配物理页,拷贝物理页,通过 COW 减少了很多不必要的数据拷贝。
时间换空间/空间换时间
同步变异步
对于处理耗时的任务,如果采用同步的方式,那么会增加任务耗时,降低系统并发度。
可以通过将同步任务变为异步进行优化。
可以通过将同步任务变为异步进行优化。
RPC 调用方式
高并发解决思路
缓存
使用缓存来提高系统性能,好比“拓宽河道”的方式抵抗高并发大流量的冲击,而且缓存的各种应用模式在 CPU Cache 中也有非常经典的案例,把体系结构学好了,缓存这些只是新瓶装旧酒。
例如拆红包,明明点开的时候显示还有,点击“拆”却显示已经被抢完,就是缓存数据和数据库里的不一致导致的。但这里我们的体验是允许这样的不一致的。
异步
某些场景下,我们不一定要实时流程中做完所有步骤,在做完一些必备的步骤后就可以返回了,剩下的丢到一个 MQ 或者发个事务消息,异步去完成。
例如支付宝每年分五福就是,转账是异步去弄的。
横向扩展(Scale-out)
这个最简单粗暴,也是最有效的方式,请求量大了,横向拓展机器就好了。当然需要写的程序能够支持分布式、横向扩展。
提升性能汇总
使用反向代理服务器让应用更快更安全
增加负载均衡服务器
缓存静态及动态内容
压缩数据
优化 SSL/TLS
实现 HTTP/2 或 SPDY
升级软件
调优 Linux
调优 Web 服务器
监控实时动态以发现问题和瓶颈
架构师实战
并发任务执行框架
架构师是什么
主要职责
4. 技术权威
5. 核心、关键或难点任务的开发
6. 开发管理
7. 沟通协调
架构师的方方面面
作用
负责系统架构设计,同时也要负责架构的实施落地、演化发展、推广重构。
架构师对某一领域有较深刻的认识,有时候甚至是坚定的技术信仰,乐于同他人分享自己的知识,希望能够推广自己的技术主张。
架构师对某一领域有较深刻的认识,有时候甚至是坚定的技术信仰,乐于同他人分享自己的知识,希望能够推广自己的技术主张。
效果
不管项目有多么艰难复杂,只要有优秀的架构师,大家就会坚信,项目一定能顺利完成。优秀的架构师带给项目组的,不只是技术和方法,更重要的是必胜的信念。这种信念是架构师自己积累起来的气场和影响力。
架构师通常会开发项目中最具技术难度和挑战性的模块,从而为整个项目的顺利进行铺平道路。这些模块包括基础框架、公共组件、通用服务等平台类产品。在大型互联网应用中,基础服务承担着海量的数据存储和核心业务处理服务,有许多挑战性的工作。所以我们的实战就是实现一个基础框架和对一个项目进行性能优化。
实现一个基础框架
需求的产生和分析
需要做什么
具体实现
流程图
测试
性能优化实战
项目背景和问题
分析和改进
继续优化
数据结构的优化
线程数的设置
缓存的优化
优化后的效果
用户体验的优化
启发
通用能力
CAP 理论
C-Consistency,一致性。
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意节点读取到的数据都是最新的状态。
举个栗子,读写分离数据库,商品服务写入主库,从读库查询。
商品信息的读写要满足一致性就是要实现如下目标:
1. 商品服务写入主数据库成功,则向从数据库查询新数据也成功;
2. 商品服务写入主数据库失败,则向从数据库查询新数据也失败。
商品信息的读写要满足一致性就是要实现如下目标:
1. 商品服务写入主数据库成功,则向从数据库查询新数据也成功;
2. 商品服务写入主数据库失败,则向从数据库查询新数据也失败。
如何实现一致性?
1. 写入主数据库后要将数据同步到从数据库。
2. 写入主数据库后,在想从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。
1. 写入主数据库后要将数据同步到从数据库。
2. 写入主数据库后,在想从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。
分布式系统一致性的特点?
1. 由于存在数据同步的过程,写操作的响应会有一定的延迟(因为要等待数据同步完成)。
2. 为了保证数据一致性,会对资源暂时锁定,待数据同步完成释放锁定资源。——影响可用性
3. 如果请求数据同步失败的节点,则会返回错误信息,一定不会返回旧数据。
1. 由于存在数据同步的过程,写操作的响应会有一定的延迟(因为要等待数据同步完成)。
2. 为了保证数据一致性,会对资源暂时锁定,待数据同步完成释放锁定资源。——影响可用性
3. 如果请求数据同步失败的节点,则会返回错误信息,一定不会返回旧数据。
A-Availability,可用性。
可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。
商品信息读取满足可用性就是要实现如下目标:
1. 从数据库接收到数据查询的请求则立即能够响应数据查询结果;
2. 从数据库不允许出现响应超时或响应错误。
1. 从数据库接收到数据查询的请求则立即能够响应数据查询结果;
2. 从数据库不允许出现响应超时或响应错误。
如何实现可用性?
1. 写入主数据库后要将数据同步到从数据库。
2. 由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
3. 即使数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。
1. 写入主数据库后要将数据同步到从数据库。
2. 由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
3. 即使数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。
分布式系统可用性的特点?
1. 所有请求都有响应,且不会出现响应超时或响应错误。
1. 所有请求都有响应,且不会出现响应超时或响应错误。
P-Partition tolerance,分区容错性。
通常分布式系统的各个节点部署在不同的子网,这就是网络分区,不可避免地会出现由于网络问题而导致节点之间通信失败,此时仍可对外提供服务,这就是分区容错性。
商品信息读写满足分区容错性就是要实现如下目标:
1. 主数据库向从数据库同步数据失败不影响读写操作;
2. 其一个节点挂掉不影响另一个节点对外提供服务。
1. 主数据库向从数据库同步数据失败不影响读写操作;
2. 其一个节点挂掉不影响另一个节点对外提供服务。
如何实现分区容忍性?
1. 尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据库,这样节点之间能有效实现松耦合。
2. 添加从数据库节点,其中一个从节点挂掉,其他从节点提供服务。
1. 尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据库,这样节点之间能有效实现松耦合。
2. 添加从数据库节点,其中一个从节点挂掉,其他从节点提供服务。
分布式系统分区容错性的特点?
1. 分区容错性是分布式系统具备的基本能力,必选。要么 AP 要么 CP。
1. 分区容错性是分布式系统具备的基本能力,必选。要么 AP 要么 CP。
在所有分布式场景中不会同时具备 CAP 三个特性,因为在具备了 P 的前提下 C 和 A 是不能共存的。
AP
通常 AP 都会保证最终一致性,BASE 理论就是根据 AP 来扩展的。因为对于大多数大型互联网应用的场景,节点众多、部署分散,集群规模越来越到,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9(99.999...%),并要达到良好的响应性能,用户体验要首先保障。服务不可用是最不可接受的。舍弃数据强一致性,保证最终一致即可。
CP
BASE 理论
BA:基本可用,Basically Available
鼓励通过架构设计,把可能影响全平台的严重问题,转化为只影响平台中的一部分数据或功能的非严重问题。
S:软状态,Soft State
允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
E:最终一致性,Eventually Consistent
指数据在多个副本之间能否保持一致的特性,也是分布式事务要解决的终极问题。
分类
因果一致性(Causal Consistency)
读己之所写(Read your Writes)
会话一致性(Session Consistency)
单调读一致性(Monotonic Read Consistency)
单调写一致性(Monotonic Write Consistency)
接口耗时 RT 长,慢接口排查与优化
MySQL 慢查询优化
频繁 GC
OOM
死锁
CPU 100%
新版的一个总结
传统方案
1. top order by with P: 1040 // 首先按进程负载排序找到 axLoad(pid)
2. top -Hp 进程 PID:1073 // 找到相关负载 线程 PID
3. printf "0x%x\n"线程 PID:0x431 // 将线程 PID 转换为 16 进制,为后面查找 jstack 日志做准备
4. jstack 进程 PID | vim +/十六进制线程 PID - // 例如:jstack 1040 | vim +/0x431 -
架构设计
SOA 架构和微服务架构的区别?
排查问题工具汇总
设计题
群聊消息的已读未读
服务端如何防止重复支付
12306数据设计、功能实现、抢票场景架构设计
微信抢红包
悲观锁
乐观锁
存储过程放在MySQL数据库中
微信红包架构设计
淘宝红包雨
如果一个外卖配送单子要发布,现在有200个骑手都想要接这一单,如何保证只有一个骑手接到单子?
美团首页每天会从10000个商家里面推荐50个商家置顶,每个商家有一个权值,你如何来推荐?第二天怎么更新推荐的商家?
1000个任务,分给10个人做,你怎么分配?先在纸上写个最简单的版本,然后优化。
全局队列,把1000个任务放在一个队列里面,然后每个人都提取,完成任务。
分10个队列,每个人分别到自己对应的队列中去取任务。
保证发送消息的有序性,消息处理的有序性。
如何把一个文件快速下发到100w个服务器
树状:
1. 每个服务器既具有文件存储能力也应具有文件分发能力。
2. 每个服务器接收到文件之后向较近的服务器分发,具体类似多叉树,应该挺快的。
A.对于树状传递,在100W台服务器这种量级上,可能存在两个问题
1.如果树上的某一个节点坏掉了,那么从这个节点往下的所有服务器全部宕机。
2.如果树中的某条路径,传递时间太长了(网络中,两个节点间的传递速度受很多因素的影响,可能相差成百上千倍),使得传递效率退化。
1.如果树上的某一个节点坏掉了,那么从这个节点往下的所有服务器全部宕机。
2.如果树中的某条路径,传递时间太长了(网络中,两个节点间的传递速度受很多因素的影响,可能相差成百上千倍),使得传递效率退化。
改进:
100W台服务器相当于有100W个节点的连通图。那么我们可以在图里生成多颗不同的生成树,在进行数据下发时,同时按照多颗不同的树去传递数据。这样就可以避免某个中间节点宕机,影响到后续的节点。同时这种传递方法实际上是一种依据时间的广度优先遍历,可以避免某条路径过长造成的效率低下。
100W台服务器相当于有100W个节点的连通图。那么我们可以在图里生成多颗不同的生成树,在进行数据下发时,同时按照多颗不同的树去传递数据。这样就可以避免某个中间节点宕机,影响到后续的节点。同时这种传递方法实际上是一种依据时间的广度优先遍历,可以避免某条路径过长造成的效率低下。
索引状:
1. 设置1000个缓存服务器,文件先下发到这些缓存上。(具体多少缓存、分几层缓存和具体业务有关。)
2. 每个缓存服务器接收1000个服务器取文件。
B.最简洁省事的方法,组播!(类似索引式)。都有100W台服务器了,自己搞个组播网络不就好了,标准的TCP/IP协议啊。
给每个组分配不同的IP段,怎么设计一种结构使得快速得知IP是哪个组的?
10亿个数,找出最大的10个。
建议:一个大小为10的小根堆。
有几台机器存储着几亿淘宝搜索日志,你只有一台2G的电脑,怎么选出搜索热度最高的十个搜索关键词?
分布式集群中如何保证线程安全?
给个淘宝场景,怎么设计一个消息队列?
10万个数,输出从小到大?
先划分成多个小文件,送进内存排序,然后再采用多路归并排序。
有10万个单词,找出重复次数最高的十个?
阿里本地生活营销平台,权益规则配置、发放/核销/回退,对接本地生活全部场景和业务入口,提供产品化的权益能力。每秒几百 k 的 QPS,一天几亿张券,数据一致性、稳定性、性能都要保障。
卡券平台
积分平台
权益中心
微信扫码登录如何实现?
1. 微信随机生成一个字符串,在它服务器这边
2. 扫码,手机肯定有带 token、cookie
3. 把 token 等跟二维码字符串传输到微信服务器上面,做一个绑定
4. 绑定之后,客户端可以建立长连接,推送过来,或者这边做轮询,页面就能实现自动跳转的效果。
2. 扫码,手机肯定有带 token、cookie
3. 把 token 等跟二维码字符串传输到微信服务器上面,做一个绑定
4. 绑定之后,客户端可以建立长连接,推送过来,或者这边做轮询,页面就能实现自动跳转的效果。
0 条评论
下一页