Skip to main content

分布式事务

本框架原生分布式多数据源事务支持,无需依赖任何外部事务协调管理器或特别的服务支持。分布式事务管理模块基于TX-LCN重构,目前支持TCC、LCN(原生回滚)和TXC(补偿回滚)三种事务模型。 下面就这三种事务模型进行讲解说明。

配置#

首先我们配置好数据源,这里继续引用前面示例中使用的test数据库。下面配置了2个数据源,一个main主数据源,一个txlog数据源给事务日志使用。

# configuration for dev environment
---
readyWork:
server:
# This is the default binding address.
ip: 0.0.0.0
database:
dataSource:
main: # 主数据源
type: mysql
#driverClass: com.mysql.cj.jdbc.Driver #since mysql jdbc 8, The driver is automatically registered via the SPI
jdbcUrl: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
username: root
password: 12345678
txlog: # 事务日志数据源
type: mysql
enabledTransaction: false
#driverClass: com.mysql.cj.jdbc.Driver #since mysql jdbc 8, The driver is automatically registered via the SPI
jdbcUrl: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
username: root
password: 12345678
readyCloud:
jdbcIpFinder: true # 既然我们已经有了数据源,就让它多发挥点作用吧
transaction:
optimizeLoadBalancer: false # 关闭负载均衡优化器,为了更好的展示分布式效果,避免优化器将关联事务都优化到同一个节点了
txLogger:
dataSource: txlog # 设置事务日值数据源为上面定义的txlog数据源,事务日志不可以和业务用同一个数据源,但可以是同一个数据库

test数据库中的demo表和演示数据sql文件在examples\quickstart示例包中的resources文件夹,也可以从数据库基础应用-演示数据复制。 上面配置中,jdbcIpFinder是节点发现设置,请看节点发现-JDBC发现,需要注意txLogger事务日志不可以和业务用同一个数据源,但可以是同一个数据库。

服务定义#

下面定义了一组对demo表进行查询和更新的数据库操作服务。

public interface DemoService {
Demo getById(int id);
int updateByName(int gender, int age, String name);
int updateById(int gender, int age, int id);
int updateByIdForTxc(int gender, int age, int id);
}
@Service
public class DemoServiceImpl extends ModelService<Demo> implements DemoService {
@Override
public Demo getById(int id) {
return findById(id);
}
@Override
@Auto("update _TABLE_ set gender = ?, age = ? where name = ?")
public int updateByName(int gender, int age, String name){ return IfFailure.get(0); }
@Override
@Auto("update _TABLE_ set gender = ?, age = ? where id = ?")
public int updateById(int gender, int age, int id){ return IfFailure.get(0); }
@Override
@Auto("update _TABLE_ set gender = ?, age = ? where id = ?")
public int updateByIdForTxc(int gender, int age, int id){ return IfFailure.get(0); }
}

LCN事务模型#

LCN事务模型来源于TX-LCN,属于两阶段事务,也是本框架的默认事务模型,该事务模型有以下特点:

  1. LCN的主要优点有
  • 该模式对代码零侵入。
  • 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
  1. LCN的主要缺点有
  • 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
  • 该模式缺陷在于代理的连接需要随事务发起方一起释放连接,增加了连接占用的时间。

下面的实例演示最基本的事务,并作为API提供给其他微服务,用于后面展示各种事务嵌套:

@Service
public class ApiService extends BusinessService {
@Autowired
private DemoService demoService;
public Demo getById(int id) {
return demoService.getById(id);
}
@Transactional // 不设置type,默认LCN
public int updateByName(int gender, int age, String name) {
return demoService.updateByName(gender, age, name);
}
@Transactional // 不设置type,默认LCN
public int updateById(int gender, int age, int id) {
return demoService.updateById(gender, age, id);
}
}

注意上面的@Transactional注解,默认事务模型是LCN,定义一个Controller来测试上面的事务,并导出API

