基本思路
先决条件
- 支持本地ACID事务的关系数据库。
- 通过JDBC访问数据库的Java应用程序。
整体机制
从两个阶段提交协议的演变:
- 阶段1:在同一本地事务中提交业务数据和回滚日志,然后释放本地锁和连接资源。
- 阶段2:
- 对于提交情况,异步快速地完成工作。
- 对于回滚情况,请根据阶段1中创建的回滚日志进行补偿。
写隔离
- 在全局锁,必须犯的阶段1的本地事务之前获取。
- 如果未获取全局锁,则不应提交本地事务。
- 如果失败,一个事务将尝试多次获取全局锁,但如果超时,则会发生超时,并回滚本地事务并释放本地锁。
例如:
两个事务tx1和tx2试图更新表a的字段m。m的原始值为1000。
tx1首先开始,开始本地事务,获取本地锁,然后执行更新操作:m = 1000-100 =900。tx1必须在提交本地事务之前获取全局锁,然后再提交本地事务并释放本地锁。
接下来,tx2开始本地事务,获取本地锁,执行更新操作:m = 900-100 =800。在tx2可以提交本地事务之前,它必须获取全局锁,但是全局锁可能由tx1持有,因此tx2会重试。在tx1执行全局提交并释放全局锁之后,tx2可以获取全局锁,然后可以提交本地事务并释放本地锁。
参见上图,tx1在阶段2中执行全局提交并释放全局锁,tx2获取全局锁并提交本地事务。
参见上图,如果tx1要执行全局回滚,则它必须获取本地锁以还原阶段1的更新操作。
但是,现在本地锁由希望获取全局锁的tx2持有,因此tx1无法回滚,但是它将尝试多次,直到tx2获取全局锁超时,然后tx2回滚本地事务并释放本地锁定后,tx1可以获取本地锁定,并成功执行分支回滚。
因为全局锁在整个过程中都由tx1保持,所以没有写脏问题。
读取隔离
本地数据库的隔离级别被读为commit commit或更高,因此全局事务的默认隔离级别被读为uncommitted。
如果当前需要读取全局事务的隔离级别,则Fescar可以通过SELECT FOR UPDATE语句来实现它。
在全局锁是SELECT FOR UPDATE语句的执行过程中被应用,如果全局锁被其他事务持有,该交易将释放本地锁重试执行SELECT FOR UPDATE语句。在整个过程中,查询将被阻塞,直到获取了全局锁为止,如果获取了锁,则意味着另一个全局事务已提交,因此全局事务的隔离级别被读取为commit。
出于性能方面的考虑,Fescar只对SELECT FOR UPDATE做代理工作。对于常规的SELECT语句,什么也不做。
工作过程
以一个例子来说明它。
业务表:product
领域 | 类型 | 键 |
---|---|---|
ID | bigint(20) | PRI |
名称 | varchar(100) | |
以来 | varchar(100) |
AT模式下分支事务的sql:
update product set name = 'GTS' where name = 'TXC';
阶段1
处理:
- 解析sql:知道sql类型为更新操作,表名称为product,条件为name ='TXC',依此类推。
- 在更新之前查询数据(在图像之前命名):为了找到将要更新的数据,请通过上述where条件生成查询语句。
select id, name, since from product where name = 'TXC';
得到了“之前的图像”:
ID | 名称 | 以来 |
---|---|---|
1个 | TXC | 2014年 |
- 执行更新sql:更新名称等于“ GTS”的记录。
- 更新后查询数据(以图像命名):通过更新前图像数据的主键找到记录。
select id, name, since from product where id = 1;
得到了残像:
ID | 名称 | 以来 |
---|---|---|
1个 | GTS | 2014年 |
- 插入回滚日志:使用前后图像以及SQL语句相关信息构建回滚日志,然后插入table中
UNDO_LOG
。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
- 在本地提交之前,该事务将应用提交给TC,以获取表产品中主键等于1的记录的全局锁。
- 提交本地事务:在同一本地事务中提交PRODUCT表的更新和UNDO_LOG表的插入。
- 向TC报告步骤7的结果。
阶段2-回退案例
- 从TC收到回滚请求后,开始本地事务,执行以下操作。
- 通过XID和分支ID检索UNDO LOG。
- 验证数据:将UNDO LOG中更新后的图像数据与当前数据进行比较,如果存在差异,则表示该数据已被当前事务以外的操作所更改,应采用不同的策略进行处理,其他将对此进行详细描述文献。
- 基于UNDO LOG中的前映像和业务SQL的相关信息,生成回滚SQL语句。
update product set name = 'TXC' where id = 1;
- 提交本地事务,将本地事务的执行结果(分支事务的回滚结果)报告给TC。
阶段2-提交案例
- 收到TC的提交请求后,将请求放入工作队列,立即将成功返回TC。
- 在队列中执行异步工作的阶段,将以批处理方式删除UNDO LOG。
附录
撤消日志表
UNDO_LOG表:不同数据库的数据类型略有不同。
对于MySQL示例:
领域 | 类型 |
---|---|
branch_id | bigint PK |
西德 | varchar(100) |
rollback_info | 长毛 |
log_status | tinyint |
log_created | 约会时间 |
log_modified | 约会时间 |
分机 | varchar(100) |
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime NOT NULL COMMENT 'create datetime',
`log_modified` datetime NOT NULL COMMENT 'modify datetime',
`ext` varchar(100) DEFAULT NULL COMMENT 'reserved field',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';