分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用。
分布式系统会把一个应用拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务,创建订单减库存事务,银行转账事务等都是分布式事务。
我们知道本地事务依赖数据库本身提供的事务特性来实现,因为以下逻辑可以控制本地事务:
begin transaction:
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
但是在分布式环境下,会变成下边这样:
begin transaction:
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
分布式事务产生的场景
1.典型的场景就是微服务架构
经典的订单服务和库存服务
2.单体系统访问多个数据库实例
3.多服务访问同一个数据库实例
分布式事务基础理论
CAP理论
CAP是Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容错性
Consistency
一致性是指写操作后的读操作可以读到最新的数据状态,当数据分布在多个节点上,从任意节点读取到的数据都是最新的状态。
上图中,商品信息的读写要满足一致性就是实现如下目标:
1.商品服务写入主数据库成功,则向从数据库查询数据也成功
2.商品服务写入主数据库失败,则向从数据库中查询数据也失败
如何实现一致性?
1、写入主数据库要将数据同步到从数据库
2、写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。
分布式系统一致性的特点:
1、由于存在数据同步的过程,写操作的响应会有一定的延迟。
2、为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
3、如果请求数据同步失败的节点则会返回错误信息,一定不会返回旧数据
Availability
可用性是指任何事物操作都可以得到响应结果,且不会出现响应超时或响应错误
上图中,商品信息读取满足可用性就是要实现如下目标:
1、从数据库接收到数据查询的请求则立即能够响应数据查询结果
2、从数据库不允许出现响应超时或响应错误
如何实现可用性?
1、写入主数据后要将数据同步到从数据库
2、由于要保证从数据库的可用性,不可将从数据库的资源进行锁定
3、即时数据还没有同步过来,从数据库也要返回查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。
分布式系统可用性的特点:
1、所有请求都有响应,且不会出现响应超时响应错误
Partition tolerance
通常分布式系统的各个节点不是在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致节点之间通信失败,此时仍可对外提供服务服务,这叫分区容忍性。
上图中,商品信息读写满足分区容忍性就是要实现如下目标:
1、主数据库向从数据库同步数据失败不影响读写操作
2、其一个结点挂掉不影响另一个结点对外提供服务。
如何实现分区容忍性?
1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据库,这样结点之间能有效的实现松耦合。
2、添加从数据库节点,其中一个从节点挂掉其他从节点提供服务。
分布式分区容忍性的特点:
1、分区容忍性是分布式系统具备的基本能力,也就是说分布式系统必须有P
CAP组合方式
在所有分布式事务场景中不会同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。
AP:
放弃一致性,追求分区容忍性和可用性,这是很多分布式系统设计时的选择。
例如:
上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的数据在一定时间内不是最新的即可。
通常实现AP都会保证最终一致性,后面的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
CP:
放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行都完成整个事务才算完成。
CA:
放弃分区容忍性,即不进行分区,不考虑由于网络不通或节点挂掉的问题,则可以实现一致性和可用性,那么系统将不是一个标准的分布式系统,我们最常用的关系型数据库就满足了CA
上边的商品管理,如果要实现CA则架构如下:
BASE理论介绍
BASE是Basically Available(基本可用) 、Soft state(软状态)、Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态,满足BASE理论的事务,我们称之为“柔性事务”
•基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如:电商网站交易付款出现问题了,商品依然可以正常浏览。
•软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态,也叫软状态。这个状态不影响系统可用性,如订单的支付中,数据同步中等状态,待数据最终一致后状态改为成功状态
•最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致,如订单的支付中状态,最终会变为支付成功或者支付失败,使订单与实际交易结果达成一致,但需要一定时间的延迟、等待。
分布式事务解决方案之2PC(两阶段提交)
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase),提交阶段(commit phase)2是指两个阶段、P是指准备阶段、C是指提交阶段
举例:张三和李四好久不见,老友约起聚餐,饭店老板要求先买单,才能出票。这时张三和李四分别抱怨近况不如意,囊中羞涩,都不愿意请客,这时只能AA。只有张三和李四都付款,老板才能出票安排就餐。但由于张三和李四都是铁公鸡,形成了尴尬的一幕:
准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。
提交阶段:老板出票,两人拿票纷纷落座就餐。
例子中形成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,并且会把已收款退回。
整个事务过程由事务管理器和参与者组成,店老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。
在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议,如下图:
1.准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
2.提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁·资源。
成功情况:
Seata方案:
Seata是由阿里中间件团队发起的开源项目。它是一个开源的分布式事务框架。传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供了AT模式(即2PC)及TCC模式的分布式就事务解决方案。
Seata的设计思想如下:
Seata的设计目标其一是对业务无侵入,因为从业务无侵入的2PC方案着手,在传统的2PC的基础上演进,并解决2PC方案面临的问题。
Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下面是全局事务与分支事务的关系图:
Version:0.9 StartHTML:0000000105 EndHTML:0000002864 StartFragment:0000000141 EndFragment:0000002824
环境搭建不赘述了 可以自己去看下官网
主要是创建代理数据源
@Configuration
public class DatabaseConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.ds0")
public DruidDataSource ds0() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Mapper
@Component
public interface AccountInfoDao {
//更新账户金额
@Update("update account_info set account_balance = account_balance + #{amount} where
account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double
amount);
}
@FeignClient(value = "seata‐demo‐bank2",fallback = Bank2ClientFallback.class)
public interface Bank2Client {
@GetMapping("/bank2/transfer")
String transfer(@RequestParam("amount") Double amount);
}
@Component
public class Bank2ClientFallback implements Bank2Client{
@Override
public String transfer(Double amount) {
return "fallback";
}
}
@Service
public class AccountInfoServiceImpl implements AccountInfoService {
private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class);
@Autowired
AccountInfoDao accountInfoDao;
@Autowired
Bank2Client bank2Client;
//张三转账
@Override
@GlobalTransactional
@Transactional
public void updateAccountBalance(String accountNo, Double amount) {
logger.info("******** Bank1 Service Begin ... xid: {}" , RootContext.getXID());
//张三扣减金额
accountInfoDao.updateAccountBalance(accountNo,amount*‐1);
//向李四转账
String remoteRst = bank2Client.transfer(amount);
//远程调用失败
if(remoteRst.equals("fallback")){
throw new RuntimeException("bank1 下游服务异常");
}
//人为制造错误
if(amount==3){
throw new RuntimeException("bank1 make exception 3");
}
}
}
将@GlobalTransactional注解标注在全局事务发起的Service实现方法上,开启全局事务:
Controller
@RestController
public class Bank1Controller {
@Autowired
AccountInfoService accountInfoService;
//转账
@GetMapping("/transfer")
public String transfer(Double amount){
accountInfoService.updateAccountBalance("1",amount);
return "bank1"+amount;
}
}
@Mapper
@Component
public interface AccountInfoDao {
//向李四转账
@Update("UPDATE account_info SET account_balance = account_balance + #{amount} WHERE
account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double
amount);
}
(2
)
Service
@Service
public class AccountInfoServiceImpl implements AccountInfoService {
private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class);
@Autowired
AccountInfoDao accountInfoDao;
@Override
@Transactionalpublic void updateAccountBalance(String accountNo, Double amount) {
logger.info("******** Bank2 Service Begin ... xid: {}" , RootContext.getXID());
//李四增加金额
accountInfoDao.updateAccountBalance(accountNo,amount);
//制造异常
if(amount==2){
throw new RuntimeException("bank1 make exception 2");
}
}
}
(3)Controller
@RestController
public class Bank2Controller {
@Autowired
AccountInfoService accountInfoService;
@GetMapping("/transfer")
public String transfer(Double amount){
accountInfoService.updateAccountBalance("2",amount);
return "bank2"+amount;
}
}
Version:0.9 StartHTML:0000000105 EndHTML:0000010827 StartFragment:0000000141 EndFragment:0000010787
try:检查余额是否够30元 扣减30元
confirm: 空
cancel:增加30元
try:增加30元
confirm: 空
cancel:减少30元
try:try幂等校验 try悬挂处理 检查余额是否够30元 扣减30元
confirm: 空
cancel:
cancel幂等校验
cancel空回滚处理
增加可用余额30元
try:空
confirm:
confirm幂等校验
正式增加30元
cancel:空
CREATE DATABASE `hmily` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE
`account_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户 主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行 卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic; INSERT INTO `account_info` VALUES (2, '张三的账户', '1', '', 10000);
CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE TABLE
`account_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户 主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行 卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic; INSERT INTO `account_info` VALUES (3, '李四的账户', '2', NULL, 0);
CREATE TABLE `local_try_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事务id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `local_confirm_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事务id', `create_time` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `local_cancel_log` ( `tx_no` varchar(64) NOT NULL COMMENT '事务id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
4.3.5 discover-server
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily‐springcloud</artifactId>
<version>2.0.4‐RELEASE</version>
</dependency>
4.3.7 dtx-tcc-demo-bank1