@RequestMapping("/api")
public class ApiController extends Controller {
@Autowired
private ApiService service;
@RequestMapping("/getById")
public Result<Demo> getById() {
int id = Assert.notNull(getParamToInt("id"), "id is required");
return Success.of(service.getById(id));
}
@RequestMapping("/updateByName")
public Result<Integer> updateByName() {
String name = Assert.notEmpty(getParam("name"), "name is required");
int gender = Assert.notNull(getParamToInt("gender"), "gender is required");
int age = Assert.notNull(getParamToInt("age"), "age is required");
return Success.of(service.updateByName(gender, age, name));
}
@RequestMapping("/updateById")
public Result<Integer> updateById() {
int id = Assert.notNull(getParamToInt("id"), "id is required");
int gender = Assert.notNull(getParamToInt("gender"), "gender is required");
int age = Assert.notNull(getParamToInt("age"), "age is required");
return Success.of(service.updateById(gender, age, id));
}
}

下面的实例展示了LCN事务嵌套,以及需要留意的嵌套死锁场景:

@Service
public class LcnDtxService extends BusinessService {
@Call(serviceId = "test-01", url = "/api/updateByName")
public int updateByName(int gender, int age, String name) {
return IfFailure.get(0);
}
@Call(serviceId = "test-01", url = "/api/updateById")
public int updateById(int gender, int age, int id) {
return IfFailure.get(0);
}
@Transactional // 不设置type,默认LCN
public boolean dtxService1(boolean withError) { // 如果传入参数打开错误开关,3次更新都会被回滚
if(updateById(1, 20, 2) > 0 && updateById(0, 21, 3) > 0 &&
updateById(1, 22, 4) > 0) {
if(withError) {
throw new RuntimeException("Error for testing lcn transaction");
}
return true; // 返回 true
}
return false;
}
@Transactional // 不设置type,默认LCN
public boolean dtxService2(boolean withError) { // 如果传入参数打开错误开关,第1个更新会被回滚
if(updateById(2, 21, 2) > 0 && // 这里会返回 true
updateById(2, 22, 211) > 0) { // 这里会返回 false, 因为表里没有 id = 211 的数据
if(withError) {
throw new RuntimeException("Error for testing lcn transaction");
}
return true;
}
return false; // 返回 false
}
@Transactional
public boolean dtxServiceDeadLock1() { // 在同一个父事务里,因为锁表冲突,导致死锁
// 更新条件是name列,该列没有索引,所以这两次更新都是锁表
// 由于该方法不是本地方法,是远程调用,每次updateByName有可能在当前节点执行也可能在其他节点执行
// 所以两次更新是不同的子事务,存在锁表冲突
if(updateByName(3, 23, "name2") > 0 &&
updateByName(3, 23, "name4") > 0) {
return true;
}
return false;
}
@Transactional
public boolean dtxServiceDeadLock2() { // 在同一个父事务里,因为锁行冲突,导致死锁
// 更新条件是id列,该列是主键,有索引,所以这两次更新都是锁行
// 由于两次更新id相同,所以两次更新会申请锁同一行
// 由于该方法不是本地方法,是远程调用,每次updateById有可能在当前节点执行也可能在其他节点执行
// 所以两次更新是不同的子事务,存在锁行冲突
if(updateById(4, 24, 2) > 0 &&
updateById(4, 24, 2) > 0) {
return true;
}
return false;
}
@Transactional
public boolean dtxServiceDeadLock3() { // 在同一个父事务里,因为锁行和锁表冲突,导致死锁
// 第一个更新条件是id列,该列是主键,有索引,所以这次更新是锁行
// 第二个更新条件是name列,该列没有索引,所以这次更新是锁表
// 由于该方法不是本地方法,是远程调用,updateById和updateByName有可能在当前节点执行也可能在其他节点执行
// 所以两次更新是不同的子事务,不论两个先后顺序如何,都会存在锁行和锁表冲突。
if(updateById(5, 25, 2) > 0 && // 这里锁定 id = 2 的行
updateByName(5, 25, "name4") > 0) { // 这里请求锁表
return true;
}
return false;
}
}

