梳理订单创建、远程检查和扣减账户余额、消息可靠性与超时未支付订单处理方案。
本篇要点
串联提交订单的业务步骤
检查余额并进行条件扣减
区分消息可靠性与延迟取消方案
**学习提示:**笔记中列出 TTL/死信队列和延迟插件等方案;方案介绍不等于当前项目的超时取消链路已使用 RabbitMQ 延迟消息。
内容回顾
1、跳转到结算页面
订单:vip、专辑、声音
都是跳转结算页面,显示vip、专辑、声音
2、生成订单接口(一部分)
今天内容 1、订单实现流程
2、订单接口 OrderService订单方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @Override public String submitOrder (OrderInfoVo orderInfoVo, Long userId) { String jsonString = JSON.toJSONString(orderInfoVo); Map map = JSON.parseObject(jsonString, Map.class); map.put("payWay" ,SystemConstant.ORDER_PAY_WAY_WEIXIN); SignHelper.checkSign(map); String tradeNo = orderInfoVo.getTradeNo(); DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript <>(); String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) " + " then return redis.call('del', KEYS[1]) " + " else return 0 " + " end" ; redisScript.setScriptText(script); redisScript.setResultType(Boolean.class); String key = "user:trade:" + userId; Boolean flag = (Boolean) redisTemplate.execute(new DefaultRedisScript <>(script, Boolean.class), Arrays.asList(key), tradeNo); if (!flag) { throw new GuiguException (ResultCodeEnum.ORDER_SUBMIT_REPEAT); } String orderNo = UUID.randomUUID().toString().replaceAll("-" , "" ); String payWay = orderInfoVo.getPayWay(); if (!"1103" .equals(payWay)) { this .saveOrder(orderInfoVo,userId,orderNo); } else { AccountLockVo accountDeductVo = new AccountLockVo (); accountDeductVo.setOrderNo(orderNo); accountDeductVo.setUserId(userId); accountDeductVo.setAmount(orderInfoVo.getOrderAmount()); accountDeductVo.setContent(orderInfoVo.getOrderDetailVoList() .get(0 ).getItemName()); Result result = userAccountFeignClient.checkAndDeduct(accountDeductVo); if (result.getCode()!=200 ) { throw new GuiguException (ResultCodeEnum.ACCOUNT_LESS); } OrderInfo orderInfo = this .saveOrder(orderInfoVo,userId,orderNo); UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo (); userPaidRecordVo.setOrderNo(orderNo); userPaidRecordVo.setUserId(orderInfo.getUserId()); userPaidRecordVo.setItemType(orderInfo.getItemType()); List<Long> itemIdList = orderInfoVo.getOrderDetailVoList() .stream().map(OrderDetailVo::getItemId) .collect(Collectors.toList()); userPaidRecordVo.setItemIdList(itemIdList); Result userResult = userInfoFeignClient.savePaidRecord(userPaidRecordVo); if (200 != userResult.getCode()) { throw new GuiguException (211 , "新增购买记录异常" ); } } return orderNo; }
远程调用:检查和扣减余额 分析 方法一:
1 2 3 4 5 6 7 8 9 10 # 检查 # 可用余额是否大于等于支付金额 SELECT * FROM user_account ua WHERE ua.available_amount>= 100 ;# 扣减余额 UPDATE user_account ua SET ua.available_amount= ua.available_amount-50 , ua.total_amount= ua.total_amount-50 WHERE ua.user_id= 35
方法二:
1 2 3 4 UPDATE user_account ua SET ua.available_amount= ua.available_amount-20 , ua.total_amount= ua.total_amount-20 WHERE ua.user_id= 35 and ua.available_amount>= 20
1 2 3 4 5 6 @PostMapping("checkAndDeduct") Result checkAndDeduct (@RequestBody AccountLockVo accountDeductVo) { userAccountService.checkAndDeduct(accountDeductVo); return Result.ok(); }
编写sql实现检查并扣减余额 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void checkAndDeduct (AccountLockVo accountDeductVo) { int count = userAccountMapper.checkAndDeduct(accountDeductVo.getAmount(), accountDeductVo.getUserId()); if (count == 0 ) { throw new GuiguException (ResultCodeEnum.ACCOUNT_LESS); } UserAccountDetail userAccountDetail=new UserAccountDetail (); userAccountDetail.setUserId(accountDeductVo.getUserId()); userAccountDetail.setTitle(accountDeductVo.getContent()); userAccountDetail.setTradeType("1204" ); userAccountDetail.setAmount(accountDeductVo.getAmount()); userAccountDetail.setOrderNo(accountDeductVo.getOrderNo()); userAccountDetailMapper.insert(userAccountDetail); }
1 2 3 4 5 6 7 8 9 10 < ! < update id= "checkAndDeduct"> UPDATE user_account SET total_amount = total_amount - #{amount}, available_amount = available_amount - #{amount} WHERE user_id = #{userId} AND available_amount >= #{amount} AND is_deleted = 0 < / update >
不编写语句,使用mp实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override public void checkAndDeduct (AccountLockVo accountDeductVo) { LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper <>(); wrapper.eq(UserAccount::getUserId,accountDeductVo.getUserId()); wrapper.ge(UserAccount::getAvailableAmount,accountDeductVo.getAmount()); LambdaQueryWrapper<UserAccount> wrapperOldUserAccount = new LambdaQueryWrapper <>(); wrapperOldUserAccount.eq(UserAccount::getUserId,accountDeductVo.getUserId()); UserAccount userAccount = userAccountMapper.selectOne(wrapperOldUserAccount); userAccount.setAvailableAmount(userAccount.getAvailableAmount().subtract(accountDeductVo.getAmount())); userAccount.setTotalAmount(userAccount.getTotalAmount().subtract(accountDeductVo.getAmount())); int count = userAccountMapper.update(userAccount, wrapper); if (count == 0 ) { throw new GuiguException (ResultCodeEnum.ACCOUNT_LESS); } UserAccountDetail userAccountDetail=new UserAccountDetail (); userAccountDetail.setUserId(accountDeductVo.getUserId()); userAccountDetail.setTitle(accountDeductVo.getContent()); userAccountDetail.setTradeType("1204" ); userAccountDetail.setAmount(accountDeductVo.getAmount()); userAccountDetail.setOrderNo(accountDeductVo.getOrderNo()); userAccountDetailMapper.insert(userAccountDetail); }
保存订单方法 1 2 3 order_info: 订单基本信息 order_detail:订单明细 order_derate:优惠明细
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private OrderInfo saveOrder (OrderInfoVo orderInfoVo, Long userId, String orderNo) { OrderInfo orderInfo = new OrderInfo (); BeanUtils.copyProperties(orderInfoVo, orderInfo); orderInfo.setOrderNo(orderNo); List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList(); String itemName = orderDetailVoList.get(0 ).getItemName(); orderInfo.setOrderTitle(itemName); orderInfo.setUserId(userId); orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID); orderInfoMapper.insert(orderInfo); if (!CollectionUtils.isEmpty(orderInfoVo.getOrderDetailVoList())) { orderInfoVo.getOrderDetailVoList().forEach(orderDetailVo -> { OrderDetail orderDetail = new OrderDetail (); BeanUtils.copyProperties(orderDetailVo, orderDetail); orderDetail.setOrderId(orderInfo.getId()); orderDetailMapper.insert(orderDetail); }); } if (!CollectionUtils.isEmpty(orderInfoVo.getOrderDerateVoList())) { orderInfoVo.getOrderDerateVoList().forEach(orderDerateVo -> { OrderDerate orderDerate = new OrderDerate (); BeanUtils.copyProperties(orderDerateVo, orderDerate); orderDerate.setOrderId(orderInfo.getId()); orderDerateMapper.insert(orderDerate); }); } String payWay = orderInfoVo.getPayWay(); if (!"1103" .equals(payWay)) { Long orderId = orderInfo.getId(); this .sendDelayMessage(orderId); } return orderInfo; }
发送延迟消息
RabbitMQ消息可靠性配置 第一个方面:
发送消息,首先到达mq服务里面交换机
其次,由交换机把消息转发给队列
第三,消费端监听队列,从队列获取消息进行消费
– 所以,如果mq消息出现问题,从三个环节找到问题
第二个方面:
使用确认模式判断消息是否到达交换机 ,如果没有到达进行重试
使用返回模式确认消息是否正确到达队列 ,如果没有到达进行重试
在消费端进行手动确认
RabbitMQ幂等性保证
**幂等性:**相同消息发送多次,只会消费一次
如何保证?
第一种,使用数据库本身,乐观锁(版本号)
第二种,使用Redis里面setnx实现
延迟消息
实现延迟消息方式有很多种:
第一种,使用RabbitMQ的TTL+死信队列实现
**– TTL:**消息存活时间,比如设置消息10s,过了10s消息死亡了
– 死信:
– 实现基本过程:
首先,发送消息到达正常队列里面,同时设置消息过期时间,比如30分钟
其次,如果正常队列里面消息过期了,称为死信,死信进入死信队列里面
第三,专门有消费端监听死信队列,当里面有消息过来,肯定过期了,进行消费。比如过了30分钟订单,超时未支付,取消订单
第二种,使用RabbitMQ延迟插件实现
第三种,使用Redisson框架实现
Redisson基于Redis封装客户端工具,使用Redisson方便实现很多功能:布隆过滤器,分布式锁,延迟队列等
使用Redisson实现订单超时未支付自动取消
1 2 3 4 5 6 7 8 9 10 11 12 13 private void sendDelayMessage (Long orderId) { RBlockingQueue<Object> blockingQueue = redissonClient.getBlockingQueue(MqConst.EXCHANGE_CANCEL_ORDER); RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingQueue); delayedQueue.offer(orderId.toString(),10 ,TimeUnit.SECONDS); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class RedisDelayHandle { @Autowired private RedissonClient redissonClient; @Autowired private OrderInfoService orderInfoService; @PostConstruct public void listener () { new Thread (()->{ while (true ) { try { RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(MqConst.EXCHANGE_CANCEL_ORDER); String orderId = blockingQueue.take(); if (StringUtils.hasText(orderId)) { orderInfoService.cancelOrder(Long.parseLong(orderId)); } } catch (InterruptedException e) { throw new RuntimeException (e); } } }).start(); } }
1 2 3 4 5 6 7 8 9 @Override public void cancelOrder (Long orderId) { OrderInfo orderInfo = orderInfoMapper.selectById(orderId); if (orderInfo.getOrderStatus().equals("0901" )) { orderInfo.setOrderStatus("0903" ); orderInfoMapper.updateById(orderInfo); } }
远程调用:添加购买记录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class UserInfoApiController { @Autowired private UserInfoService userInfoService; @Operation(summary = "处理用户购买记录") @PostMapping("/savePaidRecord") public Result savePaidRecord (@RequestBody UserPaidRecordVo userPaidRecordVo) { userInfoService.savePaidRecord(userPaidRecordVo); return Result.ok(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 @Autowired private TrackInfoFeignClient trackInfoFeignClient;@Autowired private VipServiceConfigMapper vipServiceConfigMapper;@Autowired private UserVipServiceMapper userVipServiceMapper;@Override public void savePaidRecord (UserPaidRecordVo userPaidRecordVo) { String itemType = userPaidRecordVo.getItemType(); if ("1001" .equals(itemType)) { UserPaidAlbum userPaidAlbum = new UserPaidAlbum (); userPaidAlbum.setUserId(userPaidRecordVo.getUserId()); userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo()); Long albumId = userPaidRecordVo.getItemIdList().get(0 ); userPaidAlbum.setAlbumId(albumId); userPaidAlbumMapper.insert(userPaidAlbum); } else if ("1002" .equals(itemType)) { Long id = userPaidRecordVo.getItemIdList().get(0 ); Result<TrackInfo> result = trackInfoFeignClient.getTrackInfo(id); TrackInfo trackInfo = result.getData(); List<Long> itemIdList = userPaidRecordVo.getItemIdList(); itemIdList.forEach(trackId->{ UserPaidTrack userPaidTrack = new UserPaidTrack (); userPaidTrack.setTrackId(trackId); userPaidTrack.setUserId(userPaidRecordVo.getUserId()); userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo()); Long albumId = trackInfo.getAlbumId(); userPaidTrack.setAlbumId(albumId); userPaidTrackMapper.insert(userPaidTrack); }); } else { Long userId = userPaidRecordVo.getUserId(); UserInfo userInfo = userInfoMapper.selectById(userId); Date currentDate = new Date (); if (userInfo.getIsVip().intValue()==1 && userInfo.getVipExpireTime().after(new Date ())) { currentDate = userInfo.getVipExpireTime(); } Long vipId = userPaidRecordVo.getItemIdList().get(0 ); VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipId); Integer serviceMonth = vipServiceConfig.getServiceMonth(); Date expireDate = new LocalDateTime (currentDate) .plusMonths(serviceMonth).toDate(); UserVipService userVipService = new UserVipService (); userVipService.setOrderNo(userPaidRecordVo.getOrderNo()); userVipService.setUserId(userPaidRecordVo.getUserId()); userVipService.setStartTime(new Date ()); userVipService.setExpireTime(expireDate); userVipServiceMapper.insert(userVipService); userInfo.setIsVip(1 ); userInfo.setVipExpireTime(expireDate); this .userInfoMapper.updateById(userInfo); } }