1. 提交幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。
幂等解决方案-token 机制
(1)服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
(2)然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
(3)服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
(4)如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
危险性:
a、先删除 token 还是后删除 token;
(1)先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致,请求还是不能执行。
(2)后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两边
(3)我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
b、Token 获取、比较和删除必须是原子性
(1)redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行;
(2)可以在 redis 使用 lua 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
代码实现:
在接口中生成token,返回到提交页面
@GetMapping(value = "/toTrade")
public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("confirmOrderData",confirmVo);
//展示订单确认的数据
return "confirm";
}
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息,从登录拦截器中获取
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//解决feign在远程调用时丢失上下文的问题
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
/*CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
}, threadPoolExecutor);*/
/*List<CartItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);*/
List<OrderItemVo> orderItemVo = getUserCartItems();
confirmVo.setItems(orderItemVo);
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(addressFuture).get();
return confirmVo;
}
在提交结算页面form表单中携带提交token
<form action="http://order.gulimall.com/submitOrder" method="post">
<input id="addrInput" type="hidden" name="addrId" />
<input id="payPriceInput" type="hidden" name="payPrice">
<input name="orderToken" th:value="${confirmOrderData.orderToken}" type="hidden"/>
<button class="tijiao" type="submit">提交订单</button>
</form>
在提交订单接口使用token机制解决幂等性问题
@PostMapping(value = "/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单成功来到支付选择页
//下单失败回到订单确认页重新确定订单信息
if (responseVo.getCode() == 0) {
//成功
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1: msg += "令牌订单信息过期,请刷新再次提交"; break;
case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
case 3: msg += "库存锁定失败,商品库存不足"; break;
}
attributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}catch (Exception e){
if (e instanceof NoStockException) {
String message = ((NoStockException)e).getMessage();
attributes.addFlashAttribute("msg",message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
// @GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(0);
String orderToken = vo.getOrderToken();
//验证令牌【令牌的对比和删除必须保证原子性】
//0令牌验证失败 - 1验证成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L){
//令牌验证失败
responseVo.setCode(1);
return responseVo;
}else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常,回滚订单数据
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = 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());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
//订单创建成功,发送消息MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
// int i = 1/0;
//删除购物车里的数据
// redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
}else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
}else {
responseVo.setCode(2);
return responseVo;
}
}
/*String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId());
if (orderToken != null && orderToken.equals(redisToken)){
//令牌验证通过
redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId());
}else {
//验证不通过
}*/
}
2. 分布式事务
在提交订单过程中,创建订单执行成功,但是调用远程锁定库存方法已经执行成功,但是由于网络原因超时原因已经执行成功了,但是返回了网络执行超时。或者两者都已经执行成功了,但是在后面的执行逻辑中抛出了异常,此时订单事务会进行回滚,但是库存事务不会回滚。所以使用@Transactional注解也只是能解决本地事务回滚的问题,不能解决远程事务回滚的问题。
(1)在订单逻辑和库存逻辑下面的逻辑中模拟异常场景(int i=1/0),在数据库中库存表已经发生了变化,锁定库存已经变成了1,而订单表和订单详情表却已经回滚了。
(2)本地事务失效的场景
@Transactional(timeout = 30) //a事务的所有设置就会传播到和他公用一个事务的方法
public void a(){
b(); //直接调用不会生效
c(); //直接调用不会生效
int i = 1/0;
}
@Transactional(propagation = Propagation.REQUIRED,timeout = 2)
public void b(){}
@Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
public void c(){}
在同一对象中方法相互调用失效,在a()方法中直接引用b和c方法,即使b方法和c方法上有事务也不会生效,这种相当于代码的复制,原因会绕过代理对象,事务是使用代理对象控制的。
所有要解决在本地事务失效的 问题可以引入aop类
a)在pom中引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
b)在主启动类上开启aspect动态代理模式,对外暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true) //开启了aspect动态代理模式,对外暴露代理对象
@EnableRedisHttpSession //开启springsession
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = GlobalTransactionAutoConfiguration.class)
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
c)在业务方法中获取代理对象,进行调用,当然也可以把b和c方法放在不同的接口实现类中进行调用。
@Transactional(timeout = 30) //a事务的所有设置就会传播到和他公用一个事务的方法
public void a(){
b(); //直接调用不会生效
c(); //直接调用不会生效
OrderOperateHistoryServiceImpl currentProxy = (OrderOperateHistoryServiceImpl) AopContext.currentProxy();
currentProxy.b(); //生效
currentProxy.c(); //生效
// bService.b(); //a事务
// cService.c(); //新事务(在a中发生异常不会回滚)
int i = 1/0;
}
@Transactional(propagation = Propagation.REQUIRED,timeout = 2)
public void b(){}
@Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
public void c(){}
(3)使用seata AT模式来解决下单过程中的分布式事务问题
a) 在需要控制事务的微服务必须创建undo_Log表
b)导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
c)安装事务协调器:seate-server,解压并启动seata-server,配置registry.conf:注册中心配置 修改 registry : nacos,启动后在nacos中可以看到serverAddr服务。
d)所有想要用到分布式事务的微服务使用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);
}
}
e)每个微服务,都必须导入 registry.conf file.conf,修改file.conf
vgroup_mapping.{application.name}-fescar-server-group = "default"
f)给分布式大事务的入口标注@GlobalTransactional,每一个远程的小事务用@Trabsactional,启动测试分布式事务
重新提交下单,库存表和订单表中的数据都未发生变换
3. 使用消息队列处理指定时间未下单关单问题
订单过程的消息队列流程
订单创建成功后,发送消息到交换机order-event-exchange,指定路由键order.create.order到延迟队列中,根据延迟队列的参数配置在等待指定时间后,指定路由键order.release.order到order-event-exchange交换机,在根据延迟队列的绑定路由键发送消息到order.release.order.queue队列中,监听队列释放订单。
创建交换机队列绑定关系
@Configuration
public class MyRabbitMQConfig {
/**
* 死信队列
*/
@Bean
public Queue orderDelayQueue(){
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
}
创建订单接口,发送消息到MQ
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping(value = "/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单成功来到支付选择页
//下单失败回到订单确认页重新确定订单信息
if (responseVo.getCode() == 0) {
//成功
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1: msg += "令牌订单信息过期,请刷新再次提交"; break;
case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
case 3: msg += "库存锁定失败,商品库存不足"; break;
}
attributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}catch (Exception e){
if (e instanceof NoStockException) {
String message = ((NoStockException)e).getMessage();
attributes.addFlashAttribute("msg",message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
// @GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(0);
String orderToken = vo.getOrderToken();
//验证令牌【令牌的对比和删除必须保证原子性】
//0令牌验证失败 - 1验证成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L){
//令牌验证失败
responseVo.setCode(1);
return responseVo;
}else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常,回滚订单数据
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = 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());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
//订单创建成功,发送消息MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
// int i = 1/0;
//删除购物车里的数据
// redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
}else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
}else {
responseVo.setCode(2);
return responseVo;
}
}
}
监听order.release.order.queue队列,释放订单
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
@Override
public void closeOrder(OrderEntity orderEntity) {
//关闭订单之前先查询一下数据库,判断此订单状态是否已支付
OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
eq("order_sn",orderEntity.getOrderSn()));
if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
//代付款状态进行关单
OrderEntity orderUpdate = new OrderEntity();
orderUpdate.setId(orderInfo.getId());
orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(orderUpdate);
// 发送消息给MQ
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderInfo, orderTo);
try {
//TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
//这里是发送到MQ再次处理释放库存的操作
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 定期扫描数据库,重新发送失败的消息
}
}
}
MQ使用中的问题消息不丢失问题
(1)消息发送确认:这种是用来确认生产者将消息发送给交换机,交换机传递给队列过程中,消息是否成功投递
发送确认分两步:一是确认是否到达交换机,二是确认是否到达队列
(2)消费接收确认:这种是确认消费者是否成功消费了队列中的消息
在程序中进行手动ack确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
具体实现:
(1)定制化rabbitTemplate前要在属性配置文件中先进行配置
# 开启发送端消息抵达Broker确认
spring.rabbitmq.publisher-confirms=true
# 开启发送端消息抵达Queue确认
spring.rabbitmq.publisher-returns=true
# 消息未被路由至任何一个queue,则回退一条消息
spring.rabbitmq.template.mandatory=true
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
(2)定制化rabbitTemplate,创建数据库表记录发送的消息状态,配置参考https://www.cnblogs.com/java-spring/p/15667835.html
@Configuration
public class MyRabbitConfig {
private RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、消息发送确认
* broker服务收到消息就会回调
* (1)spring.rabbitmq.publisher-confirms: true
* (2)设置确认回调ConfirmCallback
* 消息没有正确抵达队列就会进行回调
* (1)spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* (2)设置确认回调ReturnCallback
*
* 2、消息接收确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
// @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
//1. 做好消息确认机制(pulisher,consumer(手动ack))
//2. 每一个发送的消息都在数据库做好记录,定期将发送失败的消息再次发送一次
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
//3. 如果发送失败更新数据库中的消息记录状态
System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +
"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
});
}
}
消息幂等性
(1)消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
(2)消息消费失败,由于重试机制,自动又将消息发送出去
(3)成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
一般解决方案
(1)消费者的业务消费接口应该设计为幂等性的。比如扣库存根据工作单的状态标志进行判断是否扣除
(2)使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理;
(3)rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的