1 前言
同样在实现业务过程中遇到这么一个场景,我需要将视频上传到OSS(对象存储中),同时我也要将对象的连接存储到我的数据当中,这里涉及到了对象存储和数据库存储两个系统,现在可能会遇到上传视频失败,或者遇到数据存储失败。现在要保证两个系统的数据保证一致性,也就是说上传视频和存储数据两个操作同时成功,要么就同时失败,也就是所谓的分布式事务,要解决这么一个问题,所以有了这一篇文章,介绍分布式事务应该如何实现。
本文会结合此次的场景具体实现,也会介绍一些常见的解决分布式事务的方法。
2 事务
说起事务,就不得不提起MySQL数据库的事务,也就是我们平时遇到的最常见的事务 。事务是访问并可能更新数据库中各项数据项的一个执行单元, 事务是由事务开始和事务结束之间执行的全体操作组成的。简单来说,事务当中可能包含多个操作,但是这些操作要么全部同时成功,要么全部同时失败。
事务结束有两种情况:①事务中的全部操作都执行成功,提交事务 ② 事务中有操作失败,那么将会发生回滚操作,并且撤销之前的所有操作。
事务具有四个特征(ACID):原子性、一致性、隔离性和持久性
- 原子性(Atomicity):一个事务的所有操作要么全部完成,要么全部不完。
- 一致性(Consistency):操作前后,数据库保证一致性状态。
- 隔离性(Isolation):数据库允许多个事务并发的执行,事务和事务之间相互隔离,互不影响。
- 持久性(Durability):事务结束后,对数据的修改是永久的。
3 分布式事务
分布式事务是指事务得参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部操作成功,要么全部失败。简单来说,就是将数据分散存储在分布式系统中。
提到分布式事务,就说一下CAP原子(在课堂上遇到过,只记得了服务无法满足三个属性):
- 一致性(Consistency):更新操作返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。分布式环境中,一致性是指多个副本之间能否保持一致的特性。
- 可用性(Availability):系统提供的服务必须处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
- 分区容错行(Partition tolerance):分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。
为什么CAP原理在分布式系统中只能三选二呢?
在一个分布式系统中,分区故障是无法避免的。如果不考虑分区容忍性,一旦发生了故障。整个系统就无法使用了,这不符合实际的开发需要。所有所谓的三选二,其实P已经是必选的了,只能在AP,CP之间选择。 为了满足数据的一致性,到有写请求的时候,数据需要同步到全部的节点,请求必须等到所有的节点数据都更新一致后,才能响应,再次之前所有的请求都会阻塞。就很可能导致响应超时,破坏了可用性。 为了提高可用性,系统部署节点越多,但是又要保证响应的时间,所有就无法达到高一致性。
下面将介绍几种常见的分布式事务保证一致性的方法
4 异步回滚策略
4.1 简介
异步任务回滚是一种处理在执行某个主要任务过程中发生错误时,通过启动异步任务来执行回滚操作的机制。通常,在复杂的业务逻辑中,一个主要任务可能包含多个步骤,涉及多个资源或数据的修改。如果在主要任务的某个步骤出现错误,为了保持数据的一致性和完整性,我们可能需要撤销之前已经执行的操作,即执行回滚操作。
异步任务回滚的主要思想是将回滚操作封装成一个异步任务,当主要任务发生错误时,启动这个异步任务来执行回滚操作。这样可以确保回滚操作在发生错误后能够被执行,从而保持数据的一致性和完整性。
4.2 具体实现
func storeVideoLinkToDB(videoLink string) error {
// 模拟链接存储到数据库
if err := insertLinkToDatabase(videoLink); err != nil {
// 链接存储失败,创建异步回滚任务
go func() {
if err := deleteUploadedVideo(videoLink); err != nil {
// 处理删除视频失败的情况
}
}()
return err
}
return nil
}
func deleteUploadedVideo(videoLink string) error {
// 模拟删除已上传的视频
if err := deleteFromOSS(videoLink); err != nil {
// 处理删除失败的情况
return err
}
return nil
}
如果其中一个操作失败了,就会回滚到上一个事务中去,我在实际的实现中也是采用这样方法。我利用两个协程,一个存储数据库,另外一个完成视频上传,如果其中一个任务失败了,就会回滚另外一个任务,这样来达到数据的一致性:
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
// 上传视频
}()
go func() {
defer wg.Done()
// 保存到数据库
}()
wg.Wait()
// 异步回滚
if updataErr != nil || creatErr != nil {
go func() {
// 存入数据库失败,删除上传
if creatErr != nil {
}
// 上传失败,删除数据库
if updataErr != nil {
}
}()
}
4.3 补偿事务
在查阅资料的过程中,看到了补偿事务这一说话,感觉两者的思路是一样的,在这里将两者进行区分一下,以免混淆,简单总结一下,异步回滚就是补偿事务这种思想下的一种具体实现:
补偿事务: 补偿事务是一种设计模式,用于处理分布式事务失败的情况。当事务的某个步骤失败时,可以通过执行一系列的补偿操作来保持数据的一致性。这些补偿操作不一定要与原始操作完全相反,而是根据业务需求进行设计,以确保数据状态的一致性。
总的来说,异步回滚和补偿事务都是处理分布式事务失败的方法,但补偿事务更侧重于设计模式和业务逻辑,而异步回滚更强调在后台异步执行回滚操作以提高性能。
5 两阶段提交
5.1 简介
2PC 是一种协议,确保在多个参与者之间执行的事务要么全部提交,要么全部回滚。它涉及预提交、提交和回滚三个阶段,协调者与参与者之间进行交互,但因为其同步阻塞特性,可能导致性能问题和单点故障。
两阶段分为准备阶段和提交阶段,流程如下:
- 协调者通知所有的参与者,然后等待参与者的回执
- 参与者收到通知开始执行操作,参与者给协调者返回响应,参与事务操作返回"同意",否者返回“中止”
- 当收到所有参与者都是“同意”进入提交阶段,协调者想参与者发送“提交请求”
- 参与者完成提交,释放占用的资源,给协调者发送“完成”消息
- 协调者收到所有参与者返回的“完成”消息后,事务完成
- 协调者收到“中止”,则会发生回滚操作
5.2 具体实现
type Participant interface {
Prepare() error
Commit() error
Abort() error
}
type DatabaseParticipant struct{}
func (dp *DatabaseParticipant) Prepare() error {
fmt.Println("Database: Prepare")
// 执行数据库的预提交操作
return nil
}
func (dp *DatabaseParticipant) Commit() error {
fmt.Println("Database: Commit")
// 执行数据库的提交操作
return nil
}
func (dp *DatabaseParticipant) Abort() error {
fmt.Println("Database: Abort")
// 执行数据库的回滚操作
return nil
}
type OSSParticipant struct{}
func (op *OSSParticipant) Prepare() error {
fmt.Println("OSS: Prepare")
// 执行视频上传的预提交操作
return nil
}
func (op *OSSParticipant) Commit() error {
fmt.Println("OSS: Commit")
// 执行视频上传的提交操作
return nil
}
func (op *OSSParticipant) Abort() error {
fmt.Println("OSS: Abort")
// 执行视频上传的回滚操作
return nil
}
func main() {
database := &DatabaseParticipant{}
oss := &OSSParticipant{}
// 预提交阶段
if err := preparePhase([]Participant{database, oss}); err != nil {
log.Fatal("Prepare phase failed:", err)
}
// 提交阶段
if err := commitPhase([]Participant{database, oss}); err != nil {
log.Fatal("Commit phase failed:", err)
} else {
fmt.Println("Transaction committed successfully")
}
}
func preparePhase(participants []Participant) error {
fmt.Println("=== Prepare Phase ===")
for _, participant := range participants {
if err := participant.Prepare(); err != nil {
abortPhase(participants)
return err
}
}
return nil
}
func commitPhase(participants []Participant) error {
fmt.Println("=== Commit Phase ===")
for _, participant := range participants {
if err := participant.Commit(); err != nil {
abortPhase(participants)
return err
}
}
return nil
}
func abortPhase(participants []Participant) {
fmt.Println("=== Abort Phase ===")
for _, participant := range participants {
participant.Abort()
}
}
6 其余方法
6.1 消息中间件
使用消息中间件(如Kafka、RabbitMQ)来发布和订阅事件,通过事件驱动模型实现分布式事务的一致性。事件发布者将事务的操作作为事件发布,订阅者执行相应的操作。
6.2 三阶段提交
3PC 是对2PC的改进,增加了超时机制,降低了阻塞风险。它包括CanCommit、PreCommit和DoCommit三个阶段,每个阶段都有超时机制,但仍然需要解决某些不确定性情况。
三个阶段是指:
询问阶段:询问参与者是否可以执行事务操作
预提交阶段:除了没有提交事务,其它事务操作都执行了
提交阶段:提交事务或者回滚事务
6.3 其它
除了上面介绍的方法还可以利用第三方组件,其中还有很多知识在里面,比如柔性事务、刚性事务,刚刚提到的利用消息中间件和事务补偿都是柔性事务,而二阶段提交和三阶段提交属于刚性事务。还有TTC模型等等,也有很多的知识点,先只实现了两种