什么是事务?
事务从本质上讲就是:逻辑上的一组操作,组成这组操作的各个逻辑单元在不同的服务甚至服务器上,保证它们要成功就都成功,要失败就都失败。
事务的四大特性
提到事务就不得不提事务的四大特性(基本特征) ACID:
- 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
- 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
- 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
- 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
并发事务可能会带来的问题
- 脏读:一个事务可以读取另一个事务未提交的数据
- 不可重复读:一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
- 虚读(幻读):一个事务可以读取另一个事务已提交的数据 读取的数据前后多了或者少了
我们实践出真知:举个例子,mysql(5.6.16)
首先创建一张表:
CREATE TABLE `test_account` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, `money` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test_account` VALUES (1, '张三', 1000); INSERT INTO `test_account` VALUES (2, '李四', 1000); INSERT INTO `test_account` VALUES (3, '王五', 1000);
查看当前数据库隔离级别(详见下文):
select @@tx_isolation; # 结果:READ-COMMITTED # 设置事务的隔离级别 # set tx_isolation='隔离级别'; # 我们简单做下脏读的复现,开启俩个事务,第一个窗口做数据修改,但不提交: start transaction; UPDATE test_account set money = money - 100 WHERE id = 1; # 第二个窗口做数据查询,利用查询到的值去做数据处理: start transaction; SELECT money from test_account WHERE id = 1; 结果:1000 那么这时读到的数据是不准确的,这就是脏读
我们解决并发读的问题可以设置隔离级别解决问题:
隔离级别 |
脏读 |
不可重复读 |
幻读 |
读未提交 (Read uncommitted) |
√ |
√ |
√ |
读已提交 (Read committed) |
× |
√ |
√ |
可重复读 (Repeatable read) |
× |
× |
√ |
可串行化 (Serializable ) |
× |
× |
× |
Spring传播行为
Spring传播行为 |
介绍 |
REQUIRED |
支持当前事务,如果不存在,就新建一个 |
SUPPORTS |
支持当前事务,如果不存在,就不使用事务 |
MANDATORY |
支持当前事务,如果不存在,抛出异常 |
REQUIRES_NEW |
如果有事务存在,挂起当前事务,创建一个新的事务 |
NOT_SUPPORTED |
以非事务方式运行,如果有事务存在,挂起当前事务 |
NEVER |
以非事务方式运行,如果有事务存在,抛出异常 |
NESTED |
如果当前事务存在,则嵌套事务执行(嵌套式事务) |
事务的传播行为不是jdbc规范中的定义。传播行为主要针对实际开发中的问题
分布式事务
为什么要有分布式事务?
本地事务只能解决同一工程中的事务问题,而现在的场景更加复杂,关系到多个服务,怎么保证要么都成功,要么都失败?
分布式系统异常除了本地事务那些异常之外,还有:机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失等,这时候就需要引入分布式事务。
分布式事务出现的场景
- 不同的服务,不同数据库
- 相同的服务,不同数据库
- 不同的服务,相同数据库
分布式事务基础
数据库的 ACID 四大特性,已经无法满足我们分布式事务,这个时候有很多大佬提出一些新的理论。
CAP:
分布式存储系统的CAP原理(分布式系统的三个指标):
- Consistency(一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
- 对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
- Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(要求数据需要备份)
- Partition tolerance(分区容忍性):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们无法避免的。所以我们只能在一致性和可用性之间进行权衡,没有系统能同时保证这三点。要么选择CP、要么选择AP。
BASE:
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来看看BASE中的三要素:
- Basically Available(基本可用)
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。 电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
- Soft state(软状态)
- 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
- Eventually consistent(最终一致性)
- 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE模型是传统ACID模型的反面,不同于ACID,BASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了。
分布式事务解决方案
主流的解决方案如下:
- 基于XA协议的两阶段提交(2PC)
- TCC编程模式
- 消息事务+最终一致性
两阶段提交(2PC):
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交. 第二阶段:事务协调器要求每个数据库提交数据。 其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
XA 是一个两阶段提交协议,又叫做 XA Transactions。 目前主流数据库均支持2PC,XA协议。
XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换会导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
TCC补偿式事务:
TCC补偿式事务是一种编程式分布式事务。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
- Try:主要是对业务系统做检测及资源预留
- Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性。
- Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性。
整个TCC业务分成两个阶段完成:
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。
消息事务+最终一致性:
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体业务具体分析
总结:
- 高并发最终一致:消息事务+最终一致性
- 低并发基本一致:二阶段提交
- 高并发强一致:没有解决方案
分布式事务框架-seata
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
结构
Seata有3个基本组件:
- Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager(TM):事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager(RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
生命周期(重点)
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
引入seata并使用
- 把undo_log表导入数据库中
- 部署seata-server启动
- 在项目中添加seata依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.2.0</version> </dependency>
- 配置文件中添加配置:
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
- 引入steata配置文件:registry.conf和file.conf
java代码
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); } /** * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 * * @param druidDataSource The DruidDataSource * @return The default datasource */ @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }
使用
使用stata框架非常的简单,只需要引入注解就好了
- 分支事务方法上:@Transactional
- 全局事务方法上:@GlobalTransactional