最后定义Controller导出上面的服务:

@RequestMapping("/lcn")
public class LcnDtxController extends Controller {
@Autowired
private LcnDtxService lcnDtxService;
@RequestMapping
public Result<Integer> updateByName() {
return Success.of(lcnDtxService.updateByName(0, 18, "name2"));
}
@RequestMapping
public Result<Integer> updateById() {
return Success.of(lcnDtxService.updateById(0, 20, 4));
}
@RequestMapping
public Result<Boolean> dtxService1() {
boolean withError = getParamToBoolean("error", false);
return Success.of(lcnDtxService.dtxService1(withError));
}
@RequestMapping
public Result<Boolean> dtxService2() {
boolean withError = getParamToBoolean("error", false);
return Success.of(lcnDtxService.dtxService2(withError));
}
}

通过浏览器即可测试上面的几种事务场景。

TCC事务模型#

TCC属于两阶段补偿型事务模型。TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC事务机制相对于二阶段提交,其特征在于它不依赖资源管理器(RM)对XA协议的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务, 将事务分成 Try 和 Confirm/Cancel 两个阶段和三种操作: Try 尝试执行业务、 Confirm 确认执行业务、 Cancel 取消执行业务。 该事务模型有以下特点:

  1. TCC的主要优点有
  • 该模式对有无本地事务控制都可以支持使用面广。
  • 因为Try阶段检查并预留了资源,所以confirm阶段一般都可以执行成功。
  • 资源锁定都是在业务代码中完成,不会block住DB,可以做到对db性能无影响。
  • TCC的实时性较高,所有的DB写操作都集中在confirm中,写操作的结果实时返回。
  1. TCC的主要缺点有
  • 因为事务状态管理,将产生多次DB操作,这将损耗一定的性能,并使得整个TCC事务时间拉长。
  • 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
  • 事务涉及方越多,Try、Confirm、Cancel中的代码就越复杂,可复用性就越底。另外涉及方越多,这几个阶段的处理时间越长,失败的可能性也越高。
  • 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。

下面的实例演示了基本的TCC事务,并嵌套调用了上面的LCN事务:

@Service
public class TccDtxService extends BusinessService {
@Call(serviceId = "test-01", url = "/api/getById")
public Demo getById(int id) {
return IfFailure.get(null);
}
@Call(serviceId = "test-01", url = "/api/updateById")
public int updateById(int gender, int age, int id) {
return IfFailure.get(0);
}
// 这里指定了事务类型为TCC,并指定了TCC事务的confirm/cancel方法名,相对于当前类
@Transactional(type = "TCC", confirmMethod = "confirmTcc", cancelMethod = "cancelTcc")
public boolean dtxService(Map<String, Object> param) {
System.err.println("TCC transaction trying");
// 如果我们在这里调用远程方法更新id = 2的行,会和TCC的confirm/cancel方法中的调用有锁行冲突
// 尽管是同一个父事务,但他们各自在不同的LCN子事务,updateById子事务有可能在当前节点执行也可能在其他节点执行
//updateById(6, 66, 2);
param.put("lockRecord", getById(2)); // 演示假定TCC事务锁定资源
// 如果传入参数打开错误开关,会进入cancelTcc流程,如果没有打开withError开关,会走confirmTcc流程
if(param.get("withError") != null && param.get("withError").equals(true)) {
throw new RuntimeException("Error for testing tcc transaction");
}
return true;
}
public void confirmTcc(Map<String, Object> param) {
// 这是一个锁行的LCN子事务
// 由于该方法不是本地方法,是远程调用,每次updateById有可能在当前节点执行也可能在其他节点执行
updateById(5, 88, 2); // make some changes
System.err.println("TCC transaction confirmed");
}
public void cancelTcc(Map<String, Object> param) {
Demo record = (Demo)param.get("lockRecord");
// 这是一个锁行的LCN子事务
// 由于该方法不是本地方法,是远程调用,每次updateById有可能在当前节点执行也可能在其他节点执行
updateById(record.getGender(), record.getAge(), 2); // 注意了,TCC事务的回滚完全靠自己
System.err.println("TCC transaction canceled");
}
}

