本案例演示下单流程,使用本地事务远程调用库存服务,正常下单成功后,订单和库存的数据库表都做扣减操作,模拟下单时发生运行时异常后,订单数据库表做了回滚,但是库存数据库表没有做回滚操作,此处的 @Transactional
是本地事务,只能对自己服务的数据库回滚有效,需要分布式事务才可解决跨服务事务回滚问题。
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//将订单提交页面的数据放入此 threadLocal 中,用于共享数据
orderSubmitVoThreadLocal.set(vo);
//调用 ThreadLocal,获取同一线程的数据,得到当前登录的用户
MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();
//生成提交订单后返回的封装类
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
//没有抛出异常就设置状态码为0,代表成功
response.setCode(0);
//1、验证令牌【令牌的对比和删除必须保证原子性】
//对比防重删令牌,返回0和1,0代表令牌失败、1代表令牌删除成功
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();
//redis执行lua脚本,将页面输入令牌和redis中保存的令牌做对比,验证成功返回1,失败返回0
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0l){
//验证失败
response.setCode(1);
return response;
}else {
//1、令牌验证成功,解决重复提交订单成功问题,下单、创建订单、验令牌、验价格、锁库存
OrderCreateTo order = createOrder();
//2、验价,获取到应付金额
BigDecimal payAmount = order.getOrder().getPayAmount();
//获取页面提交的应付金额
BigDecimal payPrice = vo.getPayPrice();
//如果数据库订单里的应付金额和页面提交的金额差值小于0.01,就验证价格成功,因为是省略到小数点后两位,如果算相等的话,可能会出现 0.12和0.123456...
double value = Math.abs(payAmount.subtract(payPrice).doubleValue());
//如果验价成功
if (value < 0.01){
//TODO 三、保存订单
saveOrder(order);
// 四、库存锁定,stock_locked 锁定库存数 <= stock 库存数
//库存锁定,只要有异常回滚订单数据 (订单号,所有订单项)
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
//封装订单号
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
//封装订单项
wareSkuLockVo.setLocks(locks);
//TODO 4、远程锁库存
//远程调用库存服务调用锁定库存方法
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if (r.getCode() == 0){
//如果锁定成功
response.setOrder(order.getOrder());
//-------------------------------模拟异常-------------------------------------
//TODO 5、远程扣减积分
int i = 10/0;
return response;
}else {
response.setCode(3);
return response;
}
}else {
//如果验价失败
response.setCode(2);
return response;
}
}
}
本地事务:在分布式系统中,只能控制自己服务的数据库回滚,不能控制其他服务服务的数据库
分布式事务:可以控制所有服务的数据库回滚操作
微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。
但是如果发生异常,回滚的时候就会发生订单DB可以回滚,但是库存DB缺不能回滚
坑记录
在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。
解决:
0)、导入 spring-boot-starter-aop
1)、@EnableTransactionManagement(proxyTargetClass = true)
2)、@EnableAspectJAutoProxy(exposeProxy=true)
3)、AopContext.currentProxy() 调用方法
每一个微服务必须先创建 undo_log 数据库表
-- 注意此处0.3.0+ 增加唯一索引 ux_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;
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
查看seata-all的版本是 0.7.1
所以要下载的seata-server版本也要是 0.7.1,seata-server就是tc(transaction coordinate)事务协调者
从 https://github.com/seata/seata/releases,下载服务器软件包,将其解压缩
seata-server就是TC(Transactional coordinate)
此处只在 registry.conf中配置了nacos注册中心
启动nacos,再启动seata服务器,seata已注册到nacos中
所有要用到分布式事务的微服务,都需要让seata代理数据源
package com.lian.gulimall.ware.config;
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;
/**
* seata 配置代理数据源
*
*/
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(){
HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
hikariDataSource.setPoolName(dataSourceProperties.getName());
}
//将创建好的数据源交给seata代理
return new DataSourceProxy(hikariDataSource);
}
}
1、将seata-server中的 file.conf和registry.conf都复制到微服务下
2、修改 file.conf 的vgroup
#订单服务
service {
# 每一个微服务就是rm,都注入到 tc 中,服务名是 gulimall-order-fescar-service-group
vgroup_mapping.gulimall-order-fescar-service-group = "default"
或者在配置文件中修改,最好和服务名保持一致
#服务分组
alibaba:
seata:
tx-service-group: gulimall-order-fescar-service-group
适用场景
seata的分布式事务适合于在分布式服务中有多个远程调用的场景下,可以达到要么都成功,要么都失败的效果,例如商品服务的保存spu信息的业务方法,有多个远程调用,而且商品服务不是高并发场景
seata不适合用于高并发的场景下,中间件适合高并发场景,可以弥补seata的不足
例如:下单服务就是高并发场景,不适合使用seata,适合用中间件,例如用rabbitmq替代seata