事务-06丨ByteTcc分布式事务框架

Posted by jiefang on January 30, 2021

ByteTcc分布式事务框架

简介

  • ByteTCC是分布式事务管理器的一个实现,基于Try-Confirm-Cancel(TCC)机制。
  • ByteTCC与JTA兼容,可以与Spring和其他Java容器无缝集成。

ByteTCC的GitHub地址

原理

模块

  1. TCC服务:由实现业务接口的三个实现类(业务实现类,确认实现类,取消实现类)一起构成一个TCC型服务;
  2. 业务代理逻辑:TCC服务的业务实现类,在spring容器中注册为bean之后,由Spring容器创建的AOP代理对象。该类与业务实现类实现相同的业务接口,spring容器基于该代理实现请求拦截以及事务控制;
  3. CompensableManager(TCC全局事务管理器):实现JTA规范的TransactionManager,供Spring容器直接调用,负责Spring容器中声明式事务的commit、rollback、suspend、resume等管理操作;
  4. CompensableTransaction:负责全局事务相关的处理逻辑,实现TransactionListener接口,在ByteJTA本地事务commit/rollback本地事务时会接收到相应的通知及状态;
  5. CompensableCoordinator(TCC分支事务协调者):实现TransactionParticipant接口。由RPC自定义拦截器(客户端)调度,提供远程事务分支管理接入入口;
  6. ByteJTA:实现TransactionManager & TransactionParticipant接口。由ByteTCC调度,提供本地事务管理接入入口;
  7. CompensableLogger(事务日志管理模块):负责事务日志的存储及读取;
  8. TransactionRecovery(事务恢复处理模块):负责未完成事务的数据恢复、状态判定并向事务管理模块提供事务完成方向建议;

执行原理

全局事务管理

  1. 业务子系统发起业务请求时,Spring容器使用事务拦截器拦截该业务请求,在转发业务请求之前,开启全局事务,并在转发业务请求之后完成(提交/回滚)全局事务。
  2. 全局事务回滚时,会回调当前事务中业务逻辑的补偿逻辑(前提是该业务逻辑的操作已经生效,即DB事务已经提交)。

本地事务管理:Try业务操作

  1. 业务子系统发起业务请求时,Spring容器使用事务拦截器拦截该业务请求,在转发业务请求之前,开启本地事务,并在转发业务请求之后完成(提交/回滚)本地事务。

本地事务管理:Confirm/Cancel操作

  1. 事务管理器根据全局事务完成方向的决策判断来发起Confirm/Cancel请求;
  2. 当全局事务决定提交时,事务管理器触发确认操作,Spring容器使用事务拦截器拦截该确认请求,在转发确认操作之前,开启本地事务,并在确认操作完成之后提交该本地事务;
  3. 当全局事务决定回滚时,事务管理器触发取消操作,Spring容器使用事务拦截器拦截该取消请求,在转发取消操作之前,开启本地事务,并在取消操作完成之后提交该本地事务;

使用说明

使用约束

共同约束

  1. 针对一个特定的可补偿型服务接口,业务系统提供的Try、Confirm、Cancel三个实现类,其Try实现类必须定义@Compensable注解,而Confirm、Cancel实现类则不能定义Compensable注解;
  2. 可补偿型服务的Try/Confirm/Cancel实现类/实现方法必须定义Transactional注解,且propagation必须是Required, RequiresNew, Mandatory中的一种(即业务代码必须参与事务,从0.3.0开始强制要求);
  3. 业务系统尽量不要使用随机端口;
  4. 在每个参与tcc事务的数据库中创建bytejta表(ddl见bytetcc-supports.jar/bytetcc.sql);
  5. JDK版本:8.0及以上版本;

使用Spring Cloud的约束

  1. 服务提供方Controller必须添加@Compensable注解;
  2. 不允许对Feign/Ribbon/RestTemplate等HTTP请求自行进行封装,但允许拦截;
  3. 如果需要定制instanceId, 格式必须为${ip}:${自行指定}:${port};
  4. 0.5.x版本仅支持Spring Boot 2.x、Spring Cloud 2.x版本;

使用Dubbo的约束

  1. 必须且仅可指定一个元素,其name不能为空,且必须唯一;
  2. 必须且仅可指定一个元素,其port不能为空,也不能为-1;
  3. 定义dubbo服务提供者时():a、filter必须为bytetcc;b、cluster必须为failfast;c、retries必须-1;d、group必须为x-bytetcc;
  4. 定义dubbo服务消费者时():a、filter必须为bytetcc;b、cluster必须为failfast;c、retries必须为-1;d、group必须为x-bytetcc;
  5. 通过url来配置时(或自行实现loadbalance机制选择服务实例时),url的ip、port必须是dubbo发现机制发现的可选地址中的一个,即不允许指定dubbo发现机制可发现的服务实例之外的服务提供者;

业务开发

通过@Compensable注解定义的service为可补偿型service。@Compensable注解需要定义三个参数: 1)interfaceClass,必需。该值用于指定confirm/cancel针对的业务接口,该接口同时被用于校验confirm/cancel实现类。confirm/cancel实现类如果没有实现该业务接口则会被认为无效; 2)confirmableKey,可选。该值用于指定confirm实现类在容器中的beanId,若没有confirm逻辑则不必指定; 3)cancellableKey,可选。该值用于指定cancel实现类在容器中的beanId,若没有cancel逻辑则不必指定;注意:若try阶段执行了写操作则必须有相应的取消逻辑;

