web项目中请求线程到service层的时候远程调用服务之前是串行化执行每个任务都要get阻塞等待任务完成,举例当用户在购物车页面点击去结算就会请求后台toTrade请求获取订单确认的详情数据并渲染到订单详情页,现在在toTrade请求中使用异步任务编排CompletableFuture.runAsync(来使用线程池并提交任务,这导致远程请求时需要利用当前的请求线程中放在ThreadLocal的共享数据无法在CompletableFuture.runAsync提交的异步任务中获取到主请求线程的共享数据,如请求会员服务获取用户的地址,请求购物车服务获取选中的购物车项,在远程feign调用的时候需要实现拦截器为新的request设置上cookie,在拦截器中要获取到旧的request,这时不在同一个线程获取到的request就是null,理论的当前线程中 RequestContextHolder.getRequestAttributes();会保存controller中接收的request原理是ThreadLocal共享变量,现在解决方法:在异步线程编排前获取到 RequestAttributes requestAttributes= RequestContextHolder.getRequestAttributes();在异步编排的任务中
为当前线程的RequestContextHolder重新设置上原来的请求数据,让每一个线程都来共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);这样远程feign请求会创建新的request并获取原来的request保存的数据,携带cookie远程调用,就可以验证登陆后应该返回的指定用户数据。
幂等性问题解决
使用token:使用方式和验证码相同,有服务器存储一份,前端发送一份,订单提交时携带token验证通过后删除token并创建订单,当多次提交token无法验证通过。token设及的问题有先删除token还是后删除token问题,后删除token可能重复提交后创建相同的订单,破坏幂等,如果是先删除token,在微服务中token不可能单独存储在一个服务中,所以它和分布式session一样需要储存在redis中,当多个服务订单携带token时需要竞争获取服务端的token,服务端需要从redis中拿,当两个服务同时从redis中拿token成功,同时删除令牌,同时创建订单,幂等性失效。
解决方式:获取getToken判断相等getToken==token 删除delToken这三个要是原子性的保证只有一个服务执行完整操作。可以在redis的lua脚本完成操作
if redis.call("get",KYES[1]==ARGV[1] return redis.call("del".KEYS[2])else return 0 end)
2 解决方式使用各种锁机制
1数据库悲观锁
select * from xx where id=1 for update(行锁) 悲观锁使用一般随事务一起使用,数据锁定时间可能很长,需要更具实际情况选用,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会麻烦。
数据库乐观锁
数据库更新的时候加上version字段(比如订单下单成功减库存操作可能feign触发重试机制)
3 业务层使用分布式锁
各种唯一约束
1数据可唯一约束 主键唯一索引举例下订单的订单号是唯一的索引,重复下订单只有一次是有效的
2 使用redis set防重,拿百度网盘的妙功能,如果这个文件之前有人上传过就计算它的MD5值,放在set中,每一个独立存在的文件只有一个MD5值,拿订单为例,如果这个订单得到处理了,就生成一个该订单好号的MD5值,如果重复提交就检测set中是否有该订单的MD5值如果有表明订单已经处理成功,重复提交就失败。
3 防重表
使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且 他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避 免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个 事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。 之前说的 redis 防重也算。
4、全局请求唯一 id
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id; proxy_set_header X-Request-Id $request_id;
最终实现
本项目通过token解决幂等性,在订单确认页面的请求中返回一个token给前端,并在后端统一存储token到redis中,由于是分布式系统,并不像传统单点服务一样保存在当前服务中,而是通过原子性获取redis的token确保每个竞争token的服务在获取比较删除都是原子性的来保证多个服务不相互干扰引发数据混乱。下订单验证token的lua脚本实现如下:
@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {confirmVoThreadLocal.set(vo);SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();//去创建、下订单、验令牌、验价格、锁定库存...//获取当前用户登录的信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();responseVo.setCode(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();//通过lure脚本原子验证令牌和删除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(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) {//金额对比//TODO 3、保存订单saveOrder(order);//4、库存锁定,只要有异常,回滚订单数据//订单号、所有订单项信息(skuId,skuNum,skuName)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 调用远程锁定库存的方法//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务R r = wmsFeignService.orderLockStock(lockVo);if (r.getCode() == 0) {//锁定成功responseVo.setOrder(order.getOrder());// int i = 10/0;//TODO 订单创建成功,发送消息给MQrabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());//删除购物车里的数据redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());return responseVo;} else {//锁定失败String msg = (String) r.get("msg");throw new NoStockException(msg);// responseVo.setCode(3);// return responseVo;}} else {responseVo.setCode(2);return responseVo;}}}
大致内容
//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();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) //令牌验证失败