持续学习&持续更新中…
守破离
数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily),简称就是 ACID;
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常, 我们可以很容易的整体回滚;
比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败,一个事务开始,代表以下的所有操作都在同一个连接里面;
READ UNCOMMITTED(读未提交) :该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
READ COMMITTED(读已提交) :一个事务可以读取另一个已提交的事务。Oracle和SQL Server的默认隔离级别。
同一个事务中,该隔离级别下,同样的 select 多次读取会读出不一样的结果,此现象称为不可重复读问题
REPEATABLE READ(可重复读) :该隔离级别是 MySQL 默认的隔离级别 :在同一个事务里,同样的 select 操作,select到的结果始终是事务开始时时间点的状态。 因此,同样的 select 操作在该事务中读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引 擎可以通过 next-key locks 机制来避免幻读。
SERIALIZABLE(序列化) :在该隔离级别下事务都是串行顺序执行的,MySQL数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务, 就加入该事务,该设置是最常用的设置。
PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
1)、引入aop-starter:spring-boot-starter-aop;引入了aspectj
org.springframework.boot
spring-boot-starter-aop
2)、@EnableAspectJAutoProxy(exposeProxy = true);开启 aspectj 动态代理功能。
以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。
exposeProxy = true:对外暴露代理对象
3)、本类互调用代理对象调
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
//事务的隔离级别(isolation = Isolation.REPEATABLE_READ)
//REQUIRED、REQUIRES_NEW
//坑:同一个对象内,事务方法互调,他们的事务设置默认失效,原因:事务使用代理对象来控制的,直接调用绕过了代理对象
@Transactional(timeout = 30)
public void a() {
//使用this调用,b,c做任何设置都没用。都是和a公用一个事务
// this.b(); 没用
// this.c(); 没用
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b(); //a事务。应用到了自己的事务设置REQUIRED,如果是REQUIRED,那么自己的事务设置不起作用,和a()共用
orderService.c(); //新事务。应用到了自己的事务设置REQUIRES_NEW, timeout = 20 ,自己的事务设置起作用
// bService.b(); //a事务
// cService.c(); //新事务
int i = 10 / 0;
}
@Transactional(propagation = Propagation.REQUIRED, timeout = 2)
public void b() {
//执行了7s,回滚不
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)
public void c() {
}
//本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚
//应该使用分布式事务,但是分布式事务比较复杂,比较复杂的最大原因:网络问题+分布式机器。
// @GlobalTransactional //高并发
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.setCode(0);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
response.setCode(1);
return response;
} else {
//令牌验证成功 //下单:去创建订单,验令牌,验价格,锁库存...
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
if (Math.abs(order.getOrder().getPayAmount().subtract(vo.getPayPrice()).doubleValue()) < 0.01) { //金额对比
// 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 库存锁定需要的数据:订单号,所有订单项(skuId,skuName,num)
//4、远程锁库存
R r = wareFeignService.orderLockStock(getLockVo(order));
//TODO 问题1:库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,此时:订单回滚,库存不会回滚。
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 问题2:假如还有个远程扣减积分服务
// 该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。
int i = 10/0; //模拟扣减积分出异常
//TODO 清除购物车已经下单的商品
return response;
} else {
//锁定失败
response.setCode(3);
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
CAP 原则又称 CAP 定理,指的是在一个分布式系统中
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们, 剩下的 C 和 A 无法同时做到。
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所 以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证 P 和 A,舍弃 C。
http://thesecretlivesofdata.com/raft/
https://raft.github.io/
raft算法核心:领导选举、日志复制
领导选举、日志复制这两个动作的过程中有两个时间在起作用:自旋时间 (随机的,也叫选举时间)、心跳时间(固定的,指定的时间间隔)
raft算法下的节点有三个状态:随从、候选者 、领导
raft算法下对系统的所有更改都要经过领导者
Raft 可以在面对网络分区时保持一致。
BASE 理论是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以采用适当的弱一致性,即最终一致性。
BASE 是指
基本可用(Basically Available)
软状态( Soft State)
最终一致性( Eventual Consistency)
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
如果能容忍后续的部分或者全部访问不到,则是弱一致性。
如果经过一段时间后要求 能访问到更新后的数据,则是最终一致性
数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。
MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。
其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。
XA的特点:
刚性事务
XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较 低。
XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景
XA 目前在商业数据库支持的比较理想,在 mysql 数据库中支持的不太理想,mysql 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。
许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。
也有 3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间 未收到回应则做出相应处理)
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
一段业务代码分为三个方法: try(prepare)、confirm(commit)、 cancel(rollback)
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。
这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。
这种 方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调
发送端防止消息丢失:
消费者:
https://seata.apache.org/zh-cn/docs/overview/terminology/
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
https://seata.apache.org/zh-cn/docs/user/quickstart/
每一个微服务先必须创建 undo_log 表;
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
导入依赖 spring-cloud-starter-alibaba-seata ;
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
安装事务协调器;seata-server(seata-all-0.7.1,所以该项目使用seata-server-0.7.1.zip): https://github.com/seata/seata/releases
解压seata-server,修改注册中心配置registry.conf,并启动seata-server
registry {
type = "nacos"
nacos {
serverAddr = "localhost:8848"
...
}
}
所有想要用到分布式事务的微服务使用seata的DataSourceProxy代理自己的数据源
@Configuration
public class MySeataConfig {
@Autowired
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);
}
}
每个微服务,都必须把seata-server-0.7.1/conf目录下的registry.conf、file.conf这两个文件拷贝到自己的resources目录下,然后修改自己微服务下的file.conf:
vgroup_mapping.{application.name}-fescar-service-group = "default"
比如订单服务:
vgroup_mapping.gulimall-order-fescar-service-group = "default"
给分布式大事务的入口标注@GlobalTransactional
,,每一个远程的小事务用@Transactional
@GlobalTransactional
// @Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
// 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 库存锁定需要的数据:订单号,所有订单项(skuId,skuName,num)
//4、远程锁库存
R r = wareFeignService.orderLockStock(getLockVo(order));
//TODO 问题1:库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,此时:订单回滚,库存不会回滚。
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 问题2:假如还有个远程扣减积分服务
// 该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。
int i = 10/0; //模拟扣减积分出异常
}
}
/**
* 为某个订单锁定库存
*
* @Transactional(rollbackFor = NoStockException.class)
* 默认只要是运行时异常都会回滚
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
//TODO 按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
启动测试分布式事务
雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!