try定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@Compensable(
  interfaceClass = IAccountService.class 
, confirmableKey = "accountServiceConfirm"
, cancellableKey = "accountServiceCancel"
)
public class AccountController implements IAccountService {

	@Transactional
	@ResponseBody
	@RequestMapping(value = "/increase", method = RequestMethod.POST)
	public void increaseAmount(@RequestParam("acctId") String acctId, @RequestParam("amount") double amount) 
               throws ServiceException {
	    // TODO ...
	}

	@Transactional
	@ResponseBody
	@RequestMapping(value = "/decrease", method = RequestMethod.POST)
	public void decreaseAmount(@RequestParam("acctId") String acctId, @RequestParam("amount") double amount) 
               throws ServiceException {
	    // TODO ...
	}
}

confirm定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service("accountServiceConfirm")
public class AccountServiceConfirm implements IAccountService {
	static final Logger logger = LoggerFactory.getLogger(AccountServiceConfirm.class);

	@Transactional
	public void increaseAmount(String accountId, double amount) 
               throws ServiceException {
	    // TODO ...
	    logger.info("done increase: acct= {}, amount= {}", accountId, amount);
	}

	@Transactional
	public void decreaseAmount(String accountId, double amount) 
               throws ServiceException {
	    // TODO ...
	    logger.info("done decrease: acct= {}, amount= {}", accountId, amount);
	}
}

注意: 1)全局事务决定提交时,可补偿型service的confirm逻辑总是会被执行; 2)全局事务决定提交时,可能会存在某个分支事务try操作没有执行成功的情况,此时该分支的confirm逻辑仍然会被调用。存在该情况的原因是:分支事务执行出错并抛出异常(如ServiceException),其业务逻辑通过Transactional定义了该异常应该回滚事务(或容器通过判断其异常类型最终决定回滚),因而导致分支的try阶段操作没有生效;然而发起方捕捉到了分支抛出的异常,此时如果发起方可以处理分支执行出错的逻辑,则不再向外抛出异常;最终发起方的容器认为执行成功,并决定提交全局事务,因此就会通知分支事务管理器提交分支事务,而分支事务会回调分支事务中涉及的所有service的confirm逻辑。 3)confirm逻辑被回调时,若不确定try阶段事务是否成功执行,则可以通过CompensableContext.isCurrentCompensableServiceTried()来确定。 4)confirm阶段仅负责本service的confirm逻辑,而不应该再执行远程调用。如果try阶段调用过远程服务,则事务上下文已传播至远程节点,全局事务提交时,将由其所在节点的事务管理器负责执行confirm逻辑。

cancel定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service("accountServiceCancel")
public class AccountServiceCancel implements IAccountService {
	static final Logger logger = LoggerFactory.getLogger(AccountServiceCancel.class);

	@Transactional
	public void increaseAmount(String accountId, double amount) 
               throws ServiceException {
	    // TODO ...
	}

	@Transactional
	public void decreaseAmount(String accountId, double amount) 
               throws ServiceException {
	    // TODO ...
	}
}

注意: 1) 全局事务决定回滚时,分支事务中可补偿型service的cancel逻辑不一定会被执行,原因是:参与该分支事务的Try方法可能抛出异常导致其本地事务回滚,因此该服务的Try操作是没有生效的; 2) 全局事务决定回滚时,主事务中可补偿型service的cancel逻辑并不一定会被执行;原因是:主事务控制着全局事务的最终完成方向,当其最终决定回滚全局事务时,有机会通过将自己本地Try阶段的事务直接rollback来完成撤销try阶段操作,而不必通过cancel逻辑来实现。 3) cancel阶段仅负责本service的cancel逻辑,而不应该再执行远程调用。如果try阶段调用过远程服务,则事务上下文已传播至远程节点,全局事务回滚时,将由其所在节点的事务管理器负责执行cancel逻辑。

幂等性说明

ByteTCC不要求service的实现逻辑具有幂等性。事实上,ByteTCC也不推荐在业务层面保障业务操作幂等性,因为在业务层面实现幂等性,其复杂度非常高。因此ByteTCC在实现时也做了这方面的考虑。ByteTCC在TCC事务提交/回滚时,虽然也可能会多次调用confirm/cancel方法,但是ByteTCC可以确保每个confirm/cancel方法仅被”执行并提交”一次。所以,在使用ByteTCC时可以仅关注业务逻辑,而不必考虑事务相关的细节。

“仅执行并提交一次”的说明

1)Confirm操作虽然可能被多次调用,但是其参与的LocalTransaction均由ByteTCC事务管理器控制,一旦Confirm操作所在的LocalTransaction事务被ByteTCC事务管理器成功提交,则ByteTCC事务管理器会标注该Confirm操作成功,后续将不再执行该Confirm操作。 2)Cancel操作的控制原理同Confirm操作。需要说明的是,Cancel操作只有在Try阶段所在的LocalTransaction被成功提交的情况下才会被调用,Try阶段所在的LocalTransaction被回滚时Cancel操作不会被执行。