
- 特 性
- 事务是恢复和并发控制的基本单位
- 外文名
- Transaction
- 中文名
- 事务
- 解 释
- 指要做的或所做的事情
-
2022-07-19 10:53:20
概念介绍
- 事务消息:提供类似XA或Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,生产者已经成功地将消息发送到了RocketMQ服务端,但是RocketMQ服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,RocketMQ服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查。
分布式事务消息的优势
RocketMQ分布式事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性。同时,传统的大事务可以被拆分为小事务,不仅能提升效率,还不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性。在极端情况下,如果关联的某一个应用始终无法处理成功,也只需对当前应用进行补偿或数据订正处理,而无需对整体业务进行回滚。
典型场景
在淘宝购物车下单时,涉及到购物车系统和交易系统,这两个系统之间的数据最终一致性可以通过分布式事务消息的异步处理实现。在这种场景下,交易系统是最为核心的系统,需要最大限度地保证下单成功。而购物车系统只需要订阅交易订单消息,做相应的业务处理,即可保证最终的数据一致性。
交互流程
事务消息交互流程如下图所示。
事务消息发送步骤如下:
- 生产者将半事务消息发送至RocketMQ服务端。
- RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息为半事务消息。
- 生产者开始执行本地事务逻辑。
- 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
- 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
事务消息回查步骤如下:
- 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
示例代码
事务消息生产者
public enum LocalTransactionState { COMMIT_MESSAGE, ROLLBACK_MESSAGE, UNKNOW, }
事务消息发送完成本地事务后,可在execute方法中返回以下三种状态:
- COMMIT_MESSAGE:提交事务,允许消费者消费该消息。
- ROLLBACK_MESSAGE:回滚事务,消息将被丢弃不允许消费。
- UNKNOW:暂时无法判断状态,等待固定时间以后消息队列RocketMQ版服务端根据回查规则向生产者进行消息回查。
创建事务消息的Producer时必须指定TransactionListener的实现类,处理异常情况下事务消息的回查。
回查规则:本地事务执行完成后,若服务端收到的本地事务返回状态为TransactionStatus.Unknow,或生产者应用退出导致本地事务未提交任何状态。则服务端会向消息生产者发起事务回查,第一次回查后仍未获取到事务状态,则之后每隔一段时间会再次回查。
回查间隔时间:系统默认每隔30秒发起一次定时任务,对未提交的半事务消息进行回查,共持续12小时。
package com.morris.rocketmq.transaction; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import static com.morris.rocketmq.util.Contant.NAME_SERVER_ADDRESS; /** * 事务消息生产者 */ public class TransactionProducer { public static void main(String[] args) throws Exception { TransactionMQProducer producer = new TransactionMQProducer("transaction-producer-demo"); producer.setNamesrvAddr(NAME_SERVER_ADDRESS); ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; }); producer.setExecutorService(executorService); // 指定事务会查的实现类 producer.setTransactionListener(new TransactionListener() { private final AtomicInteger transactionIndex = new AtomicInteger(0); private final ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); System.out.println(Thread.currentThread().getName()+ "-executeLocalTransaction:" + new String(msg.getBody()) + ",value=" + value); int status = value % 3; localTrans.put(msg.getTransactionId(), status); return LocalTransactionState.UNKNOW; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { System.out.println(Thread.currentThread().getName()+ "-checkLocalTransaction:" + new String(msg.getBody())); Integer status = localTrans.get(msg.getTransactionId()); if (null != status) { switch (status) { case 0: return LocalTransactionState.COMMIT_MESSAGE; case 1: return LocalTransactionState.UNKNOW; case 2: return LocalTransactionState.ROLLBACK_MESSAGE; } } return LocalTransactionState.COMMIT_MESSAGE; } }); producer.start(); for(int i = 0; i < 10; i++) { Message message = new Message("TransactionTopic", ("transactionDemo" + i).getBytes()); // 发送事务消息 producer.sendMessageInTransaction(message, i); System.out.println(message); } } }
第一次消息回查最快时间:该参数支持自定义设置。若指定消息未达到设置的最快回查时间前,系统默认每隔30秒一次的回查任务不会检查该消息。
设置方式如下:
Message message = new Message(); message.putUserProperties(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "60");
说明:因为系统默认的回查间隔,第一次消息回查的实际时间会向后有0秒~30秒的浮动。
例如:指定消息的第一次消息最快回查时间设置为60秒,系统在第58秒时达到定时的回查时间,但设置的60秒未到,所以该消息不在本次回查范围内。等待间隔30秒后,下一次的系统回查时间在第88秒,该消息才符合条件进行第一次回查,距设置的最快回查时间延后了28秒。
事务消息消费者
事务消息的Group ID不能与其他类型消息的Group ID共用。与其他类型的消息不同,事务消息有回查机制,回查时服务端会根据Group ID去查询生产者客户端。
package com.morris.rocketmq.transaction; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import static com.morris.rocketmq.util.Contant.NAME_SERVER_ADDRESS; /** * 事务消息消费者 */ public class TranscationConsumer { public static void main(String[] args) throws Exception { // 实例化消息生产者,指定组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction-consumer-group"); // 指定Namesrv地址信息. consumer.setNamesrvAddr(NAME_SERVER_ADDRESS); // 订阅Topic consumer.subscribe("TransactionTopic", "*"); //负载均衡模式消费 consumer.setMessageModel(MessageModel.CLUSTERING); // 注册回调函数,处理消息 consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { try { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); } catch (Exception e) { e.printStackTrace(); return ConsumeConcurrentlyStatus.RECONSUME_LATER; } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); //启动消息者 consumer.start(); System.out.printf("Consumer Started.%n"); } }
使用说明
- 事务消息不支持延时消息和批量消息。
- 事务回查的间隔时间:BrokerConfig.transactionCheckInterval,通过Broker的配置文件设置好。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为15次,但是用户可以通过Broker配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过N次的话(N=transactionCheckMax)则Broker将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写AbstractTransactionCheckListener类来修改这个行为。
- 事务消息将在Broker配置文件中的参数transactionMsgTimeout这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS来改变这个限制,该参数优先于transactionMsgTimeout参数。
- 事务性消息可能不止一次被检查或消费。
- 事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
- 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过RocketMQ本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
- 事务消息的生产者ID不能与其他类型消息的生产者ID共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者ID查询到消费者。
更多相关内容 -
分布式事务
2021-12-07 18:05:05本地事务ACID 通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被...一.分布式问题
一.本地事务ACID
通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
数据库事务的四大特性ACID:
A:原子性,事务中所有操作要么全部成功,要么全部失败。
C:一致性,要保证数据库内部完整性约束、声明性约束。
I:隔离性,对同一资源操作的事务不能同时发生。
D:持久性,对数据库做的一切修改将永久保存,不管是否出现故障。二.分布式事务
一.概念
分布式事务指事务的操作位于不同的节点上,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如:
- 跨数据源的分布式事务
- 跨服务的分布式事务
- 综合情况
二.举例
例如电商行业中比较常见的下单付款操作需要访问三个不同的微服务和三个不同的数据库,包括下面几个行为:
- 创建新订单
- 扣减商品库存
- 从用户账户余额扣除金额
把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。
此时ACID难以满足,这是分布式事务要解决的问题
二.理论基础
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。
一.CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
一.致性
- Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
- 如果修改其中一个节点的数据时,两者的数据产生了差异,要想保住一致性,就必须实现节点间的数据同步。
二.可用性
- Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
- 当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:
三.分区容错
- Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
- Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
四.矛盾
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。
当节点接收到新的数据变更时,就会出现问题了:
- 如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
- 如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。
- 也就是说,在P一定会出现的情况下,A和C之间只能实现一个。
二.BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- **Soft State(软状态):**在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
三.解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
-
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
-
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC):
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
三.Seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。
一.Seata的架构
Seata事务管理中有三个重要的角色:
-
TC (Transaction Coordinator) - **事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。
-
TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) - **资源管理器:**管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
二.部署TC服务
一、部署Seata的tc-server
1.下载
首先我们要下载seata-server包,地址在http😕/seata.io/zh-cn/blog/download.html
2.修改配置
修改conf目录下的registry.conf文件:
registry { # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等 type = "nacos" nacos { # seata tc 服务注册到 nacos的服务名称,可以自定义 application = "seata-tc-server" serverAddr = "192.168.136.160:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "SH" username = "nacos" password = "nacos" } } config { # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置 type = "nacos" # 配置nacos地址等信息 nacos { serverAddr = "192.168.136.160:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
3.在nacos添加配置
特别注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。
# 数据存储方式,db代表数据库 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true store.db.user=root store.db.password=159735 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 # 事务、日志等配置 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 # 客户端与服务端传输方式 transport.serialization=seata transport.compressor=none # 关闭metrics功能,提高性能 metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898
4.创建数据库表
特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。
新建一个名为seata的数据库,运行sql文件:SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- 分支事务表 -- ---------------------------- DROP TABLE IF EXISTS `branch_table`; CREATE TABLE `branch_table` ( `branch_id` bigint(20) NOT NULL, `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `status` tinyint(4) NULL DEFAULT NULL, `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime(6) NULL DEFAULT NULL, `gmt_modified` datetime(6) NULL DEFAULT NULL, PRIMARY KEY (`branch_id`) USING BTREE, INDEX `idx_xid`(`xid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- 全局事务表 -- ---------------------------- DROP TABLE IF EXISTS `global_table`; CREATE TABLE `global_table` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `timeout` int(11) NULL DEFAULT NULL, `begin_time` bigint(20) NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime NULL DEFAULT NULL, `gmt_modified` datetime NULL DEFAULT NULL, PRIMARY KEY (`xid`) USING BTREE, INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE, INDEX `idx_transaction_id`(`transaction_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
5.启动TC服务
进入bin目录,运行其中的seata-server.bat即可:
启动成功后,seata-server应该已经注册到nacos注册中心了,可以去nacos服务列表查看。三.微服务集成Seata
1.引入依赖
首先,在需要被事务管理的微服务中引入依赖:
<!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <!--版本较低,1.3.0,因此排除--> <exclusion> <artifactId>seata-spring-boot-starter</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <!--seata starter 采用1.4.2版本--> <version>${seata.version}</version> </dependency>
2.配置TC地址
被TC管理的微服务application.yml中,配置TC服务信息,通过注册中心nacos,结合服务名称获取TC地址:
seata: registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址 type: nacos # 注册中心类型 nacos nacos: server-addr: 192.168.136.160:8848 # nacos地址 namespace: "" # namespace,默认为空 group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP application: seata-tc-server # seata服务名称 username: nacos password: nacos tx-service-group: seata-demo # 事务组名称 service: vgroup-mapping: # 事务组与cluster的映射关系 seata-demo: SH
微服务如何根据这些配置寻找TC的地址呢?
我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:
- namespace:命名空间
- group:分组
- application:服务名
- cluster:集群名
namespace为空,就是默认的public结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。
四.事务模式
一.XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持,像mogondb、redis这些不支持
一.两阶段提交
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
1.正常情况:
2.异常情况:
一阶段:- 事务协调者通知每个事物参与者执行本地事务
- 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
二阶段:
- 事务协调者基于一阶段的报告来判断下一步操作
- 如果一阶段都成功,则通知所有事务参与者,提交事务
- 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
二.Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM一阶段的工作: ① 注册分支事务到TC
② 执行分支业务sql但不提交
③ 报告执行状态到TC
TC二阶段的工作:
-
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务
RM二阶段的工作:
- 接收TC指令,提交或回滚事务
三.优缺点
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
四.实现XA模式
(1)修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata: data-source-proxy-mode: XA
(2)给发起全局事务的入口方法添加@GlobalTransactional注解:
(3)重启服务并测试回滚
库存count只有10,显然mysql会出异常,然后全部微服务回滚了
五.优缺点
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
二.AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
一.Seata的AT模型
阶段一RM的工作:- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
二.脏写问题
解决:
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
三.实现AT模式
AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log
(1)undo_log表和全局锁表lock_table导入到数据库:-- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `undo_log` ( `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id', `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id', `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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(6) NOT NULL COMMENT 'create datetime', `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime', UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact; -- ---------------------------- -- Records of undo_log -- ---------------------------- -- ---------------------------- -- Table structure for lock_table -- ---------------------------- DROP TABLE IF EXISTS `lock_table`; CREATE TABLE `lock_table` ( `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `branch_id` bigint(20) NOT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime NULL DEFAULT NULL, `gmt_modified` datetime NULL DEFAULT NULL, PRIMARY KEY (`row_key`) USING BTREE, INDEX `idx_branch_id`(`branch_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
(2)修改所有相关微服务的application.yml文件,将事务模式修改为AT模式即可:
seata: data-source-proxy-mode: AT # 默认就是AT
(3)重启服务并测试
三个微服务都回滚
四. 优缺点
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
三.TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
-
Try:资源的检测和预留;
-
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
-
Cancel:预留资源释放,可以理解为try的反向操作。
一.流程分析
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
- 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初识余额:
余额充足,可以冻结:
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元- 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:
二.Seata的TCC模型
三.优缺点
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
四.事务悬挂和空回滚
(1)空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。
(2)业务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
五.实现TCC模式
(1)定义一张冻结表
CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) NOT NULL COMMENT '是全局事务id', `user_id` varchar(255) DEFAULT NULL COMMENT '用户id', `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额', `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
(2)实体类
(3)mapper
那此时,我们的业务开怎么做呢?
- Try业务:
- 记录冻结金额和事务状态到account_freeze表
- 扣减account表可用金额
- Confirm业务
- 根据xid删除account_freeze表的冻结记录
- Cancel业务
- 修改account_freeze表,冻结金额为0,state为2
- 修改account表,恢复可用金额
- 如何判断是否空回滚?
- cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
- 如何避免业务悬挂?
- try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
改造account-service,利用TCC实现余额扣减功能。
(4)声明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,我们在account-service项目中的
cn.itcast.account.service
包中新建一个接口,声明TCC三个接口:@LocalTCC public interface AccountTCCService { /** * try的接口 */ @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money")int money); /** *提交事务的接口 * @param ctx 可以拿到事务信息和参数信息 * @return */ boolean confirm(BusinessActionContext ctx); /** * 回滚接口 */ boolean cancel(BusinessActionContext ctx); }
(5)编写实现类
@Service @Slf4j public class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { // 0.获取事务id String xid = RootContext.getXID(); //1.判断freeze中是否有冻结数据,有回滚记录则,会造成业务悬挂 AccountFreeze oldFreeze = freezeMapper.selectById(xid); if (oldFreeze!=null){ return; } // 1.扣减可用余额 accountMapper.deduct(userId, money); // 2.记录冻结金额,事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { // 1.获取事务id String xid = ctx.getXid(); // 2.根据id删除冻结记录,删除是天然幂等性 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { // 0.查询冻结记录c String xid = ctx.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); //1.判断空回滚,根据freeze是否为null String userId = ctx.getActionContext("userId").toString(); if (freeze==null){ //try没执行,需要回滚,并记录回滚 freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); freeze.setXid(xid); freezeMapper.insert(freeze); return true; } //2.幂等性判断,防止重复操作 if (freeze.getState()==AccountFreeze.State.CANCEL){ //说明已经回滚过了 return true; } // 1.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 2.将冻结金额清零,状态改为CANCEL freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; } }
四.SAGA模式
Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。
一.原理
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
二.优缺点
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
五.四种模式对比
AT与XA的区别:- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致
五. 高可用
一.高可用架构模型
搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可。
但集群并不能确保100%安全,万一集群所在机房故障怎么办?所以如果要求较高,一般都会做异地多机房容灾。
比如一个TC集群在上海,另一个TC集群在杭州:
微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。二.TC服务的高可用和异地容灾
(1)模拟异地容灾的TC集群
计划启动两台seata的tc服务节点:节点名称 ip地址 端口号 集群名称 seata 127.0.0.1 8091 SH seata2 127.0.0.1 8092 HZ 将seata目录复制一份,起名为seata2,修改seata2/conf/registry.conf内容如下:
registry { # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等 type = "nacos" nacos { # seata tc 服务注册到 nacos的服务名称,可以自定义 application = "seata-tc-server" serverAddr = "127.0.0.1:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "HZ" username = "nacos" password = "nacos" } } config { # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置 type = "nacos" # 配置nacos地址等信息 nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
进入seata2/bin目录,然后运行命令:
seata-server.bat -p 8092
然后打开nacos控制台查看服务列表
(2)将事务组映射配置到nacos
接下来,我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。
新建一个配置:
# 事务组映射关系 service.vgroupMapping.seata-demo=SH service.enableDegrade=false service.disableGlobalTransaction=false # 与TC服务的通信配置 transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableClientBatchSendRequest=false transport.threadFactory.bossThreadPrefix=NettyBoss transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler transport.threadFactory.shareBossWorker=false transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector transport.threadFactory.clientSelectorThreadSize=1 transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread transport.threadFactory.bossThreadSize=1 transport.threadFactory.workerThreadSize=default transport.shutdown.wait=3 # RM配置 client.rm.asyncCommitBufferLimit=10000 client.rm.lock.retryInterval=10 client.rm.lock.retryTimes=30 client.rm.lock.retryPolicyBranchRollbackOnConflict=true client.rm.reportRetryCount=5 client.rm.tableMetaCheckEnable=false client.rm.tableMetaCheckerInterval=60000 client.rm.sqlParserType=druid client.rm.reportSuccessEnable=false client.rm.sagaBranchRegisterEnable=false # TM配置 client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.defaultGlobalTransactionTimeout=60000 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 # undo日志配置 client.undo.dataValidation=true client.undo.logSerialization=jackson client.undo.onlyCareUpdateColumns=true client.undo.logTable=undo_log client.undo.compress.enable=true client.undo.compress.type=zip client.undo.compress.threshold=64k client.log.exceptionRate=100
(3)微服务读取nacos配置
接下来,需要修改每一个微服务的application.yml文件,让微服务读取nacos中的client.properties文件:seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos group: SEATA_GROUP data-id: client.properties
重启微服务,现在微服务到底是连接tc的SH集群,还是tc的HZ集群,都统一由nacos的client.properties来决定了。
-
JMeter每秒事务数TPS插件
2017-01-16 19:05:25关于插件的详细安装及使用步骤参考博客《让你的 JMeter 像 LoadRunner 那样实时查看每秒事务数(TPS)、事务响应时间(TRT)》,博客地址:http://blog.csdn.net/defonds/article/details/54576604。 -
事务处理原理 第2版
2012-12-30 10:49:38阅读完本书之后,您会理解事务中间件的工作原理和何时使用事务中间件,以及事务中间件和数据库系统如何协同工作以支持可靠的分布式事务处理应用程序。还会迅速掌握如何使用任何事务中间件产品或数据库系统来支持... -
MySQL事务——万字详解
2021-11-25 16:06:50介绍什么是事务,隔离级别和操作 解释MVCC原理目录
1.5.5 begin开启的事务不会受MySQL事务提交方式的影响
前言
MySQL是一个网络服务。大多数情况下,会有很多客户端连接MySQL服务。当多个客户端访问同一个表时,可能会出现问题。
比如:火车票售票系统,当两个客户端同时买票,操作同一张票数表。当客户端A检测还有一张票,将票买掉,但是还没有更新数据库。于此同时,客户端B,也在买票,也检测到还有一张票,客户端B也将票买了。这样就导致一张票被卖了两次。
于是MySQL需要对此现象加以控制。这就是事务解决的问题。
要解决上面的问题,至少需要满足下面的属性(拿买票的过程举例):
- 买票的过程得是原子的
- 多个客户端买票,互相之间不能影响
- 买完票后,数据应该是永久有效的
- 买票前各个客户端之间看到的票数要是一样的;买票后,各个客户端看到的票数是一样的
一.事务的概念
1.1 什么是事务
事务就是一组DML类的SQL语句,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。
简单来说,事务就是要完成一件事,所用到的所有SQL语句(至少一条)。这些语句要么全部执行成功,要么全部执行失败。即,其中一条执行失败,就全部执行失败。数据立马回滚到执行前的状态。
1.2 事务的属性
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成。不会结束在中间的某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务没有发生一样。
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。可以理解成,事务执行完后,就将数据刷新到磁盘中了。但是,这并没有这么简单,因为,MySQL是是一个应用程序,数据刷新需要操作系统来做。故MySQL需要先将数据放到内核缓冲区中,再由操作系统将数据刷新到磁盘。
- 隔离性:数据库允许多个事务并发运行,并且允许事务同时对数据进行读,写和修改。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致的情况。但是,MySQL为了考虑效率,多个事务读,写或者修改同一数据时,并不一定是串行运行的。而是分成了不同的级别:读未提交(read uncommitted),读提交(read committed),可重复读(repeatable read)和串行化(serializable)。
- 一致性:在事务开始前和事务结束后,数据库的完整性。并且事务执行后,会完全符合结果。这需要MySQL和用户共同来保证的。即,MySQL保证了数据保护出错,用户需要保存流程正确。
上面四个属性,可以简称ACID。
- 原子性(Atomicity,又称不可分割性)
- 一致性(Consistency)
- 隔离性(Isolation,又称独立性)
- 持久性(Durability)
事务并不是伴随着数据库系统天生就有的,而是为应用层的服务的。
这样就使得用户在操作数据库时不需要考虑数据的安全问题,简化了编程模型。
1.3 事务的版本支持
事务是存储引擎提供的,在MySQL中只有使用了innodb存储引擎的数据库或者表才支持事务,MyISAM存储引擎不支持事务。
查看数据库存储引擎:
1.4 事务提交方式
事务提交方式有两种:
- 手动提交
- 自动提交
查看事务的提交方式:
修改事务的提交方式:
1.5 事务的常见操作
1.5.1 准备阶段:
手动启动一个事务:
begin/start transaction;--开始一个事务 ...--对表进行操作 commit;--提交事务
设置隔离级别为读未提交,后面在隔离性有详细解释。
注意:设置完global.transaction_isolation,需要重启MySQL。
创建表:
create table if not exists account( id int primary key, name varchar(50) not null default '', blance decimal(10,2) not null default 0.0 )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
1.5.2 手动演示回滚操作
创建保存点:savepoint 保存点名;
回滚:rollback 回滚的保存点;
1.5.3 简单证明原子性
注意:现在的隔离等级是读为提交。
事务没有执行完发生了错误,会回滚到最开始,相当于这个事务没有发生一样。
1.5.4 简单证明持久性
当一个事务完成后,操作完的数据会永久保存,系统是否故障。
1.5.5 begin开启的事务不会受MySQL事务提交方式的影响
结论:begin开启的事务,不受MySQL事务提交方式的影响,必须手动commit提交。
1.5.6 MySQL中SQL与事务的关系
结论:在MySQL中没有手动begin开启事务,增删查改都会被MySQL封装成一个事务,即使只有一条SQL语句。
提交方式为:自动提交。
提交方式为:手动提交
正确:
1.5.7 总结
- 事务可以手动回滚,同时,操作异常会自动回滚。
- 使用begin/start transaction开启的事务,必须通过commit才能提交,才会持久化,与MySQL的提交方式无关。
- 对于MySQL存储引擎使用innodb,每一条SQL语句都会默认封装成事务,提交方式看auto_commit是否开启。
注意点:
- 如果没有设置保存点,也可以回滚,只能回滚到事务最开始。直接使用rollback,前提是事务还没有提交。
- 如果一个事务被提交(commit),则不可以回滚。
- 可以选择回滚到哪个保存点。
- Innodb可以支持事务,MyISAM不支持事务。
- 手动开始事务使用begin,或start transaction。
二.隔离级别
再次说明,MySQL是一个网络服务,同一时间可能有很多客户端连接。那么在多个事务在执行多个SQL时,有可能出现问题,比如:多个事务访问同一张表,同一行数据时。
一个事务的时间段可以分为执行前(该事务还没有开始),执行中(该事务正在执行),执行后(该事务已经提交)。
隔离性:保证了事务执行过程中尽量不受干扰。
为了提高效率,执行过程并不是串行化,而是允许事务受到不同程度的干扰,于是就有了一个重要的特征:隔离级别。
2.1 隔离级别介绍
注意:隔离级别是针对事务之间的。
- 读未提交(read uncommitted):在该隔离级别下,所有事务都能看到其它事务没有提交的执行结果。相当于没有任何隔离性。在实际生产中不会使用这一隔离级别,因为会有很多并发问题,如脏读,幻读,不可重复度等。
- 读提交(read committed):事务只能看到其它事务已经提交后执行结果。这种隔离级别是大多数数据库的默认隔离级别(不是MySQL的),这种隔离级别会引起不可重复读的问题。
- 可重复度(repeatable read):在该隔离级别下,在一时间段同时运行的事务,执行中,看不到其它事务已经提交的执行结果,必须等事务执行完才能看到其它事务提交后执行结果。可能会出现幻读的问题。这是MySQL的默认隔离级别。
- 串行化(serialization):强制事务在增删改同一张表的同一行时排序执行,使之不可能相互冲突。实际,它是在访问的每一个数据行上加了共享锁。这是事务最高隔离级别。但是这个的效率很低,可能导致超时和锁竞争。(这种隔离太极端,生活中基本不使用)
隔离基本上都是通过加锁来实现的,不同隔离级别,锁的使用是不同的。常见的有,表锁,行锁,读锁,写锁,间隙锁,Next-key锁(GAP+行锁)。
读提交和可重复读的区别:
有两个事务,事务A和事务B。
事务A修改数据,提交后(commit)。读提交,事务B在提交前(commit)可以看到事务A修改后的数据。可重复读,事务B在提交前(commit)看不到事务A修改后的数据,事务B在提交后,才能看到。
2.2 查看和设置隔离性
查看隔离性:
设置隔离等级:
这是设置当前会话或者全局隔离级别的语法。
set [session | global] transaction isolation level {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE};
设置当前会话隔离性,另起一个会话,隔离性不会被设置,只影响当前会话。
设置全局隔离级别,另起一个会话,会被影响。
会话隔离级别开始启动时,会和全局隔离级别一样。
设置了全局会话隔离级别,当前会话隔离级别并没有改变,需要重启MySQL才会改变当前会话隔离级别。
修改当前会话隔离级别:
- 直接修改会话隔离级别
- 修改全局隔离级别,重启MySQL
2.3 操作演示
隔离级别时针对事务和事务之间的。
2.3.1 读未提交
现象:如果隔离级别为读未提交,一个事务A增删改表的内容,未提交事务,在另外一个事务B中可以看到变化的内容。
但是,如果事务A没有提交异常退出了,内容会发生回滚,回滚到事务开始前的内容,原因是事务的原子性。事务A提交了,事务B,提交后也可以看到修改之后的内容,这是应为事务的持久性。
一个事务执行中,读到了另外一个事务的增删改,但是没有commit的数据,这种现象叫做脏读。
2.3.2 读提交
现象:两个事务,事务A和事务B,隔离等级为读提交。事务A增删改表的内容,未提交事务,在事务B中,看不到增删改的内容。当事务A提交后,事务B中可以看到增删改的内容。注意事务B没有提交。
问题:此时在事务B中,没有提交事务。但是同样的读取,在同一个事务内,在不同的时间段,读取到的值不同,这种现在叫做不可重复读取。
2.3.3 可重复读
现象:两个事务,事务A和事务B。事务A增删改表的内容,未提交,事务B看不到修改后的数据;事务A提交后,事务B没有提交,仍然看不到提交的数据;事务B提交后,由于持久化,可以看到修改后的数据。
注意:并不是,同一时段的两个事务,事务A提交的数据,事务B就一定看不到。这个取决于select 的位置。下面详细解释了。
问题:
幻读:在事务只执行中,不同时间段查询,会查找出来新的记录。即,事务A插入一条数据,事务B在未提交前,看到了插入的数据。
但是,在下面的演示中,在事务A向表中插入数据,事务B中在未提交前没有看到插入的数据。这是因为MySQL解决了幻读的问题。
但是,一般的数据库,在事务未提交前能够读到其它数据插入的数据。这是为什么呢?
因为隔离性实现是对数据加锁实现的,而insert数据,插入的数据,在表中并不存在,一般的加锁无法屏蔽这里问题。
而MySQL是如何解决的呢?
MySQL用的是Next-Key锁(GAP+行锁)解决的。
2.3.4 串行化
现象:当前有多个事务时,一个事务要增删改表的数据,会发生阻塞。当其它数据全部退出,才能正常进行增删改操作。使得增删改时,事务之间运行是串行的。效率比较低。
但是:如下图,我们发现,查找,并没有串行化,有多个事务同时运行时,查找不会被阻塞,串行化。这是因为查询并不会修改数据。
2.3.5 总结
- 隔离等级越高,安全性越高,并发性越低,因为加锁。往往要在两者之间找一个平衡点。
- 不可重复读是,同一条件下,事务在执行过程中,不同时间段,读取的数据不同。
- 幻读是,同样条件下,事务在执行过程中,不同时间段,读出来,数据量比之前增加了。
- 脏读:同样条件下,事务在执行过程中,读到了其它事务修改的数据。
- 事务也有长短的概念,事务之间相互影响,指的是,在事务执行过程中,都没有提交(commit),影响会比较大。
针对MySQL:
隔离级别 脏读 幻读 不可重复读 读未提交【 Read Uncommitted 】有 有 有 读提交【 Read Committed 】没有 有 有 可重复读【 Repeatable Read 】没有 没有 没有 串行化【 serializable 】没有 没有 没有 注意:
当多个事务,同时进行update, insert或者delete时,是会有加锁现象的。即,会发生阻塞。但是select查询和增删改并不冲突,即不会发生阻塞。这是通过读写锁(锁由行锁或者表锁)+MVCC完成的。
这里不好演示,直接给了结论。
2.4 一致性
- 事务执行结果,必须使得数据库从一个一致性的结果,变成另外一个一致性状态。如果系统运行时发生中断,某个事务,被迫中断,而未完成事务对数据的修改,此时数据库出于一种不一致的状态。
- 一致性性就是,在事务开始前,数据是一致的,事务完成后,数据也是一致的。
- 其实一致性和用户业务的逻辑性相关,需要MySQL提供技术支持,保证数据不会出错。还需要用户业务逻辑上的支持,在MySQL文档中,一致性,是由用户来决定的。
- 技术上,数据库的原子性,隔离性,持久性保证了一致性。
三.解决并发的原理MVCC
对数据库操作实际上就是对数据库进行读和写的操作,并发就是,多个事务同时对数据库进行读和写操作。而数据库正是对在效率和安全方面的考虑,解决读和写并发的问题。
3.1 数据库并发的三种场景
- 读和读:不会存在任何问题,不需要并发的控制。不会修改数据。
- 写和写:有安全问题,可能会存在数据更新丢失,比如:第一类更新丢失,第二类更新丢失。如事务A更新了数据,事务B回滚导致事务A更新的数据丢失了。需要加锁,串行运行。
- 读和写:有安全问题,但是为了效率的考虑,不一定加锁,串行运行。总和考虑效率和安全,所以有了隔离等级,但是,仍然可能有其它问题,脏读,幻读,不可重复读的问题。
读和读,写和写的问题好处理,但是读和写的问题就比较麻烦,下面主要讨论如何解决读和写并发问题。
3.2 读和写
多版本并发控制(Multiversion concurrency control),简称MVCC,是一种解决读写冲突的无锁并发控制。
主要是:为事务分配单向增长的事务ID,为每一个修改保存一个版本,版本与事务ID关联,读操作只读改事务开始前的数据的快照(快照,后面有解释,需要先理解,才能理解)。所以MVCC可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时,不用阻塞写操作,写操作时也不用阻塞读操作,因为读写的数据不是一个版本。提高了数据库并发读写的性能。
- 同时还解决了脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
注意:没有同时启动的事务,事务的启动一定会有先后的。所以每个事务的ID一定不同。
想要理解MVCC需要知道三个前提知识:
- 3个记录隐藏字段
- undo日志
- read view
3.2.1 三个隐藏字段
当我们在数据库中创建一个表时,表中的字段不仅会有我们设计的字段,还有有几个隐藏字段。
这里主要介绍三个:
- DB_TRX_ID:6字节,保存最近修改(修改/插入)改数据的事务的ID
- DB_ROLL_PTR:7字节,回滚指针,执行这条记录的上一个版本。
- DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)。在innodb存储引擎中,如果数据没有主键,innodb会自动创建一个隐藏主键来构建索引。这个隐藏主键就是DB_ROW_ID字段记录的主键。
补充:实际上还会有一个删除flag隐藏字段,即记录删除。实际上的删除一个字段,并不是真正的删除,而是将flag设置为删除标记。
演示:
此时:表的全部信息为:
我们目前并不知道创建该记录的事务ID,隐式主键。默认设置成NULL和1。第一条记录之前也没有其它版本,我们设置回滚指针为NULL。
3.2.2 undo日志
undo日志,实际上时MySQL内存缓冲区里的一段空间,即buffer pool中的一段空间,用来保存日志数据,之后再刷新到磁盘中。
MySQL是一个服务进程,所有操作都需要在内存中完成的。比如:修改数据,先将数据拿到内存中,修改后,再刷新到磁盘中。日志也是一样,先将日志信息写到MySQL内部缓冲区中,之后再刷新到磁盘中。
3.2.3 模拟MVCC过程
说明:现在有一个事务,事务ID为10,对student表中的字段做的修改(update),将name从张三,修改成了李四。
过程:
- 事务10,因为要修改字段,先给记录加上行锁。防止有其它记录同时修改。
- 修改前,先将当前记录拷贝到undo log中。
- 所以现在MySQL中有了两行相同的记录。现在修改原始记录,将名字改为李四。
- 修改隐藏字段DB_TRX_ID为当前事务10的事务ID,
- 修改回滚指针DB_ROLL_PTR,里面填入之前拷贝数据的起始地址,从而指向副本数据,表明这是上一个版本。
- 事务10提交,释放锁。
现在又有一个事务11,对student表进行修改(update):将年龄age由28改成38。
注意:修改,修改的是最新记录。
- 事务11,因为要修改,给最新记录加上行锁。
- 修改前,将当前记录拷贝到undo log中,产生新的副本,我们采用头插的方式,插入到undo log中。
- 现在修改原始记录中的年龄,改成38。
- 修改隐藏字段DB_TRX_ID为当前事务ID11.
- 修改回滚指针DB_ROLL_PTR列,保存undo log中新副本的起始地址,从而指向副本记录,表示上一个版本就是它。
- 事务11提交,释放锁。
于是这样就是形成了一个基于链表记录的版本链。所谓回滚,就是,用历史数据,覆盖当前数据。
快照:就是undo log中的一个个版本。
注意:只有在增删改动作的时候,才会形成快照。
上面主要讲的是修改数据undo log
如果是delete呢?删除数据了,这个字段就没有了,如何生成这个字段的快照呢?
上面补充的时候说了,在表中还有一个隐藏字段flag,来标记当前字段是否被删除。所以删除数据,并不是将数据真正删除了,而是将flag字段,设置成了删除标记。所以,删除字段也可以生成快照,保存在undo log中。
如果是insert呢?插入数据,之前没有插入数据前面没有历史版本,如果要回滚到插入前怎么办的?
起始在MySQL中由记录数据的log,就像上面一样。还由基于语句的log,记录相反的语句。比如:insert一条数据,在log中就会记录delete的语句。
如果是select呢?
select不会对数据进行修改,所以不需要为select维护多版本。
select读取的时候,是读取的最新数据还是,快照呢?
select既可以读取最新数据,也可以读取快照。
当前读:读取最新数据。增删改,都是当前读,因为需要修改最新的数据。select也可以当前读,(select lock in share mode,select for update),select当前读时,就需要对数据加锁,因为增删改也是当前读,避免数据错误。
快照读:读取快照。增删改是当前读,如果select采用快照读,这样不需要加锁,可以实现读写并发,这就是MVCC的意义所在。
那什么决定了select快照读还是当前读?
隔离级别决定的。不同的隔离级别,访问的数据版本不同。
比如:读提交:每次都可以访问到修改后的数据,当前读。读未提交:访问不到需改,但是未提交的数据,快照读。
事务操作哪个版本是在事务启动时确定的,事务总有先后。事务启动时,会分配一个事务id,通过对比事务id来确定操作哪个快照。
3.2.4 read view
Read view是事务进行快照读操作的时候生产的读视图(read view)。决定了这个select可以看到多少版本数据。这个读视图中,维护了系统当前活跃事务的id。
简单来说就是,在我们某个事务进行快照读时,对该记录创建一个read view,把他当作条件,用来判断当前事务能够看到哪个版本的数据。即,可能时最新版本,也可能是undo log中的版本。
在read view中主要由以下四个成员:
- m_ids:相当于一个数组。记录read view生成时刻,系统正在执行的事务。
- up_limit_id:记录m_ids列表中事务最小的ID。
- low_limit_id:read view生成时刻,系统尚未分配的下一个事务ID。
- creator_trx_id:创建read view的事务id。
我们读取数据版本链时,能够获得每一个版本对应的事务ID。即:DB_TRX_ID。
我们只需要拿read_view和DB_TRX_ID对比,就可以知道可以读取的id。
最终根据隔离级别,可以获得要读取的id。
-
Creator_trx_id == DB_TRX_ID || DB_TRX_ID < up_limit_id
在形成read view前已经提交的事务,形成的版本链数据,可以看到。
-
Up_limit_id <= DB_TRX_ID < low_limit_id
在形成read view时,正在执行的事务id保存在m_ids中。不在m_id中说明在形成快照前提交了,但是仍然在up_limit_id和low_limit_id范围内。
注意:m_ids中的事务id不一定是连续的,可能由中间的事务ID已经提交了。比如:我们有11,12,13,14,15号事务。在形成快照前12,14号事务提交了。形成快照后,m_ids中保存的就是11,13,15。up_limit_id等于11,low_limit_id等于16。
此时如果DB_TRX_ID没有m_ids中,说明已经提交(commit),可以被查询。
如果DB_TRX_ID在m_ids中,说明还没有被提交,不可以被查询,
看下面源代码就懂了。并且可重复读和读提交,是因为read view不同,所以呈现的现象不同。下面有详细介绍。
-
DB_TRX_ID >= low_limit_id
说明是形成read view后,形成的事务,对数据进行了修改。插入到了版本链中。
比如:此时有11,12,13,14,15号事务,形成read view。up_limit_id等于11,low_limit_id等于16。之后,又开始了一个事务16,并且修改了数据,所以版本链中会有大于low_limit_id的版本数据。
这种数据不能被查看。
对应源码策略:
- id就是版本链中的DB_TRX_ID
- 当Up_limit_id <= DB_TRX_ID < low_limit_id,只要m_ids中没有id,就可以被查询。
注意:
- read veiw的作用就是用来判断版本链中的数据是否可以看到。
- 从版本链第一个结点开始查看,如果查到不应该看到的版本,就遍历下一个版本,直到可以查看。
- read view是进行可见性判断的,即有了read view才直到哪些版本链的版本可见,哪些版本链的版本不可见。
- read view是在select时,自动生成的。
MVCC流程演示,方便理解:
假设当前有一条记录:
事务操作:
事务1(id = 1) 事务2(id = 2) 事务3(id = 3) 事务4(id = 4) 事务开始 事务开始 事务开始 事务开始 ... ... ... 修改并提交 进行中 快照读 进行中 ... ... ... 事务修改了name,将name有张三修改成了李四。
当事务2对某行数据进行快照读,数据库为改行数据形成移和read view读视图。
该read view中保存的数据为:
m_id2 = {1,3};
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
版本链为:
只有事务4修改了数据,并在事务2快照读前,提交了事务。
事务2拿着版本链的DB_TRX_ID去跟up_limit_id和low_limit_id和活跃事务ID列表进行比较,判断当前版本是否可以查看。
此时
该read view中保存的数据为:
m_id2 = {1,3};
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
DB_TRX_ID = 4
比较步骤:
- DB_TRX_ID(4) < up_limit_id(1) 不小于,判断下一个条件。如果小于可以查看。
-
DB_TRX_ID(4) >= low_limit_id(5) ? 不大于,判断写一个条件。如果大于,不可以查看,遍历版本链中的下一个结点。
-
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务 4 不在当前的活跃事务中。此时就需要隔离级别来决定是否可以查看。隔离级别为读提交,可以查看,可重复读,不可以查看。
四.RR和RC的本质区别
RR是可重复度,RC是读提交。
对于表:
设置当前会话的隔离级别为可重复读
介绍一个当前读语法:
select * from 表名 lock in share mode案例1:
事务A操作 事务A描述 事务B描述 事务B操作 begin 开启事务 开启事务 begin select * from student 快照读 快照读 select * from student update student set age=30 where name='张三' 更新名字为张三的年龄为30 commit 提交事务 select没有读到张三的年龄为30 select * from student select可以读到张三的年龄为30 select * from student lock in share mode 案例2:
事务A操作 事务A描述 事务B描述 事务B操作 begin 开启事务 开启事务 begin select * from student 快照读,查到年龄为28 update student set age=30 where name='张三' 更新名字为张三的年龄为30 commit 提交事务 select可以读到张三的年龄为30 select * from student select可以读到张三的年龄为30 select * from student lock in share mode 两测试用例的区别:
- 案例1在事务A提交和修改前,快照读了一次。
- 案例2在事务提交和修改前,没有快照读。
结论:
针对隔离级别是可重复读(repeatable read)
-
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。即,此时版本链中有多少结点。
可重复读和读提交本质区别:首先强调:源代码中,如果Up_limit_id <= DB_TRX_ID < low_limit_id,只要不在m_ids中就可以被查询。read view是在快照读的时候才形成的。- 在RR下,某个事务,只会对某条记录的第一次快照读会创建一个快照及Read view。
- 之后在调用快照读时,还是使用的同一个read view,对形成read view之后的的修改是不可见的。
- 在RC下,事务中,每一次快照读都会新生成一个快照和read view,这就是在RC级别下的事务中可以看到别的事务提交的更新的原因。
- RR和RC区别是因为read view的不同,RR只在第一次形成一个read veiw,并且之后都只会看这一个,而RC每一次快照读都会形成read view。
-
事务的传播机制
2022-04-05 12:13:46事务的传播行为目录
事务的传播机制有7种,如下图所示:
1.形象说明:
为了更好的理解,下面我们形象的说明一下几种传播机制是什么意思:
比如你下班回家,有以下几种场景
0.REQUIRED ------ 就是如果老婆做饭了,你就吃老婆做的饭;如果老婆没有做饭,你就自己做饭吃,反正你就是要吃饭(反正要在事务中运行);
1.SUPPORTS ------ 如果老婆做饭了,你就吃老婆做的饭;如果老婆没有做饭,你就不吃(不一定非要在事务中运行);
2.MANDATORY ------ 非要吃老婆做的饭,老婆要是没有做饭,你就大发脾气,典型的家暴男;
3.REQUIRES_NEW ------ 劳资非要吃自己做的饭,就算老婆把饭做好了,你也不吃老婆做的;
4.NOT_SUPPORTED ------ 劳资就是不吃饭,就算老婆把饭做好了,我也不吃;
5.NEVER ------ 劳资就是不吃饭,如果老婆把饭做好了,我还要发脾气;
6.NESTED ------ 暂不做解释,后面会详解;
本文主要是想用代码实现这几种传播机制的具体使用;
2.代码演示:
有两张表:
school表
student表
我们如何去测试两个方法是否使用的同一个事务呢?就看是否共用同一个数据库连接或者共用同一个会话;
2.1 REQUIRED
支持使用当前事务,如果当前事务不存在,创建一个新事务。
2.1.1 验证共用一个事务
schoolService.updateSchool()方法:
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); //更新id为1的学校名称为湖南大学,地址为湖南 schoolMapper.updateByPrimaryKeySelective(school); //调用另一个方法更新学生信息 studentService.updateStudent(); System.out.println(1/0); }
studentService.updateStudent()方法,加了事务的,默认REQUIRED:
@Transactional public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); //更新sid为1的学生年龄为25 studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法是加了事务的,调用完studentService.updateStudent方法后,会报错,如果updateSchool和updateStudent共用同一个事务,updateSchool报错,自身回滚,肯定会带着updateStudent一起回滚;如果不是共用同一个事务,那么updateStudent会执行成功并提交,不会回滚;
结果:
updateSchool成功回滚了
updateStudent也成功回滚了
看过我写的深入理解@Transactional注解的使用和原理就知道,两个方法会共用同一个数据库连接,也就共用同一个事务,两个方法一起提交或者回滚;
2.1.2 验证当前没有事务,就新建一个事务
schoolService.updateSchool()方法:
public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); //更新id为1的学校名称为湖南大学,地址为湖南 schoolMapper.updateByPrimaryKeySelective(school); //调用另一个方法更新学生信息 studentService.updateStudent(); }
studentService.updateStudent()方法:
@Transactional public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); //更新sid为1的学生年龄为25 studentMapper.updateByPrimaryKeySelective(student); //报错 System.out.println(1/0); }
如上所示:updateSchool方法是没有加事务的,调用studentService.updateStudent方法,updateStudent方法加了事务,并且会报错,如果updateStudent没有新建事务的话,不会回滚,如果是建了事务,就会回滚;
结果:
1.控制台提示报错
2. updateSchool方法没有回滚(将北京大学刚改为湖南大学)
3.updateStudent方法回滚了(将age更新为25)
验证通过;
2.2 SUPPORTS
支持使用当前事务,如果当前事务不存在,则不使用事务。
2.2.1 支持使用当前事务
updateSchool方法,加事务,更新id为1的name为“湖南大学”,location为“湖南”,输出1/0,肯定会报错的;
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); System.out.println(1/0); }
updateStudent方法更新id为1的age为25;
@Transactional(propagation = Propagation.SUPPORTS) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法是加了事务的,studentService.updateStudent方法也加了事务,而且传播机制是SUPPORTS,如果updateSchool和updateStudent共用同一个事务,updateSchool报错,自身回滚,肯定会带着updateStudent一起回滚;如果不是共用同一个事务,那么updateStudent会执行成功并提交,不会回滚;
结果:
1.控制台提示报错
2. updateSchool方法回滚(将北京大学刚改为湖南大学)
3.updateStudent方法回滚(将age改为25)
显然,updateSchool和updateSchool共用同一个事务,正好验证了传播机制为supports,如果当前有事务,就支持使用当前事务;
2.2.2 如果当前事务不存在,则不使用事务
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,将@Transactional注释掉了,没有加事务
//@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为SUPPORTS,打印1/0,将报错
@Transactional(propagation = Propagation.SUPPORTS) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); System.out.println(1/0); }
如上所示:updateSchool方法没有加事务,studentService.updateStudent方法加了事务,而且传播机制是SUPPORTS,如果updateStudent没有事务,报错就不会回滚,如果有事务,就会回滚;
结果:
1.控制台报错
2.updateSchool方法没有回滚,无事务执行(无事务这个说法其实不准确,任何操作数据库肯定是有事务的);
3.updateStudent方法也没有回滚,也是无事务执行的;
显然: 事务传播机制为SUPPORTS的方法,支持使用当前事务,如果当前事务不存在,则不使用事务。
2.3 MANDATORY
中文翻译为强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。
2.3.1 支持使用当前事务
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,打印1/0,会报错;
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); System.out.println(1/0); }
updateStudent方法更新id为1的age为25,事务传播机制为MANDATORY
@Transactional(propagation = Propagation.MANDATORY) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法加事务,studentService.updateStudent方法加了事务,而且传播机制是MANDATORY,如果两者共用一个事务,都会回滚;
结果:
1.控制台报错
2. updateSchool方法回滚(将北京大学刚改为湖南大学)
3.updateStudent方法回滚(将age改为25)
显然: 传播机制为MANDATORY,如果当前有事务,就使用当前事务;
2.3.2 如果当前事务不存在,则抛出Exception
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,没有事务;
//@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为MANDATORY;
@Transactional(propagation = Propagation.MANDATORY) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法没有加事务,studentService.updateStudent方法加了事务,而且传播机制是MANDATORY,就看执行到updateStudent就不会报错;
结果:
1.控制台报错
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
2.updateSchool方法没有回滚(将北京大学刚改为湖南大学)
3.pdateStudent方法直接报错: No existing transaction found for transaction marked with propagation 'mandatory',传播机制为mandatory的,必须有当前事务存在,不存在就报错;
综上:传播机制为MANDATORY,支持使用当前事务,如果当前事务不存在,则抛出Exception。
2.4 REQUIRES_NEW
新建一个新事务;如果当前事务存在,把当前事务挂起。
2.4.1 当前事务不存在,创建一个新事务
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,没有事务;
//@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为REQUIRES_NEW,打印1/0,报错;
@Transactional(propagation = Propagation.MANDATORY) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); System.out.println(1/0); }
如上所示:updateSchool方法没有加事务,studentService.updateStudent方法加了事务,而且传播机制是REQUIRES_NEW,如果updateStudent方法新建了事务,打印1/0,报错就会回滚,如果没有新建事务,updateStudent方法就不会回滚;
结果:
1.控制台报错
2.updateSchool方法没有回滚,无事务执行(无事务这个说法其实不准确,任何操作数据库肯定是有事务的);
3. updateStudent方法回滚(将age改为25)
显然:传播机制为REQUIRES_NEW的,当前没有事务,就新建一个事务,在事务中运行;
2.4.2 新建一个事务,当前事务存在,把当前事务挂起
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,有事务,打印1/0,报错;
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); System.out.println(1/0); }
updateStudent方法更新id为1的age为25,事务传播机制为REQUIRES_NEW;
@Transactional(propagation = Propagation.REQUIRES_NEW) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法有加事务,studentService.updateStudent方法加了事务,而且传播机制是REQUIRES_NEW,如果updateStudent方法新建了另一个事务,updateSchool报错,updateStudent不会回滚,如果不是新建一个事务而是共用一个事务,就会一起回滚;
结果:
1.控制台报错
2.updateSchool方法回滚(将北京大学刚改为湖南大学)
3. updateStudent没有回滚(将age改为25)
显然:updateStudent新建了另外一个事务,和updateSchool并不是共用一个事务;
总结:传播机制是REQUIRES_NEW,新建一个新事务;如果当前事务存在,把当前事务挂起。
使用场景:被调用方法,不想因为调用方出错而回滚,可以使用REQUIRES_NEW;
2.5 NOT_SUPPORTED
无事务执行,如果当前事务存在,把当前事务挂起。
2.5.1 无事务执行
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,没有事务,打印1/0,报错;
//@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为NOT_SUPPORTED;
@Transactional(propagation = Propagation.NOT_SUPPORTED) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); System.out.println(1/0); }
如上所示:updateSchool方法没有事务,studentService.updateStudent方法加了事务,而且传播机制是NOT_SUPPORTED,如果updateStudent没有事务,那么在执行打印1/0报错后,也就不会回滚;
结果:
1.控制台报错
2.updateSchool方法不回滚(将北京大学刚改为湖南大学)
3. updateStudent没有回滚(将age改为25)
显然:两个方法都没有在事务中运行,都没有回滚,所以,如果当前没有事务,NOT_SUPPORTED并不会新建一个事务,也是无事务执行;
2.5.2 如果当前事务存在,把当前事务挂起。
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,有事务;
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为NOT_SUPPORTED,打印1/0,报错;
@Transactional(propagation = Propagation.NOT_SUPPORTED) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); System.out.println(1/0); }
如上所示:updateSchool方法有事务,studentService.updateStudent方法加了事务,而且传播机制是NOT_SUPPORTED,如果updateStudent没有事务,那么在执行打印1/0报错后,也就不会回滚,而updateSchool检测到报错,如果当前事务有效,updateSchool就会回滚;
结果:
1.控制台报错
2.updateSchool方法回滚(将北京大学刚改为湖南大学)
3. updateStudent没有回滚(将age改为25)
显然:当前有事务的情况下,传播机制为NOT_SUPPORTED的方法无事务运行;
总结:事务传播机制为NOT_SUPPORTED,无事务执行,如果当前存在事务,把当前事务挂起;
使用场景:被调用方法想无事务运行,但又不影响调用方的事务,可以用NOT_SUPPORTED;
2.6 NEVER
无事务执行,如果当前有事务则抛出Exception。
这个和MANDATORY就是两个完全相反的极端,一个强制不要事务,一个强制要事务,不满足都会报错;
2.6.1 无事务执行
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,没有事务;
//@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为NEVER,打印1/0,报错;
@Transactional(propagation = Propagation.NEVER) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); System.out.println(1/0); }
如上所示:updateSchool方法没有事务,studentService.updateStudent方法加了事务,而且传播机制是NEVER,打印1/0,会报错,如果updateStudent是无事务执行,那么就不会回滚
结果:
1.控制台报错
2.updateSchool方法不回滚(将北京大学刚改为湖南大学)
3.updateStudent没有回滚(将age改为25)
显然:updateSchool和updateStudent都没有回滚,都是无事务执行,所以,传播机制为NEVER,如果当前没有事务,则无事务执行;
2.6.2 如果当前有事务则抛出Exception
updateSchool方法更新id为1的name为“湖南大学”,location为“湖南”,有事务;
@Transactional public void updateSchool(){ School school = new School(); school.setId(1); school.setName("湖南大学"); school.setLocation("湖南"); schoolMapper.updateByPrimaryKeySelective(school); studentService.updateStudent(); }
updateStudent方法更新id为1的age为25,事务传播机制为NEVER;
@Transactional(propagation = Propagation.NEVER) public void updateStudent(){ Student student = new Student(); student.setSid(1); student.setAge(25); studentMapper.updateByPrimaryKeySelective(student); }
如上所示:updateSchool方法有事务,studentService.updateStudent方法加了事务,而且传播机制是NEVER,当前有事务,就看执行updateStudent方法时到底报不报错;
结果:
1.控制台报错
2.updateSchool方法回滚(将北京大学刚改为湖南大学)
3. updateStudent直接报错
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
显然:传播机制为NEVER,如果当前有事务,则报错;
总结:这种传播机制感觉没啥用途,哈哈,反正我是基本没用过;
2.7 NESTED
嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。
这个直接说,如果父事务回滚,子事务也会跟着回滚;如果子事务回滚,并抛出异常,父事务肯定会跟着回滚;
如果当前没有事务,就和REQUIRED,新建一个事务运行;
关于@Transactional注解的原理和事务传播机制的原理可以看我上一篇文章:
-
七种常见分布式事务详解(2PC、3PC、TCC、Saga、本地事务表、MQ事务消息、最大努力通知)
2022-01-10 00:19:28在分布式系统中一次操作需要由多个服务协同完成,这种由不同的服务之间通过网络协同完成的事务称为分布式事务。本文详解介绍七种常见分布式事务的原理以及优缺点和适用场景(2PC、3PC、TCC、Saga、本地事务表、MQ... -
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
2021-09-04 22:08:00---- 推荐访问权限问题方法用final修饰未被spring管理多线程调用表不支持事务未开启事务事务不回滚错误的传播特性自己吞了异常手动抛了别的异常自定义了回滚异常嵌套事务回滚多了其他常见问题编程式事务大事务问题 ... -
分布式事务:seata
2022-01-15 22:19:43目录 一、事务简介 二、本地事务 三、常见的分布式事务解决方案 分布式事务理论基础 两阶段提交协议 两阶段提交协议的问题 AT模式(Auto Transaction) TCC模式 四、Seata Seata是什么 Seata的三大角色 设计思路 ... -
事务 跨库事务 分布式事务及解决方案
2019-08-29 05:42:46什么是事务? 本地事务 事务传播行为: 事务的四大特性 ACID 并发事务产生的问题可以分为4类 事务的隔离级别 什么是分布式事务 分布式涉及到的原理: CAP原理: BASE理论 柔性事务和刚性事务 柔性事务分为:... -
数据库事务的简单介绍
2022-04-12 22:07:09事务和锁 一、什么是事务 事务是逻辑上的一组数据库操作,要么都执行,要么都不执行。 例子:假如张三要给李四转账200元,这个转账会涉及到两个关键操作就是:将张三的余额减少200元,将李四的余额增加200元。如果... -
【MySQL】InnoDB解决事务并发问题
2022-01-15 10:48:59我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时... -
数据库事务的概念及其实现原理
2021-07-02 10:59:051. 认识事务 1.1 为什么需要数据库事务 1.2 什么是数据库事务 1.3 事务如何解决问题 1.4 事务的ACID特性以及实现原理概述 2.并发异常与并发控制技术 2.1 常见的并发异常 2.2 事务的隔离级别 2.3 ... -
Redis事务详解
2021-06-29 13:40:05本文来说下redis事务相关的话题。 文章目录Redis事务命令 Redis事务命令 官网:https://redis.io/commands#transactions -
Redis - 7、事务操作
2022-06-21 16:05:43redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 redis事务的主要作用就是串联多个命令防止 别的命令插队。... -
详解事务的7种传播行为
2021-04-11 16:52:36什么是Spring事务的传播行为?事务传播行为的七种类型有哪些?将异常try-catch捕获,事务是否还会回滚? -
聊聊spring事务失效的12种场景,太坑了
2021-09-04 13:29:15对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,... -
分布式事务七种解决方案,最后一种经典了!
2021-11-14 00:57:17随着业务的快速发展、业务复杂度越来越高,几乎每个公司的系统都会从单体走向分布式,特别是转向微服务架构。随之而来就必然遇到分布式事务这个难题,这篇文章总结了分布式事务最经典的解决方案,分享给... -
MySQL事务和锁
2022-05-03 22:13:04MySQL的事务和锁:事务四大特性、事务并发的三个问题,InnoDB锁 -
什么是事务和事务隔离级别
2021-05-07 10:45:22什么是事务 事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换为另一种状态。 事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性... -
分布式事务最全解决方案
2022-04-10 17:58:151.分布式事务理论依据(讨论的前提) 1.1本地事务、分布式事务 如果说本地事务是解决单个数据源上的数据操作的一致性问题的话,那么分布式事务则是为了解决跨越多个数据源上数据操作的一致性问题。 1.2 强一致性、弱... -
Spring事务
2020-09-28 18:56:52一、什么是事务 1.1 事务的概念 事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合... -
Spring源码之事务处理
2022-05-05 15:31:54spring 事务处理的源码阅读 -
Java--SpringBoot使用@Transactional注解添加事务
2021-08-10 21:41:17一、Java事务 1、通常的观念认为,事务仅与数据库相关。 事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性 (isolation)和持久性(durability)的缩写。 事务的... -
Spring之事务传播行为
2020-05-10 23:07:06事务传播行为主要用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的事务中,该事务如何传播。这个概述可能不好理解,换句话就是当一个事务方法被另一个事务方法调用时,这个事务方法应 -
MySQL 事务隔离级别
2021-09-07 08:35:33MySQL 中事务的隔离级别一共分为四种,分别如下: 序列化(SERIALIZABLE) 可重复读(REPEATABLE READ) 提交读(READ COMMITTED) 未提交读(READ UNCOMMITTED) 四种不同的隔离级别含义分别... -
31 Redis 的事务机制
2021-12-12 12:45:0431 Redis 的事务机制前言一、事务 ACID 属性的要求二、 Redis 如何实现事务?三、Redis 的事务机制能保证哪些属性?原子性一致性隔离性持久性总结 前言 事务在执行时会提供专门的属性保证,包括原子性(Atomicity... -
MySQL事务
2021-06-27 20:06:29三、事务之间的相互影响四、MySQL及事务隔离级别1、查询全局事务隔离级别2、查询会话事务隔离级别3、设置全局事务隔离级别4、设置会话事务隔离级别 五、事务控制语句1、测试提交事务2、测试回滚事务3、测试多点... -
分布式事务解决方案全解析
2021-01-22 16:45:37对于刚刚接触分布式系统的伙伴来说,分布式看起来非常...目前已有Dubbo、SpringCloud等较好的分布式框架,但分布式事务仍是分布式系统一大痛点,本文结合一些经典博客文章,简单解析一些常见的分布式事务解决方案。 -
SQL事务
2019-09-11 10:30:12了解事务和锁 事务:保持逻辑数据一致性与可恢复性,必不可少的利器。 锁:多用户访问同一数据库资源时,对访问的先后次序权限管理的一种机制,没有他事务或许将会一塌糊涂,不能保证数据的安全正确读写。 死锁:...