分布式事物
本地事务概念
- ACID:
- Atomicity(原子性)
- Consistency(一致性)
- Isolation(隔离性) ----消除幻读
- Durability(持久性)
分布式理论
当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里所说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫CAP定理,那么CAP定理指的是什么呢?
CAP定理
CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:
一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
可用性(Availability) : 每个操作都必须以可预期的响应结束
分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
分布式事务概念
- 难点
- 高度并发
- 资源分布
- 大时间跨度
分布式事务解决方法
1.两阶段提交(2PC)
XA规范下的2pc事务
@Transactional
public String doOrder(String busId,String idcard) {
System.out.println("begin.....");
String sql = "UPDATE tcc_fly_order SET bus_id=?,STATUS = 1,idcard=?,remark=? WHERE STATUS = 0 LIMIT 1";
//锁定订单
int ret = ghJdbcTemplate.update(sql,busId,idcard,"国航");
if (ret != 1){
throw new RuntimeException("国航下单失败,无票可订");
}
//锁定订单
ret = nhJdbcTemplate.update(sql,busId,idcard,"南航");
if (ret != 1){
throw new RuntimeException("南航下单失败,无票可订");
}
System.out.println("end.....");
return "success";
}
只要doOrder方法里抛出了RuntimeException,则两个数据库里的sql结果,都会被回滚掉
原理:先操作服务1,再才做服务2,都成功则全部提交,否则全部回滚,所使用的是两阶段提交方法
两阶段提交原理图
两阶段提交协议(Two-phase commit protocol)是XA用于在全局事务中协调多个资源的机制。
TM和RM间采取两阶段提交(Two-phase Commit)的方案来解决一致性问题。
两阶段提交需要一个协调者(TM)来掌控所有参与者节点(RM)的操作结果并且指引这些节点是否需要最终提交。
2PC事务解决方案的利弊
- 优点:
- 严格的ACID
- 严格的ACID
- 缺点:
- 效率非常低(微服务架构下已不太适用)
- 全局事务方式下,全局事务管理器(TM)通过XA接口使用二阶段提交协议(2PC)与资源层(如数据库)进行交互。使用全局事务,同时锁定两个数据库的数据,直到全局事务结束,事务锁定时间大大延长。
- 与本地事务相比,XA协议的系统开销相当大,因而应当慎重考虑是否确实需要分布式事务,只有支持XA协议的资源才能参与分布式事务
2.补偿事务(TCC)
base理论/柔性事务 ----- 为了可用性,牺牲一致性
- base理论的本质,是不再遵守ACID标准,不做强一致性
- base理论的S,即指事务执行的中间状态(commit之前的状态),可以不隔离锁定,允许对外界表现出不一致
-----------一个事务执行需要时间 ------- a步骤—》b步骤 -------- 20毫秒 ---- 中间状态 - base理论的E,即指保证最终所有的步骤都得到执行,达到最终数据是一致的
整个base理论的过程,如下:
事务开始前 ------ 数据是一致的
事务进行中 ------ 数据是不一致的 ---- 中间态不一致 ---- 放宽标准
事务结束后 ------ 数据又一致
- 对比ACID事务,base事务就是砍掉其中的数据锁定状态
----------------为了可用性 — 数据锁定的状态— 不可用状态 - 业界对事务分类标准:
ACID标准,强一致性要求的事务标准 ----- 刚性事务
牺牲一致,放弃ACID里的强一致要求-------柔性事务:
tcc补偿性事务流程图
tcc补偿性事务示例讲解
准备工作
如果你要玩儿 TCC 分布式事务,必须引入一款 TCC 分布式事务框架,比如国内开源的 ByteTCC、Himly、TCC-transaction。
否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了
tcc补偿性事务流程分析
- 一个普通业务接口-----切成三个接口:try–confirm–cancel
- try ------- 将原业务接口,涉及到的资源锁定/或者做预备 ------ 通过一个冻结状态,做排它处理
- confirm ------ 正式调用原业务接口方法,如:insert一条数据1 ----- 因try阶段已经为此语句准备好了执行条件,不会再出现业务的冲突情况(业务一定是可行的)
- cancel -------- 恢复资源状态,取消资源的冻结状态
这里以在南航和国航个买往返票为例
-
第一阶段:try
- 锁定资源 改一下资源状态
案例中为 锁定南航,国航各一张票的资源,更具需求在数据表中加一个frozen字段
- 锁定资源 改一下资源状态
//try方法,busId:生成的唯一订单号,idcard:身份证号
public int doOrder(String busId,String idcard) {
System.out.println("frozen order:"+busId);
//幂等性要求,如果订单号已存在,则直接返回,保证只锁定1条数据
Integer count = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM tcc_fly_order t WHERE t.bus_id = ?",Integer.class,busId);
if (count > 0){
return 1;
}
//锁定国航订单 设置1条数据frozen = 1,订单号为当前订单号
int ret = jdbcTemplate.update(
"UPDATE tcc_fly_order SET frozen = 1,bus_id=? WHERE STATUS = 0 AND frozen=0 LIMIT 1",busId);
if (ret != 1){
throw new RuntimeException("下单失败,国航无票可订");
}
//锁定南航订单,道理与锁定国航订单相同
nhService.doOrder(busId,idcard);
return ret;
}
- 第二阶段:confirm
-
try阶段已经为此语句准备好了执行条件,不会再出现业务的冲突情况(业务一定是可行的)
-
confirm 的时候会查询try里面的操作是否成功
如果你在各个服务里引入了一个 TCC 分布式事务的框架,订单服务里内嵌的那个 TCC 分布式事务框架可以感 知到,各个服务的 Try 操作都成功了。此时,TCC 分布式事务框架会控制进入 TCC 下一个阶段,也就是 Confirm 阶段。案例中为查询南航,国航资源是否都成功锁定
如果都成功则执行数据修改nhService.confirmOrder和ghservice.confirmOrder
-
//分别调用
public int confirmOrder(String busId,String idcard) {
System.out.println("confirm order:"+busId);
//本语句是幂等性的
int ret = jdbcTemplate.update("UPDATE tcc_fly_order SET frozen = 0,STATUS = 1,idcard=? WHERE bus_id=? and STATUS = 0 ",idcard,busId);
return ret;
}
- 第三阶段:cancel
-
恢复资源状态,取消资源的冻结状态
如果有Try 操作执行失败,那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。案例中为查询南航,国航资源时发现资源锁定失败
则解冻南航,国航被都冻结的资源nhService.cancelOrder和ghservice.cancelOrder
-
public int cancelOrder(String busId,String idcard) {//补偿性
System.out.println("cancel order:"+busId);
//本语句是幂等性的
int ret = jdbcTemplate.update("UPDATE tcc_fly_order SET frozen = 0,STATUS = 0 WHERE bus_id=?",busId);
return ret;
}
}
try-confirm-cancel的三个步骤联动过程
- 如果try不出问题正常结束,则框架自动去调用confirm方法
- 如果try出了异常,框架去调用cancel补偿方法
tcc框架对业务方法的要求
- 可查询操作:操作具有唯一标识,可查询操作执行结果
- 幂等操作: y = f(x) = f(f(x)) = f(f(f(x)))
- TCC操作 : Try-Confirm-Cancel
- 补偿操作 : 抵销(或部分抵销)正向业务操作的业务结果
tcc是如何解决2PC的问题呢?
Try操作主要是为了“保证业务操作的前置条件都得到满足”,然后在Confirm/Cancel阶段,因为前置条件都满足了,所以可以不断重试保证成功。这就要求Confirm/Cancel都必须是幂等操作。
关于tcc介绍,介绍一篇很不错的博客:https://www.cnblogs.com/jajian/p/10014145.html
3.其它事务框架
异步确保型事务
-------- 典型的使用MQ中间件来传递事务调用。
-------- 其重点是确保事务消息一定会传达到目标系统,目标系统只许成功不许失败(不成功我也不管)
-------- 因为是异步的,因此事务发起后,就不再关注了。名曰事务,已不像事务范畴
最大努力通知型
-------- 通过多线程异步重试,来传递事务调用
-------- 即通过重试机制,努力将事务调用传递到目标系统,目标系统只许成功不许失败(不成功我也不管)
-------- 因为是异步的,其执行已经脱离事务范畴