订单中心主要包括物流信息,用户信息,支付信息,订单信息,促销信息,商品信息
解决:加一个feign远程调用的拦截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @description:
* @author: wei-xhh
* @create: 2020-07-26
*/
@Configuration
public class GreymallFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// System.out.println("feign远程之前先进行RequestInterceptor.apply");
//1、RequestContextHolder拿到刚进来的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes != null){
HttpServletRequest request = attributes.getRequest(); //老请求
if(request != null){
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
}
}
}
};
}
}
解决方法:
在线程开始执行之前拿到共享数据,然后再每个线程里共享一下
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//解决Feign异步 ThreadLocal 问题
//获取当前线程的数据
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有收货地址列表
//设置主线程中的数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List address =
memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
//设置主线程中的数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List items =
cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, executor).thenRunAsync(() -> {
List items = confirmVo.getItems();
List collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkuHasStock(collect);
List data = hasStock.getData(new TypeReference>() {
});
if (data != null) {
Map map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
在数据库级别可以给订单号添加唯一索引来确保幂等性
token机制也就是令牌机制,常见的实现方式有验证码
代码:
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
submitVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.setCode(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();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript(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、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
double abs = Math.abs(payAmount.subtract(payPrice).doubleValue());//44442
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//TODO 远程锁库存
//问题:库存成功了,但是网络原因超时了,订单回滚,库存不回滚
//为了保证高并发,库存服务自己回滚,可以发消息个库存服务;
//库存服务本身也可以自动解锁模式 使用消息队列
R r = wmsFeignService.orderLockStock(lockVo);
if(r.getCode() == 0){
//锁成功了
response.setOrder(order.getOrder());
//TODO 模拟远程扣减积分,出异常
// int i = 10/0; //订单回滚,库存不回滚
//TODO 订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
// response.setCode(3);
// return response;
}
} else {
response.setCode(2);
return response;
}
}
// String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
// if(orderToken != null && orderToken.equals(redisToken)){
// //令牌验证通过
// redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
//
// } else {
// //不通过
// }
}
锁库存代码:
/**
* 为某个订单锁定库存
*默认只要是运行时异常都会回滚
*
*库存解锁的场景
* 1、下订单成功,订单过期没有支付被系统自动取消,被用户手动取消,都要解锁库存
* 2、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚
* 之前解锁的库存就要自动解锁
* @param vo
* @return
*/
@Transactional(rollbackFor = NoStockException.class)
@Override
public boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情
*
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存
//1、找到每个商品在哪个仓库都有库存
List locks = vo.getLocks();
List collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每个商品都锁定成功,将当前商品锁定了几件的工作单记录发送给MQ
//2、如果锁定失败,前面保存的工作的回滚,发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
//
for (Long wareId : wareIds) {
//成功就返回1,否则为0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if(count == 1){
skuStocked = true;
// TODO 告诉MQ库存锁定成功
WareOrderTaskDetailEntity entity =
new WareOrderTaskDetailEntity(null, skuId, null, hasStock.getNum(), taskEntity.getId(), wareId, 1);
wareOrderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity,stockDetailTo);
//只发id不行,防止回滚以后找不到数据
lockedTo.setDetail(stockDetailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if(skuStocked == false){
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部锁定成功
return true;
}