最后定义Controller导出上面的服务:

@RequestMapping("/tcc")
public class TccDtxController extends Controller {
@Autowired
private TccDtxService tccDtxService;
@RequestMapping
public Result<Boolean> dtxService() {
boolean withError = getParamToBoolean("error", false);
return Success.of(tccDtxService.dtxService(Kv.by("withError", withError)));
}
}

通过浏览器即可测试上面的TCC事务场景。

TXC事务模型#

TXC事务模型来源于阿里,也属于两阶段事务,该事务模型有以下特点:

  1. TXC的主要优点有
  • 该模式同LCN一样,对代码零侵入。
  • 该模式不会占用数据库的连接资源。
  1. TXC的主要缺点有
  • 该模式仅限于对支持SQL方式的模块支持。
  • 该模式由于每次执行SQL之前需要先查询并保存受影响的数据快照,因此相比LCN模式消耗资源与时间要多。

下面的实例演示了基本的TXC事务,并嵌套调用了上面的LCN事务:

@Service
public class TxcDtxService extends BusinessService {
@Autowired
private DemoService demoService;
@Call(serviceId = "test-01", url = "/api/updateByName")
public int updateByName(int gender, int age, String name) {
return IfFailure.get(0);
}
@Call(serviceId = "test-01", url = "/api/updateById")
public int updateById(int gender, int age, int id) {
return IfFailure.get(0);
}
// 这里指定了事务类型为TXC
@Transactional(type = "txc")
public boolean dtxService(boolean withError) {
// 这里通过DemoService进行了一次对id = 3的数据的本地更新操作,归属于本次TXC本地事务,也可以在前面API服务里面去定义,变成子事务,这里为了演示方便
demoService.updateByIdForTxc(6, 26, 3);
// 上面锁id = 3的行,下面2个锁行的LCN子事务,是锁不同的id行,所以不会有冲突
// 由于该方法不是本地方法,是远程调用,每次updateById有可能在当前节点执行也可能在其他节点执行
if(updateById(6, 26, 2) > 0 && // 会锁 id = 2
updateById(6, 26, 4) > 0) { // 会锁 id = 4
// 如果传入参数打开错误开关,第1个更新会被TXC事务通过快照回滚,两个LCN事务会通过ROLLBACK回滚
if(withError) {
throw new RuntimeException("Error for testing txc transaction");
}
return true;
}
return false;
}
}

最后定义Controller导出上面的服务:

@RequestMapping("/txc")
public class TxcDtxController extends Controller {
@Autowired
private TxcDtxService txcDtxService;
@RequestMapping
public Result<Boolean> dtxService() {
boolean withError = getParamToBoolean("error", false);
return Success.of(txcDtxService.dtxService(withError));
}
}

通过浏览器即可测试上面的TXC事务场景。

SAGA事务模型#

暂不支持SAGA事务模型,后续根据需要确定是否增加对该事务模型的支持。

注意与建议#

注意

在一个事务中,需要留意所调用的微服务及子事务是否有对相同内容的修改动作,事务与子事务是否具有兼容合并条件,如果没有那么就要考虑修改内容是否涉及事务冲突。

强烈建议远程微服务嵌套层次越少越好,层次嵌套会大幅度降低性能(http请求嵌套),增加逻辑关系复杂度,而且后续开发容易忘记嵌套了多少层了。 最关键是还会导致事务嵌套关系复杂且难以管理,尤其是不同事务类型的嵌套。 嵌套限制在2层内,即只做1次远程调用效果最好,既能发挥分布式微服务的优点,又具有较低的业务逻辑关系和事务关系复杂度。