随着数据规模不断上涨,数据操作的性能越来越低,为了提升性能,在数据库层面,通常通过分库分表提升性能,提升吞吐量,但也引发出一些新的问题,如分布式查询,分布式事务等。本文就分布式事务,对目前市面上主流的解决方案进行解读,让大家更好地了解、使用好分布式事务。
问题是如何出现的
分库分表前
同一个数据库,同一个数据库连接,依赖数据库实现事务功能。
db.connection{
begin
sql 1
sql 2
...
commit or rollback
}
// 事务实现了
db.connection()
分库分表后
不同数据库,不同数据库连接,如何实现事务功能?
db1.connection{
begin
sql 1
sql 2
...
commit or rollback
}
db2.connection{
begin
sql 1
sql 2
...
commit or rollback
}
// 这时1、2作为一个整体事务,没有实现最终一致
db1.connection() && db2.connection()
解决方案
2PC(Two-Phase Commit)
概念:事务实际分为两部分
第一部分 prepare 预执行
begin
sql 1
sql 2
...
第二部分 commit or rollback 确认
commit
在网络正常,数据库正常的情况下,过了第一部分的数据操作,肯定能commit成功(原理可以了解数据库事务和锁)
rollback
第一部分出现问题(比如数据逻辑问题),取消事务的提交
解决方法伪代码
执行代码{
//一阶段
aStatus=参与者A.prepare()
bStatus=参与者B.prepare()
//二阶段
if aStatus and bStatus:
参与者A.commit()
参与者B.commit()
else:
参与者A.rollback()
参与者B.rollback()
}
存在问题
同步阻塞:执行过程中,所有参与者都是阻塞的。
单点故障: 代码(也叫协调者)在执行完之前发生故障,尤其是刚完成一阶段进入二阶段,那么所有参与者都在阻塞状态。
数据不一致:二阶段由于网络终端等原因,部分参与者执行commit,部分参与者没有执行commit,数据最终不一致
遗留问题:代码执行到 参与者A.commit() 后宕机,同时参与者A也宕机,这时候没人知道事务是否已经成功(发送成功,或者执行成功)
适用场景
3PC(Two-Phase Commit)
原理
减少因网络等异常造成的长时间阻塞
- preapre前确认各参与方是否可执行事务;
- 引入超时机制,最多等待N秒,然后直接commit或回滚。
执行代码{
//一阶段
参与者A.ping()
参与者B.ping()
//一阶段 --> 二阶段
aStatus=参与者A.prepare().timeout(seconds).returnFalse()
bStatus=参与者B.prepare().timeout(seconds).returnFalse()
//二阶段 --> 三阶段
if aStatus and bStatus:
参与者A.commit().timeout(seconds).returnFalse()
参与者B.commit().timeout(seconds).returnFalse()
else:
参与者A.rollback().timeout(seconds).returnFalse()
参与者B.rollback().timeout(seconds).returnFalse()
}
改进地方
1.在prepare前,增加一个阶段,检查网络等的资源是否可用
2.prepare阶段,增加超时机制
是的,数据不一致的问题,依旧没有解决,只是缓解了阻塞和单点问题
TCC(try,commit,cancel)
原理
把事务逻辑从sql,提取出来,自行编码实现
执行代码{
//try阶段,类似信用卡预授权,先冻结额度,未实际扣除
aStatus=参与者A.冻结资源().commit()
bStatus=参与者B.冻结资源().commit()
//Confirm阶段,实际扣除(并解冻)
if aStatus and bStatus:
(参与者A.扣除资源().commit()).异步执行()
(参与者B.扣除资源().commit()).异步执行()
else:
//Cancel阶段,取消冻结
(参与者A.解冻资源().commit()).异步执行()
(参与者B.解冻资源().commit()).异步执行()
}
改进地方
1.消除由于多个数据库事务造成长时间阻塞问题,大大提高了吞吐量
2.可以比较方便实现最终数据一致性
存在问题:所有参与者都需要实现 冻结、扣除、解冻 三个逻辑。且扣除和解冻者两个操作,需要实现幂等。代码实现成本比较高。
QA
- 为什么try阶段有commit?
单个参与者资源的冻结(可能有局部事务),不需要分布式事务(全局)
- 为什么confirm阶段要用thread? 而try阶段没有
理论上,只要通过try阶段(冻结),confirm阶段肯定能执行(除非数据库,网络,账号被封等异常),因此可并行执行。try阶段只能串行,因为要确认所有资源均足够执行这次分布式事务。
基于可靠消息(mq版)
原理
- 用mq消息异步传递子事务状态,最终达到全局事务的完成。
- 特点:由发起方决定是否回滚,也就是说只要发起者成功,后续的子事务基本都能成功。比如刷卡后,增加消费积分。
执行(事务主题, transId, 参与者){
name = 参与者.名称()
mq.setMsg(事务主题, transId, name, 'ping')
mStatus = 参与者.prepare();
mq.setMsg(事务主题, transId, name, 'prepare')
if mStatus:
excuted = 数据库是否提交(transId, name)
if not excuted:
参与者.commit()
mq.setMsg(事务主题, transId, name, 'commit')
else:
参与者.rollback()
mq.setMsg(事务主题, transId, name, 'rollback')
}
发起者A{
transId = getTransactionId()
执行(事务主题X, transId, 参与者A)
name = 参与者A.名称()
mq.订阅事件(事务主题X, name).触发函数(e){
transId = e.transacationId;
定时任务(事务主题X, transId, name)
if e.message == 'commit':
//启动下一个兄弟事务
mq.setMsg(事务主题X, transId, 参与者B名称, "wake up and work")
else if e.message == 'rollback':
mq.delMsg(事务主题X, transId)
}
}
参与者B{
name = 参与者B.名称()
mq.订阅事件(事务主题X, name).触发函数(e){
transId = e.transacationId;
定时任务(事务主题X, tranId,name)
if e.message == 'wake up and work':
执行(事务主题X, transId, 参与者B)
else if e.message == 'commit':
mq.delMsg(事务主题X, transId)
else if e.message == 'rollback':
//人工处理,因为一般都能执行成功
}
}
QA
- 数据库操作,发送mq信息,不能确保同时(不)发生,如何保证事务一致性?
为确保事务能正常工作,需对每个参与者的事务状态,进行超时检测,并作出处理(俗称捞起)。
比如:实际进行到了prepare阶段,但事务状态在ping阶段,需回补一条prepare消息。
比如:commit后,消息发送失败,消息状态仍为prepare,此时需确认事务是否已commit,来决定只发送commit消息,或者重新执行commit并发送commit消息。
最后为了达到幂等,还需要对每个数据库的commit,进行是否commit的确认。
数据库是否提交(transId,name){
}
定时任务{
name, status = mq.get(事务主题X, transId)
if status == 'prepare':
excuted = 数据库是否提交(transId,name)
if excuted:
//启动下一个兄弟事务
mq.setMsg(事务主题, transId, name, 'commit')
else:
//执行
参与者实例 = 实例化(name)
参与者实例.commit()
mq.setMsg(事务主题, transId, name, 'commit')
}
SAGA
原理
(消息)异步 + TCC(部分)
执行代码{
总事务开始()
new Thread(
aStatus = 参与者A.冻结资源().commit()
if aStatus:
参与者A.扣除资源().commit()
else
参与者A.解冻资源().commit()
).start()
new Thread(
bStatus = 参与者A.冻结资源().commit()
if bStatus:
参与者B.扣除资源().commit()
else:
参与者B.解冻资源().commit()
).start()
}
自行实现的定时任务{
if 全部参与者已完成事务:
if 全部扣除commit成功:
总事务成功()
else
已扣除资源退还()
总事务失败()
}
额外- 阿里Seata
原理
- 一个事务由多个子事务组成,每个子事务直接commit,不需要等待其它兄弟事务确认prepare或者commit
- 有一个兄弟事务rollback,则全部兄弟事务rollback。
- 框架实现 commit后rollback,不需要业务方编码!!实现机制可以到官方查看
执行代码{
框架开始监听该事务()
参与者A.扣除资源().commit()
参与者B.扣除资源().commit()
}
框架自动实现的回滚操作{
if 兄弟事务发生rollback:
全部兄弟rollback()
}
QA
- 这个框架这么牛EN,其它方式还有存在的必要么?
1.业务场景不一定全部适用
2.需控制全部参与者的数据源,旧系统迁移成本很高
参考文档:
https://mp.weixin.qq.com/s/T-Q9eouj4unrWh8Q9bJoOA
https://www.cnblogs.com/jajian/p/10014145.html
网友评论