目录
商城业务-分布式事务-本地事务在分布式下的问题
商城业务-分布式事务-本地事务隔离级别&传播行为等复习
商城业务-分布式事务-分布式CAP&Raft原理
商城业务-分布式事务-BASE
商城业务-分布式事务-分布式事务常见解决方案
商城业务-分布式事务-Seata&环境准备
商城业务-分布式事务-Seata分布式事务体验
商城业务-分布式事务-最终一致性库存解锁逻辑
本地事务会失效不回滚的两种情况:
①锁库存假失败,由于网络原因导致连接超时,但是锁库存已经操作成功。此时,订单数据回滚而锁库存数据没有回滚。
②其它远程服务调用失败,订单数据回滚,但是已经执行成功的远程服务调用的数据库数据无法回滚
一、事务的特性
原子性:一系列操作整体不可拆分,要么全做,要么全不做
一致性:数据在事务的前后,业务整体一致
例如:转账 A:1000 B:1000 转200 事务成功; A:800 B:1200
隔离性:事务与事务之间互相隔离
持久性:一旦事务成功,数据一定会落盘在数据库
二、 事务的隔离级别
READ UNCOMMITED(读未提交):该隔离级别下的事务会读到别的事务未提交的数据,此现象被称之为脏读
READ COMMITED (读已提交):一个事务可以读取其它事务提交的数据,多次读取导致
前后读取的数据不一致,此现象称之为不可重复读。Oracle和SQL Server的默认隔离级别为读已提交
REPEATABLE READ (可重复度):在一个事务中读取数据前后不一致,产生的原因是有另外一个事务进行了insert操作,此现象称之为幻读。MySQL的默认隔离级别为可重复读。
SERIALIZABLE (序列化):在该隔离级别下事务都是串行顺序执行的,MySQL数据库的innoDB引擎会给读操作隐式加一把共享锁,从而避免了脏读、不可重复读和幻读问题。
三、事务的传播行为
1.PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新的事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
2.PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
3.PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
4.PROPAGATION_REQUIRES_NEW:创建新的事务,无论当前是否存在事务,都创建新的事务。
5.PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。
6.PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
7.PROPAGATION_NESTED:如果当前存在事务,则嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
其中:PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW是最常用的
案例一:
方法B()和方法A()共用一个事务,方法C则创建一个新事务,若出现异常则方法B()和方法A()会回滚,方法C()则不会
案例二:
方法B()设置了事务的超时时间,但是方法B()和方法A()共用方法A()的事务,因此,以方法A设置的超时时间为准。
SpringBoot事务的坑
事务失效的原因:绕过了代理
①未启用事务
@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,这个注解千万不要忘记写了
② 方法不是public类型的
@Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效
③数据源未配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
④自身调用问题
spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。
看下面代码,大家思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?
@Component
public class UserService {
public void m1(){
this.m2();
}
@Transactional
public void m2(){
//执行db操作
}
}
显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的,如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,@1在UserService中注入了自己,此时会产生更为严重的问题:循环依赖
@Component
public class UserService {
@Autowired //@1
private UserService userService;
public void m1() {
this.userService.m2();
}
@Transactional
public void m2() {
//执行db操作
}
}
⑤ 异常类型错误
spring事务回滚的机制:对业务方法进行try catch,当捕获到有指定的异常时,spring自动对事务进行回滚,那么问题来了,哪些异常spring会回滚事务呢?
并不是任何异常情况下,spring都会回滚事务,默认情况下,RuntimeException和Error的情况下,spring事务才会回滚。
也可以自定义回滚的异常类型(需继承RuntimeException):
@Transactional(rollbackFor = {异常类型列表})
⑥异常被吞了
当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。
如下代码,事务操作2发生了异常,但是被捕获了,此时事务并不会被回滚
@Transactional
public void m1(){
事务操作1
try{
事务操作2,内部抛出了异常
}catch(Exception e){
}
}
⑦业务和spring事务代码必须在一个线程中
spring事务实现中使用了ThreadLocal,ThreadLocal大家应该知道吧,可以实现同一个线程中数据共享,必须是同一个线程的时候,数据才可以共享,这就要求业务代码必须和spring事务的源码执行过程必须在一个线程中,才会受spring事务的控制,比如下面代码,方法内部的子线程内部执行的事务操作将不受m1方法上spring事务的控制,这个大家一定要注意
@Transactional
public void m1() {
new Thread() {
一系列事务操作
}.start();
}
解决方案:
本地事务失效的原因:同一个对象内事务方法互相调用默认失效,原因绕过了代理对象,事务使用代理对象来控制
解决:使用代理对象来调用事务方法
方法B()和方法C()的事务属性设置会失效,原因是绕过了代理,SpringBoot的事务是通过AOP代理实现的
解决事务失效的步骤:
1.引入aspectj依赖
org.springframework.boot
spring-boot-starter-aop
2. 开启aspectj动态代理功能,以后所有的动态代理都是aspectj创建的。通过设置exposeProxy暴露代理对象
3. 本类互调用对象
@Transactional(timeout = 30)
public void A(){
// B();
// C();
OrderServiceImpl service =(OrderServiceImpl)AopContext.currentProxy();
service.B();
service.C();
int i = 10/0;
}
@Transactional(propagation = Propagation.REQUIRED,timeout = 20)
public void B(){
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void C(){
}
分布式系统经常出现异常的原因:
机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失...
CAP定理:
一致性(Consitency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户的读写请求。(对数据更新具备高可用性)
分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另外一台服务器放在美国,这就叫两个区,它们之间可能无法通信。
CAP原则指的是,这三个要素最多只能同时实现两个点,不可能三者兼得。
一般来说,分区容错不可避免,因此,可以认为CAP中的P总是成立。CAP定理告诉我们,剩下的C和A无法同时做到。
分布式系统中实现一致性的算法raft算法
演示传送门:http://thesecretlivesofdata.com/raft/
Raft算法的原理说明:
首先,在Raft中一个节点有三种角色:①追随者(Follower)②候选人(Candidate)③领导者(Leader)
一开始,所有节点都是追随者状态,如果没有领导者给他们发信息,他们可以变成候选人,候选人将会给追随者发起选举,追随者们将会投票给候选人,如果候选人得到了大多数票则它将会成为领导者。这个过程被成为:领导选举。
追随者是如何成为候选人的呢?首先,节点有一个自旋超时时间(150ms-300ms),谁自旋结束的快谁就是候选者,候选人发起选举,如果节点在此轮选举中还没有投票,那么节点将会投票给它,一旦候选人收到大多数投票那么它将成为领导者。成为领导者之后则开始心跳联络,定期向节点发出我还在的消息,节点回复收到,这种状态直到领导者挂掉为止。
所有改变将需要听从领导者,假设客户端发来一条 SET 5 命令,首先,领导者会将这条命令保存到log中,然后会将 SET 5 命令发送给它的追随者,追随者们也是将命令保存至log中,领导者接收到大多数节点的回复--已经将这条命令写入log中了,此时,所有节点日志中的这条命令都是uncommited的。然后,领导会将这条命令commit并通知它的追随者让它们也去提交。这个过程被成为:日志复制。
日志复制过程在分区中的体现:由于网络原因,A、B被划分为1区,C、D、E被划分为2区,1区和2区之间不能通信,A原来是领导者所以在1区它还是领导者,2区经过多轮选举选出了新的领导者,现在有Client1给1区发 SET 10 的命令,A保存命令至日志然后通知B也保存日志,但是通知没有得到大多数节点的回复因此是uncommited的状态,Client2给2区发 SET 100 命令,2区领导者保存命令至日志,同时通知其它节点页保存命令至日志并且收到大多数节点的回复,2区领导者将会commit并会通知其它节点也去commit的。最终,1区和2区的通信回复了,由于2区的领导者是经过多轮选举选出的所以它成为了所以节点的领导者,原来1区的领导者就变成了追随者,1区A、B节点发现跟领导者的日志不一致,马上回滚日志并更新新的日志和提交,至此所有节点的数据是一致的。
CP面临的问题:
对于多数大型互联网应用场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.9999999(N个9),即保证P和A,舍弃C。舍弃C的含义是:保证数据的最终一致性而不是去追求强一致性。
一、2PC模式
2PC(2 phrase commit 二阶段提交),又叫做:XA Transactions
MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7 开始支持。
其中,XA 是一个二阶段提交协议,该协议分为以下两个阶段:
第一阶段:事务协调器要求每个涉及事务的数据库预提交(precommit)此操作,并反映是否可以提交。
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
2PC模式在高并发场景下的不太理想,分布式场景下并不会选择这种模式
二、柔性事务-TCC事务补偿型方案
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性。
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致性。
Try代码模块中需要Coder自己编写业务逻辑,Confirm代码块中会提交数据(例如:加2),那么在Cancel中则需要Coder编写回滚逻辑(例如:减2)
一阶段 Prepare 行为:调用自定义的 prepare 逻辑
二阶段 commit 行为:调用自定义的commit逻辑
二阶段 rollback 行为:调用自定义的rollback逻辑
所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
三、 柔性事务-最大努力通知型事务
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通信时,比如:调用微信或者支付宝支付后的支付结果。这种方案也是结合MQ进行实现,例如:通过MQ发送Http请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询核对、对账文件),支付宝的支付成功异步回调。
四、柔性事务-可靠消息+最终一致性方案(异步确保型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
Seata使用的是2PC的模式
Seata快速开始传送门:Seata 快速开始
首先,让我们了解一下Seata中的专业术语:
Seata的工作模式: 首先,TM会告诉TC全局事务开始了,由各个事务分支向TC汇报事务的状态,是成功还是回滚。如果有一个事务分支汇报回滚,则之前提交的事务都会回滚,回滚的依赖于Seata中的Magic表,用于记录提交之前的版本和数据。
开启Seata分布式事务的步骤:
1.为每一个微服务创建undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.导入Seata依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
seata-all
io.seata
io.seata
seata-spring-boot-starter
1.2.0
3.安装seata server V1.2.0
传送门:https://github.com/seata/seata/releases
4. 配置
将官方文档中V1.2.0的配置文件复制到conf,因为conf文件配置不全
传送门:seata/file.conf at 1.2.0 · seata/seata · GitHub
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.my_test_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
sagaBranchRegisterEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
namespace = ""
cluster = "default"
username = "naocs"
password = "naocs"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
将registry.conf中的type修改为nacos并修改serverAddr为本机注册中心地址
配置:
命名规则:服务名-fescar-service-group
,修改file.conf中的配置:file.conf和yml中的配置要一致
spring:
cloud:
alibaba:
seata:
tx-service-group: gulimall-order-fescar-service-group
spring:
cloud:
alibaba:
seata:
tx-service-group: gulimall-ware-fescar-service-group
其它服务同理
5.所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源
注意细节:高版本之后无需配置数据源,这步可忽略
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration
public class MySeataConfig {
@Autowired
private DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
6.给分布式事务的大入口标注@GlobalTransactional
7. 每一个远程的小事务@Transactional
回滚效果如下图所示:
seata 的AT模式并不适合于高并发场景,原因在于:加锁导致整个线程变成串行化执行,效率太低下了
seata的TCC模式、SAGA模式可以自行学习:
传送门:https://github.com/seata/seata-samples