当前位置: 首页 > news >正文

网站集约化建设解读江西省城乡建设培训网官方网站

网站集约化建设解读,江西省城乡建设培训网官方网站,wordpress项目管理主题,微网站的价格一、订单服务1、订单基本概念2、订单基本构成3、订单状态4、订单流程5、配置拦截器拦截订单请求6、订单确认页模型抽取7、订单确认页vo封装8、Feign 远程调用请求头丢失问题\*\*\*\*\* 惨痛教训9、Feign 异步调用请求头丢失问题10、查看库存状态11、模拟计算运费12、接口幂等性…一、订单服务1、订单基本概念2、订单基本构成3、订单状态4、订单流程5、配置拦截器拦截订单请求6、订单确认页模型抽取7、订单确认页vo封装8、Feign 远程调用请求头丢失问题\*\*\*\*\* 惨痛教训9、Feign 异步调用请求头丢失问题10、查看库存状态11、模拟计算运费12、接口幂等性1什么是接口幂等性2哪些情况要防止接口幂等性3什么情况下需要幂等性4幂等性解决方案token 机制各种锁机制\- 数据库悲观锁\- 数据库乐观锁\- 业务层分布式锁各种唯一约束\- 数据库唯一约束\- redis防重\- 防重表\- 全局唯一id13、订单确认页防重令牌14、提交订单二、分布式事务1、本地事务1事务的基本性质2事务的隔离级别3事务的传播行为SpringBoot 中事务的坑2、分布式事务1为什么出现分布式事务2CAP 理论3面临的额问题4BASE 理论5 分布式事务的几种解决方案2PC模式柔性事务-TCC事务补偿型方案柔性事务- 最大努力型通知方案柔性事务- 可靠消息 最终一致性方案异步确保型3、Seata11.4.2 Seata Nacos 搭建使用seata 服务端配置seata 客户端配置20.9 版本 Seata 使用三、使用延迟队列自动解锁库存解决 远程调用问题四、定时关闭订单五、支付服务1、一些概念说明2、沙箱环境测试3、内网穿透4、整合阿里云支付服务5、支付成功同步回调6、异步通知内网穿透环境搭建7、支付完成8、收单六、秒杀1、定时任务与Cron表达式2、秒杀商品上架3、秒杀上架幂等性问题4、查询所有的秒杀商品时区BUG5、查询某一个秒杀 商品6、秒杀系统设计7、秒杀流程七、Sentinel1、熔断降级限流2、SpringBoot整合Sentinel3、实时监控4、自定义sentinel的返回信息5、RabbitTemplate 与 MyRabbitConfig 循环依赖问题6、熔断降级7、开启自定义的受保护资源8、网关流控9、自定义网关流控回调总结一、订单服务 视频来源: 【Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目】 笔记对应视频集数P261~P338 1、订单基本概念 电商系统涉及到 3 流分别时信息流资金流物流而订单系统作为中枢将三者有机的集 合起来。 订单模块是电商系统的枢纽在订单这个环节上需求获取多个模块的数据和信息同时对这 些信息进行加工处理后流向下个环节这一系列就构成了订单的信息流通 2、订单基本构成 1、用户信息 用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成用户账 户需要绑定手机号码但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加 多个收货信息用户等级信息可以用来和促销系统进行匹配获取商品折扣同时用户等级 还可以获取积分的奖励等 2、订单基础信息 订单基础信息是订单流转的核心其包括订单类型、父/子订单、订单编号、订单状态、订 单流转的时间等。 1订单类型包括实体商品订单和虚拟订单商品等这个根据商城商品和服务类型进行区 分。 2同时订单都需要做父子订单处理之前在初创公司一直只有一个订单没有做父子订 单处理后期需要进行拆单的时候就比较麻烦尤其是多商户商场和不同仓库商品的时候 父子订单就是为后期做拆单准备的。 3订单编号不多说了需要强调的一点是父子订单都需要有订单编号需要完善的时候 可以对订单编号的每个字段进行统一定义和诠释。 4订单状态记录订单每次流转过程后面会对订单状态进行单独的说明。 5订单流转时间需要记录下单时间支付时间发货时间结束时间/关闭时间等等 3、商品信息 商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息 等从用户下单行为记录的用户下单数量商品合计价格等。 4.、优惠信息 优惠信息记录用户参与的优惠活动包括优惠促销活动比如满减、满赠、秒杀等用户使 用的优惠券信息优惠券满足条件的优惠券需要默认展示出来具体方式已在之前的优惠券 篇章做过详细介绍另外还虚拟币抵扣信息等进行记录。 为什么把优惠信息单独拿出来而不放在支付信息里面呢 因为优惠信息只是记录用户使用的条目而支付信息需要加入数据进行计算所以做为区分。 5、支付信息 1支付流水单号这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支 付流水号财务通过订单号和流水单号与支付通道进行对账使用。 2支付方式用户使用的支付方式比如微信支付、支付宝支付、钱包支付、快捷支付等。 支付方式有时候可能有两个——余额支付第三方支付。 3商品总金额每个商品加总后的金额运费物流产生的费用优惠总金额包括促 销活动的优惠金额优惠券优惠金额虚拟积分或者虚拟币抵扣的金额会员折扣的金额等 之和实付金额用户实际需要付款的金额。 用户实付金额商品总金额运费-优惠总金额 6.、物流信息 物流信息包括配送方式物流公司物流单号物流状态物流状态可以通过第三方接口来 获取和向用户展示物流每个状态节点。 3、订单状态 1、待付款 用户提交订单后订单进行预下单目前主流电商网站都会唤起支付便于用户快速完成支 付需要注意的是待付款状态下可以对库存进行锁定锁定库存需要配置支付超时时间超 时后将自动取消订单订单变更关闭状态。 2、已付款/待发货 用户完成订单支付订单系统需要记录支付时间支付流水单号便于对账订单下放到 WMS 系统仓库进行调拨配货分拣出库等操作。 3、待收货/已发货 仓储将商品出库后订单进入物流环节订单系统需要同步物流信息便于用户实时知悉物 品物流状态 4.、已完成 用户确认收货后订单交易完成。后续支付侧进行结算如果订单存在问题进入售后状态 6、售后中 用户在付款后申请退款或商家发货后用户申请退换货。 售后也同样存在各种状态当发起售后申请后生成售后订单售后订单状态为待审核等待 商家审核商家审核通过后订单状态变更为待退货等待用户将商品寄回商家收货后订单 状态更新为待退款状态退款到用户原账户后订单状态更新为售后成功。 4、订单流程 订单流程是指从订单产生到完成整个流转的过程从而行程了一套标准流程规则。而不同的 产品类型或业务类型在系统中的流程会千差万别比如上面提到的线上实物订单和虚拟订单 的流程线上实物订单与 O2O 订单等所以需要根据不同的类型进行构建订单流程。 不管类型如何订单都包括正向流程和逆向流程对应的场景就是购买商品和退换货流程正 向流程就是一个正常的网购步骤订单生成–支付订单–卖家发货–确认收货–交易成功。 而每个步骤的背后订单是如何在多系统之间交互流转的可概括如下图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-832bKPJ4-1675935821383)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/%E7%94%B5%E5%95%86%E8%AE%A2%E5%8D%95%E6%B5%81%E7%A8%8B%E5%9B%BE.png)] 1、订单创建与支付 (1) 、订单创建前需要预览订单选择收货信息等 (2) 、订单创建需要锁定库存库存有才可创建否则不能创建 (3) 、订单创建后超时未支付需要解锁库存 (4) 、支付成功后需要进行拆单根据商品打包方式所在仓库物流等进行拆单 (5) 、支付的每笔流水都需要记录以待查账 (6) 、订单创建支付成功等状态都需要给 MQ 发送消息方便其他系统感知订阅 2、逆向流程 (1) 、修改订单用户没有提交订单可以对订单一些信息进行修改比如配送信息 优惠信息及其他一些订单可修改范围的内容此时只需对数据进行变更即可。 (2) 、订单取消用户主动取消订单和用户超时未支付两种情况下订单都会取消订 单而超时情况是系统自动关闭订单所以在订单支付的响应机制上面要做支付的 限时处理尤其是在前面说的下单减库存的情形下面可以保证快速的释放库存。 另外需要需要处理的是促销优惠中使用的优惠券权益等视平台规则进行相应补 回给用户。 (3) 、退款在待发货订单状态下取消订单时分为缺货退款和用户申请退款。如果是 全部退款则订单更新为关闭状态若只是做部分退款则订单仍需进行进行同时生 成一条退款的售后订单走退款流程。退款金额需原路返回用户的账户。 (4) 、发货后的退款发生在仓储货物配送在配送过程中商品遗失用户拒收用户 收货后对商品不满意这样情况下用户发起退款的售后诉求后需要商户进行退款 的审核双方达成一致后系统更新退款状态对订单进行退款操作金额原路返 回用户的账户同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果 发生双方协调不一致情况下可以申请平台客服介入。在退款订单商户不处理的情 况下系统需要做限期判断比如 5 天商户不处理退款单自动变更同意退款。 5、配置拦截器拦截订单请求 在购物车中点击 ‘去结算’ 跳转到 订单确认页面再次之前增加拦截器判断用户是否登录如果没有登录跳转到登录界面进行登录。 public class OrderInterceptor implements HandlerInterceptor {public static ThreadLocalMemberRespVo threadLocal new ThreadLocal();Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {MemberRespVo memberRespVo (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if (memberRespVo null) {request.getSession().setAttribute(msg,请先登录);// 没有进行登录跳转到登录界面登录response.sendRedirect(http://auth.gulimall.com/login.html);return false;}else{threadLocal.set(memberRespVo);return true;}} }Configuration public class OrderWebConfig implements WebMvcConfigurer {/** 增加拦截器* */Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new OrderInterceptor()).addPathPatterns(/**);} }6、订单确认页模型抽取 修改一个BUG 在购物车页面应该只有勾选的商品才加到总价格中 过滤出勾选的商品即可。 订单确认页模型抽取 1、首先是收货人信息。有更多地址即有多个收货地址其中有一个默认收货地址 2、支付方式这里不做过多的设计使用固定的支付方式 3、订单购物项这里的购物项是购物车中勾选的商品 4、优惠信息这里只使用京豆优惠 —— 会员的积分 5、订单的总金额 和 支付的金额 最终的订单确认Vo public class OrderConfirmVo {// 收货地址Getter SetterListMemberAddressVo address;// 订单购物项Getter SetterListOrderItemVo items;// 会员积分Getter SetterInteger integration;// 订单总金额BigDecimal total ;// 订单支付的总金额BigDecimal payPrice;// 订单令牌防止重复提交订单String orderToken;// 订单总金额 每个购物项价格累加起来// 每个购物项的价格 每个商品价格 * 商品数量public BigDecimal getTotal() {return (items ! null items.size() 0)? items.stream().map(item -item.getPrice().multiply(new BigDecimal(item.getCount()))).reduce(BigDecimal::add).get(): new BigDecimal(0);}public BigDecimal getPayPrice() {return getTotal();} }Data public class MemberAddressVo {TableIdprivate Long id;/*** member_id*/private Long memberId;/*** 收货人姓名*/private String name;/*** 电话*/private String phone;/*** 邮政编码*/private String postCode;/*** 省份/直辖市*/private String province;/*** 城市*/private String city;/*** 区*/private String region;/*** 详细地址(街道)*/private String detailAddress;/*** 省市区代码*/private String areacode;/*** 是否默认*/private Integer defaultStatus; } Data public class OrderItemVo {private Long skuId;private String title;private String defaultImage;private BigDecimal price ;private Integer count 1;private BigDecimal totalPrice ;private ListString skuAttr; }7、订单确认页vo封装 OrderWebController Controller public class OrderWebController {AutowiredOrderService orderService;GetMapping(/toTrade)public String toTrade(Model model) {OrderConfirmVo vo orderService.orderConfirm();// 保存订单确认vomodel.addAttribute(orderConfirmData,vo);return confirm;} }OrderServiceImpl AutowiredMemberFeignService memberFeignService;Autowiredprivate CartFeignService cartFeignService;Overridepublic OrderConfirmVo orderConfirm(){OrderConfirmVo orderConfirmVo new OrderConfirmVo();MemberRespVo memberRespVo OrderInterceptor.threadLocal.get();// 1、查询用户的收货地址 —— 远程调用ListMemberAddressVo address memberFeignService.getMemberAddresses(memberRespVo.getId());// 2、查询订单的购物项 —— 远程调用ListOrderItemVo items cartFeignService.getUserCartItems();// 3、积分信息orderConfirmVo.setIntegration(memberRespVo.getIntegration());// 总价格、应付价格自动封装...// TODO 订单防重复令牌return orderConfirmVo;}MemberFeignService FeignClient(gulimall-member) public interface MemberFeignService {/** 获取用户的收货地址* */GetMapping(member/memberreceiveaddress/{memberId}/getMemberAddresses)public ListMemberAddressVo getMemberAddresses(PathVariable(memberId) Long memberId);}CartFeignService FeignClient(gulimall-cart) public interface CartFeignService {/** 获取用户所有勾选的购物项* */GetMapping(/getUserCartItems)public ListOrderItemVo getUserCartItems(); }CartController /** 获取用户所有勾选的购物项* */GetMapping(/getUserCartItems)ResponseBodypublic ListCartItem getUserCartItems() {return cartService.getUserCartItems();}CartServiceImpl Overridepublic ListCartItem getUserCartItems() {UserInfoTo userInfoTo CartInterceptor.threadLocal.get();if (userInfoTo.getUserId() ! null) {String cartKey CART_TYPE_PREFIX userInfoTo.getUserId();// 获取购物车中所有的购物项ListCartItem cartItems getCartItems(cartKey);// 过滤掉未勾选的商品。并且商品的价格应该从数据库中查询assert cartItems ! null;ListCartItem collect cartItems.stream().filter(CartItem::isCheck).map(cartItem - {// 从数据库中查询最新的商品价格R skuInfo productFeignService.getSkuInfo(cartItem.getSkuId());SkuInfoVo data skuInfo.getData(skuInfo, new TypeReferenceSkuInfoVo() {});cartItem.setPrice(data.getPrice());return cartItem;}).collect(Collectors.toList());return collect;}else {return null;}}MemberReceiveAddressController /* * 获取用户的收货地址 * */ GetMapping(/{memberId}/getMemberAddresses) public ListMemberReceiveAddressEntity getMemberAddresses(PathVariable(memberId) Long memberId) {return memberReceiveAddressService.getMemberAddresses(memberId); }MemberReceiveAddressServiceImpl Overridepublic ListMemberReceiveAddressEntity getMemberAddresses(Long memberId) {return this.list(new QueryWrapperMemberReceiveAddressEntity().eq(member_id,memberId));}8、Feign 远程调用请求头丢失问题 由于我们 Order 服务调用 Cart 服务获取购物项时并没有传入用户id是 cart 服务自己去判断并获取的。这就会导致一个问题 用户 id 是从 session 中获取的并且这个 session 是和 GULISESSION 的 cookie相关联的如果请求中没有携带这个 cookie那么服务器就会新创建一个 session自然就没有用户信息了。也就是说使用 Feign 远程调用会出现 请求头丢失的问题。 在 Feign 远程调用之前会遍历所有的 RequestInterceptor 如果我们自己定义一个 RequestInterceptor 并且在里面设置请求头问题就迎刃而解了。 Configuration public class GuliFeignConfig {Bean(requestInterceptor)public RequestInterceptor requestInterceptor(){return new RequestInterceptor(){Overridepublic void apply(RequestTemplate requestTemplate) {// 获取老请求。和 Controller 中传入的 HttpServletRequest 一样ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request attributes.getRequest();requestTemplate.header(Cookie,request.getHeader(Cookie));}};} }RequestContextHolder 就是利用了 ThreadLocal 共享了同一个线程中的request变量。 ***** 惨痛教训 Controller 中的方法被远程调用时一定要加上 ResponseBody 或者在 Controller 上加 RestController 否则就会报 org.thymeleaf.exceptions.TemplateInputException: Error resolving template 9、Feign 异步调用请求头丢失问题 将查询订单确认数据改成异步的方式 Overridepublic OrderConfirmVo orderConfirm() throws ExecutionException, InterruptedException {OrderConfirmVo orderConfirmVo new OrderConfirmVo();MemberRespVo memberRespVo OrderInterceptor.threadLocal.get();CompletableFutureVoid addressFuture CompletableFuture.runAsync(() - {// 1、查询用户的收货地址 —— 远程调用ListMemberAddressVo address memberFeignService.getMemberAddresses(memberRespVo.getId());orderConfirmVo.setAddress(address);}, threadPoolExecutor);CompletableFutureVoid orderItemsFuture CompletableFuture.runAsync(() - {// 2、查询订单的购物项 —— 远程调用// Feign在远程调用之前会构造请求。ListOrderItemVo items cartFeignService.currentUserCartItems();orderConfirmVo.setItems(items);}, threadPoolExecutor);// 3、积分信息orderConfirmVo.setIntegration(memberRespVo.getIntegration());CompletableFuture.allOf(addressFuture,orderItemsFuture).get();// 总价格、应付价格自动封装...// TODO 订单防重复令牌return orderConfirmVo;}此时又会出现一个问题当使用 Feign 远程调用执行拦截器时当获取老请求时会出现空指针异常。 原因就是当没有使用异步任务的时候无论是Controller、Service、Dao、拦截器都由一个线程执行。因此可以共享 ThreadLocal 中的数据。 而使用异步任务由多个线程处理不会共享 ThreadLocal 中的数据。 因此解决办法就是在每个异步任务里都放一个request对象。 10、查看库存状态 WareFeignService FeignClient(gulimall-ware) public interface WareFeignService {/** 查询sku是否有库存* */PostMapping(ware/waresku/hasStock)public HashMapLong, Boolean hasStock(RequestBody ListLong skuIds); }OrderServiceImpl 11、模拟计算运费 1、将收货地址与运费封装成一个 Vo Data public class FareVo {private MemberAddressVo address;private BigDecimal fare ; }2、WareInfoController /** 计算运费* */GetMapping(/fare)public R fare(RequestParam(addrId)Long addrId) {FareVo fare wareInfoService.getFare(addrId);return R.ok().setData(fare);}3、计算运费的方式手机号的最后一位如有需要可对接顺丰、圆通…等各开放平台。 Overridepublic FareVo getFare(Long addrId) {R r memberFeignService.info(addrId);MemberAddressVo data r.getData(memberReceiveAddress, new TypeReferenceMemberAddressVo() {});if (data ! null) {FareVo fareVo new FareVo();String phone data.getPhone();// 使用手机号最后一位作为运费。String fareString phone.substring(phone.length() - 1, phone.length());BigDecimal fare new BigDecimal(fareString);fareVo.setFare(fare);fareVo.setAddress(data);return fareVo;}return null;}4、MemberFeignService FeignClient(gulimall-member) public interface MemberFeignService {RequestMapping(member/memberreceiveaddress/info/{id})public R info(PathVariable(id) Long id); } 12、接口幂等性 1什么是接口幂等性 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的不会因 为多次点击而产生了副作用比如说支付场景用户购买了商品支付扣款成功但是返回结 果的时候网络异常此时钱已经扣了用户再次点击按钮此时会进行第二次扣款返回结 果成功用户查询余额返发现多扣钱了流水记录也变成了两条,这就没有保证接口 的幂等性。 2哪些情况要防止接口幂等性 用户多次点击按钮 用户页面回退再次提交 微服务互相调用由于网络问题导致请求失败。feign 触发重试机制 其他业务情况 3什么情况下需要幂等性 以 SQL 为例有些操作是天然幂等的。 SELECT * FROM table WHER id?无论执行多少次都不会改变状态是天然的幂等。 UPDATE tab1 SET col11 WHERE col22无论执行成功多少次状态都是一致的也是幂等操作。 delete from user where userid1多次操作结果一样具备幂等性 insert into user(userid,name) values(1,a) 如 userid 为唯一主键即重复操作上面的业务只 会插入一条用户数据具备幂等性。 UPDATE tab1 SET col1col11 WHERE col22每次执行的结果都会发生变化不是幂等的。 insert into user(userid,name) values(1,a) 如 userid 不是主键可以重复那上面业务多次操 作数据都会新增多条不具备幂等性。 4幂等性解决方案 token 机制 1、服务端提供了发送 token 的接口。我们在分析业务的时候哪些业务是存在幂等问题的 就必须在执行业务前先去获取 token服务器会把 token 保存到 redis 中。 2、然后调用业务接口请求时把 token 携带过去一般放在请求头部。 3、服务器判断 token 是否存在 redis 中存在表示第一次请求然后删除 token,继续执行业 务。 4、如果判断 token 不存在 redis 中就表示是重复操作直接返回重复标记给 client这样 就保证了业务代码不被重复执行 危险性 1、先删除 token 还是后删除 token (1) 先删除可能导致业务确实没有执行重试还带上之前 token由于防重设计导致 请求还是不能执行。 (2) 后删除可能导致业务处理成功但是服务闪断出现超时没有删除 token别 人继续重试导致业务被执行两边 (3) 我们最好设计为先删除 token如果业务调用失败就重新获取 token 再次请求。 2、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各种锁机制 - 数据库悲观锁 select * from xxxx where id 1 for update; 悲观锁使用时一般伴随事务一起使用数据锁定时间可能会很长需要根据实际情况选用。 另外要注意的是id 字段一定是主键或者唯一索引不然可能造成锁表的结果处理起来会 非常麻烦。 - 数据库乐观锁 这种方法适合在更新的场景中 update t_goods set count count -1 , version version 1 where good_id2 and version 1 根据 version 版本也就是在操作库存前先获取当前商品的 version 版本号然后操作的时候 带上此 version 号。我们梳理下: 我们第一次操作库存时得到 version 为 1调用库存服务 version 变成了 2但返回给订单服务出现了问题订单服务又一次发起调用库存服务当订 单服务传如的 version 还是 1再执行上面的 sql 语句时就不会执行因为 version 已经变 为 2 了where 条件就不成立。这样就保证了不管调用几次只会真正的处理一次。 乐观锁主要使用于处理读多写少的问题 - 业务层分布式锁 如果多个机器可能在同一时间同时处理相同的数据比如多台机器定时任务都拿到了相同数 据处理我们就可以加分布式锁锁定此数据处理完成后释放锁。获取到锁的必须先判断 这个数据是否被处理过 各种唯一约束 - 数据库唯一约束 插入数据应该按照唯一索引进行插入比如订单号相同的订单就不可能有两条记录插入。 我们在数据库层面防止重复。 这个机制是利用了数据库的主键唯一约束的特性解决了在 insert 场景时幂等问题。但主键 的要求不是自增的主键这样就需要业务生成全局唯一的主键。 如果是分库分表场景下路由规则要保证相同请求下落地在同一个数据库和同一表中要 不然数据库主键约束就不起效果了因为是不同的数据库和表主键不相关。 - redis防重 很多数据需要处理只能被处理一次比如我们可以计算数据的 MD5 将其放入 redis 的 set 每次处理数据先看这个 MD5 是否已经存在存在就不处理。 - 防重表 使用订单号 orderNo 做为去重表的唯一索引把唯一索引插入去重表再进行业务操作且 他们在同一个事务中。这个保证了重复请求时因为去重表有唯一约束导致请求失败避 免了幂等问题。这里要注意的是去重表和业务表应该在同一库中这样就保证了在同一个 事务即使业务操作失败了也会把去重表的数据回滚。这个很好的保证了数据一致性。 之前说的 redis 防重也算 - 全局唯一id 调用接口时生成一个唯一 idredis 将数据保存到集合中去重存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id proxy_set_header X-Request-Id $request_id; 13、订单确认页防重令牌 在 购物车 点击 去结算 —— 服务器发送令牌给浏览器一份redis存一份 ——跳转到 订单确认页 在提交订单时我们就可以判断这个令牌防止幂等性。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mU4EZDFU-1675935821387)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/%E8%AE%A2%E5%8D%95%E7%A1%AE%E8%AE%A4%E9%A1%B5%E6%B5%81%E7%A8%8B.png)] // TODO 订单防重复令牌String token UUID.randomUUID().toString().replace(-,);orderConfirmVo.setOrderToken(token);redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIXmemberRespVo.getId(),token);14、提交订单 提交订单的整体流程 收集订单确认面的数据为创建订单为准备验证令牌是否是重复的请求。【获取令牌、验证令牌必须保证原子性操作。可使用LUA脚本或者Redisson】、验证令牌成功就是 创建订单的数据。包括订单信息【用户信息、收货人信息、积分信息、优惠信息】订单中每个订单项信息【订单信息、spu信息、sku信息、优惠信息、积分信息、价格信息】运费、订单总金额订单创建成功进行验价比较订单支付的金额与提交订单的金额是否一致。如果不一致说明价格发生变化提醒用户确认订单、验价完成就是锁定库存判断仓库中是否有足够的库存。 以上每一步失败都应该保证事务的回滚。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t6LYPAfq-1675935821387)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6(41)].png) 锁定库存流程 1、收集订单确认页的数据 Data public class OrderSubmitVo {// 收货地址idprivate Long addrId;// 支付类型。目前只使用在线支付private Integer patType;// 商品信息无需在这里封装在购物车中重新查询一次// 用户信息可以直接从 session 中获取// 应付金额 —— 验证价格private BigDecimal payPrice;// 防重令牌private String orderToken;// 备注private String notes; }2、构建完订单返回支付页面的vo Data public class OrderSubmitResponseVo {// 订单信息private OrderEntity order;// 订单提交失败 —— 错误状态码// 0 提交成功private Integer Code 0; } 3、锁定库存vo Data public class WareSkuLockVo {// 订单号private String orderSn;// 需要锁定的库存信息private ListOrderItemVo locks; }4、库存不足的异常、放到 common 模块下 public class NoStockException extends RuntimeException{private Long skuId;public NoStockException(Long skuId) {super(skuId 商品,没有足够的库存);}public NoStockException() {super(商品没有足够的库存);}public Long getSkuId() {return skuId;}public void setSkuId(Long skuId) {this.skuId skuId;} } 5、库存不足响应码、响应信息 NO_STOCK_EXCEPTION(21000,商品库存不足),6、 controller 层处理提交订单的请求 /** 提交订单* */PostMapping(/submitOrder)public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {try {OrderSubmitResponseVo 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;}}7、验证令牌、创建订单、验证价格、锁定库存 OverrideTransactional(rollbackFor NoStockException.class) // 保证事务回滚public OrderSubmitResponseVo submitOrder(OrderSubmitVo vo) throws NoStockException{OrderSubmitResponseVo responseVo new OrderSubmitResponseVo();MemberRespVo memberRespVo OrderInterceptor.threadLocal.get();// 1、验证令牌【必须保证获取令牌、删除令牌的原子性】String orderToken vo.getOrderToken();// LUA 脚本: 0代表删除失败1 代表删除成功String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;// 执行 LUA 脚本Long execute redisTemplate.execute(new DefaultRedisScriptLong(script, Long.class),Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX memberRespVo.getId()),orderToken);if (execute ! null execute 0) {// 令牌验证失败responseVo.setCode(1);return responseVo;}else{// 令牌验证成功: 创建订单、验证价格、锁定库存....// 1、创建订单CreatedOrderTo orderTo createOrder(vo);// 2、验证价格BigDecimal payPrice vo.getPayPrice();BigDecimal payAmount orderTo.getOrder().getPayAmount();if (Math.abs(payAmount.subtract(payPrice).doubleValue()) 0.01) {// 3、验价成功保存订单saveOrder(orderTo);// 4、锁定库存库存不足抛出异常并回滚事务WareSkuLockVo wareSkuLockVo new WareSkuLockVo();ListOrderItemVo locks createOrderItems(orderTo.getOrder().getOrderSn()).stream().map(item - {OrderItemVo orderItemVo new OrderItemVo();orderItemVo.setCount(item.getSkuQuantity());orderItemVo.setSkuId(item.getSkuId());orderItemVo.setTitle(item.getSkuName());return orderItemVo;}).collect(Collectors.toList());wareSkuLockVo.setOrderSn(orderTo.getOrder().getOrderSn());wareSkuLockVo.setLocks(locks);R r wareFeignService.orderLockStock(wareSkuLockVo);if (r.getCode() 0) {// 锁定成功responseVo.setOrder(orderTo.getOrder());return responseVo;}else {// 锁定失败.抛出异常responseVo.setCode(3);throw new NoStockException();}}else {// 验价失败responseVo.setCode(2);return responseVo;}}// 这种方式不能保证原子性。应该使用 LUA脚本或者Redisson分布式锁/* String redisToken (String) redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX memberRespVo.getId());if (orderToken ! null orderToken.equals(redisToken)) {// 令牌验证通过}else {// 令牌验证失败}*/}/** 保存订单、订单项* */private void saveOrder(CreatedOrderTo orderTo) {OrderEntity order orderTo.getOrder();order.setCreateTime(new Date());// 保存订单this.save(order);// 保存订单项orderItemService.saveBatch(orderTo.getItems());}/** 创建 CreatedOrderTo* */private CreatedOrderTo createOrder(OrderSubmitVo vo) {CreatedOrderTo orderTo new CreatedOrderTo();// 1、构建订单OrderEntity orderEntity createOrderEntity(vo);// 创建订单号: MyBatis-Plus 中自动生成String orderSn IdWorker.getTimeId();orderEntity.setOrderSn(orderSn);// 2、构建订单项ListOrderItemEntity items createOrderItems(orderSn);// 3、计算订单价格assert items ! null;computePrice(items,orderEntity);orderTo.setOrder(orderEntity);orderTo.setItems(items);return orderTo;}/** 计算订单的价格* */private void computePrice(ListOrderItemEntity items, OrderEntity orderEntity) {// 总价BigDecimal total new BigDecimal(0.0);// 优惠券优惠分解金额BigDecimal coupon new BigDecimal(0.0);// 积分优惠分解金额BigDecimal integration new BigDecimal(0.0);// 商品促销分解金额BigDecimal promotion new BigDecimal(0.0);// 积分、成长值Integer integrationTotal 0;Integer growthTotal 0;// 循环叠加每一个订单项的优惠价格、订单总价格、积分、成长信息for (OrderItemEntity orderItem : items) {// 优惠价格coupon coupon.add(orderItem.getCouponAmount());promotion promotion.add(orderItem.getPromotionAmount());integration integration.add(orderItem.getIntegrationAmount());// 订单总价格total total.add(orderItem.getRealAmount());//积分信息和成长值信息integrationTotal orderItem.getGiftIntegration();growthTotal orderItem.getGiftGrowth();}// 设置订单的总价格、优惠价格、积分信息orderEntity.setTotalAmount(total);orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));orderEntity.setCouponAmount(coupon);orderEntity.setPromotionAmount(promotion);orderEntity.setIntegrationAmount(integration);orderEntity.setIntegration(integrationTotal);orderEntity.setGrowth(growthTotal);//设置删除状态(0-未删除1-已删除)orderEntity.setDeleteStatus(0);}/** 构建 OrderEntity* */NotNullprivate OrderEntity createOrderEntity(OrderSubmitVo vo) {// 一、构建订单数据OrderEntity orderEntity new OrderEntity();// 1、订单收货信息、运费R r wareFeignService.fare(vo.getAddrId());if (r.getCode() 0) {FareVo fareVo r.getData(data, new TypeReferenceFareVo() {});orderEntity.setFreightAmount(fareVo.getFare());orderEntity.setMemberId(fareVo.getAddress().getMemberId());orderEntity.setReceiverPhone(fareVo.getAddress().getPhone());orderEntity.setReceiverCity(fareVo.getAddress().getCity());orderEntity.setReceiverName(fareVo.getAddress().getName());orderEntity.setReceiverDetailAddress(fareVo.getAddress().getDetailAddress());orderEntity.setReceiverPostCode(fareVo.getAddress().getPostCode());orderEntity.setReceiverProvince(fareVo.getAddress().getProvince());orderEntity.setReceiverRegion(fareVo.getAddress().getRegion());}// 2、设置订单相关的状态信息orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());orderEntity.setAutoConfirmDay(7);orderEntity.setConfirmStatus(0);return orderEntity;}/** createdOrderItems:构建所有的订单项* */private ListOrderItemEntity createOrderItems(String orderSn) {// 购物车中的所有购物项ListOrderItemVo orderItemVos cartFeignService.currentUserCartItems();if ( orderItemVos ! null orderItemVos.size() 0 ) {// 将 orderItemVo —— orderItemEntityListOrderItemEntity items orderItemVos.stream().map(orderItemVo - {// 构建订单项OrderItemEntity orderItemEntity createOrderItem(orderItemVo);orderItemEntity.setOrderSn(orderSn);return orderItemEntity;}).collect(Collectors.toList());return items;}return null ;}/** orderItemVo 构建每个订单项* */private OrderItemEntity createOrderItem(OrderItemVo orderItemVo) {OrderItemEntity orderItemEntity new OrderItemEntity();// 1、订单信息 √// 2、spu信息 —— 远程调用R r productFeignService.getSpuInfoBySkuId(orderItemVo.getSkuId());if (r.getCode() 0) {SpuInfoVo spuInfoVo r.getData(data, new TypeReferenceSpuInfoVo(){});orderItemEntity.setSpuId(spuInfoVo.getId());orderItemEntity.setSpuName(spuInfoVo.getSpuName());orderItemEntity.setCategoryId(spuInfoVo.getCatelogId());orderItemEntity.setSpuBrand(spuInfoVo.getBrandId().toString());}// 3、sku信息orderItemEntity.setSkuId(orderItemVo.getSkuId());orderItemEntity.setSkuName(orderItemVo.getTitle());orderItemEntity.setSkuPic(orderItemVo.getDefaultImage());// collectionToDelimitedString spring提供的可以将list集合按照指定字符拼接成string字符串orderItemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(orderItemVo.getSkuAttr(),;));orderItemEntity.setSkuQuantity(orderItemVo.getCount());orderItemEntity.setSkuPrice(orderItemVo.getPrice());// 4、优惠信息 —— 省略// 5、积分信息orderItemEntity.setGiftIntegration(orderItemVo.getPrice().intValue());orderItemEntity.setGiftGrowth(orderItemVo.getPrice().intValue());// 6、每个订单项的价格 sku价格 * sku数量// 商品促销分解金额orderItemEntity.setPromotionAmount(BigDecimal.ZERO);// 优惠券优惠分解金额orderItemEntity.setCouponAmount(new BigDecimal(0.0));// 积分优惠分解金额orderItemEntity.setIntegrationAmount(new BigDecimal(0.0));// 订单项最终的价格BigDecimal initPrice orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity()));BigDecimal finalPrice initPrice.subtract(orderItemEntity.getPromotionAmount()).subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount());orderItemEntity.setRealAmount(finalPrice);return orderItemEntity;} 令牌key public class OrderConstant {public static final String USER_ORDER_TOKEN_PREFIX order:token:; }8、远程调用接口 FeignClient(gulimall-product) public interface ProductFeignService {/** 根据 skuId 获取 spu 信息* */GetMapping(product/spuinfo/{skuId})public R getSpuInfoBySkuId(PathVariable(skuId)Long skuId); } FeignClient(gulimall-ware) public interface WareFeignService {/** 查询sku是否有库存* */PostMapping(ware/waresku/hasStock)public HashMapLong, Boolean hasStock(RequestBody ListLong skuIds);/** 计算运费* */GetMapping(ware/wareinfo/fare)public R fare(RequestParam(addrId)Long addrId);/** 锁定库存* */PostMapping(ware/wareinfo/lock/order)public R orderLockStock(RequestBody WareSkuLockVo vo); }9、product 模块对应的远程调用方法 /** 根据 skuId 获取 spu 信息* */GetMapping(/{skuId})public R getSpuInfoBySkuId(PathVariable(skuId)Long skuId) {SpuInfoEntity spuInfo spuInfoService.getSpuInfoBySkuId(skuId);return R.ok().setData(spuInfo);}Overridepublic SpuInfoEntity getSpuInfoBySkuId(Long skuId) {SkuInfoEntity skuInfo skuInfoService.getById(skuId);SpuInfoEntity spuInfo this.getById(skuInfo.getSpuId());return spuInfo;}10、ware 模块对应的远程调用方法 WareSkuController /** 查询sku是否有库存* */PostMapping(/hasStock)public HashMapLong, Boolean hasStock(RequestBody ListLong skuIds) {HashMapLong, Boolean map wareSkuService.getSkusHasStock(skuIds);return map;}/** 查询 sku 是否有库存* */Overridepublic HashMapLong, Boolean getSkusHasStock(ListLong skuIds) {HashMapLong, Boolean map new HashMap();for (Long skuId : skuIds) {// 查询库存: 当前库存 - 锁定库存// SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id 1Integer count baseMapper.getSkusHasStock(skuId);map.put(skuId,count ! null count0);}return map;}对应的 mapper 映射文件 select idgetSkusHasStock resultTypejava.lang.IntegerSELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id #{id}/selectWareInfoController /** 锁定库存* */PostMapping(/lock/order)public R orderLockStock(RequestBody WareSkuLockVo vo) {try {Boolean result wareSkuService.orderLockStock(vo);return R.ok();} catch (NoStockException e) {return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(),BizCodeEnum.NO_STOCK_EXCEPTION.getMessage());}}}/** 计算运费* */GetMapping(/fare)public R fare(RequestParam(addrId)Long addrId) {FareVo fare wareInfoService.getFare(addrId);return R.ok().setData(fare);}Overridepublic FareVo getFare(Long addrId) {R r memberFeignService.info(addrId);MemberAddressVo data r.getData(memberReceiveAddress, new TypeReferenceMemberAddressVo() {});if (data ! null) {FareVo fareVo new FareVo();String phone data.getPhone();// 使用手机号最后一位作为运费。String fareString phone.substring(phone.length() - 1, phone.length());BigDecimal fare new BigDecimal(fareString);fareVo.setFare(fare);fareVo.setAddress(data);return fareVo;}return null;}/** 锁定库存* */OverrideTransactional(rollbackFor NoStockException.class )public Boolean orderLockStock(WareSkuLockVo vo) {// 1、找到商品在哪个仓库有库存ListOrderItemVo locks vo.getLocks();ListSkuWareHasStock hasStocks locks.stream().map(item - {SkuWareHasStock stock new SkuWareHasStock();stock.setSkuId(item.getSkuId());// 查询哪些仓库有库存。ListLong wareIds baseMapper.listWareIdHasStock(item.getSkuId());stock.setWareId(wareIds);stock.setNum(item.getCount());return stock;}).collect(Collectors.toList());// 2、锁定库存for (SkuWareHasStock hasStock : hasStocks) {// 标志位表示当前商品是否被锁住Boolean skuStocked false;Long skuId hasStock.getSkuId();ListLong wareIds hasStock.getWareId();if (wareIds null || wareIds.size() 0) {// 该商品没有库存,直接抛出异常throw new NoStockException(skuId);}for (Long wareId : wareIds) {// 锁定库存,成功返回 1失败返回0Long count baseMapper.lockSkuStock(skuId,wareId,hasStock.getNum());if (count 1){// 当前商品锁定成功skuStocked true;break;}else {// 当前商品锁定失败,}}// 当前商品所有仓库都没有锁定成功if (!skuStocked) {throw new NoStockException(skuId);}}return true;}}/* * 哪个仓库有对应的商品 * */ Data class SkuWareHasStock {private Long skuId;// 锁多少件private Integer num ;private ListLong wareId; }对应的 Mapper 映射文件 !--锁定库存--update idlockSkuStockUPDATE wms_ware_sku SET stock_locked stock_locked #{num}WHERE sku_id #{skuId} AND ware_id #{wareId} AND stock - stock_locked #{num}/update!--查询哪些仓库有对应商品的库存--select idlistWareIdHasStock resultTypejava.lang.LongSELECT ware_id FROM wms_ware_sku WHERE sku_id #{skuId} AND stock - stock_locked 0/select二、分布式事务 在提交订单中使用本地事务进行事务回滚但是在分布式下可能会出现一些问题 1、远程服务假失败 ​ 远程服务其实成功了由于网络故障等没有返回 ​ 导致订单回滚库存却扣减 2、远程服务执行完成下面的其他方法出现问题 ​ 导致已执行的远程请求肯定不能回滚 本地事务只能保证在同一个服务同一个数据库中的回滚事务无法感知其他服务中出现的异常。 1、本地事务 1事务的基本性质 数据库事务的几个特性原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily)简称就是 ACID 原子性一系列的操作整体不可拆分要么同时成功要么同时失败一致性数据在事务的前后业务整体一致。 转账。A:1000B:1000 转 200 事务成功; A800 B1200 隔离性事务之间互相隔离。持久性一旦事务成功数据一定会落盘在数据库。 在以往的单体应用中我们多个业务操作使用同一条连接操作不同的数据表一旦有异常 我们可以很容易的整体回滚 Business我们具体的业务代码 Storage库存业务代码扣库存 Order订单业务代码保存订单 Account账号业务代码减账户余额 比如买东西业务扣库存下订单账户扣款是一个整体必须同时成功或者失败 一个事务开始代表以下的所有操作都在同一个连接里面 2事务的隔离级别 READ UNCOMMITTED读未提交 该隔离级别的事务会读到其它未提交事务的数据此现象也称之为脏读。 READ COMMITTED读提交 一个事务可以读取另一个已提交的事务多次读取会造成不一样的结果此现象称为不可重 复读问题Oracle 和 SQL Server 的默认隔离级别。 REPEATABLE READ可重复读 该隔离级别是 MySQL 默认的隔离级别在同一个事务里select 的结果是事务开始时时间 点的状态因此同样的 select 操作读到的结果会是一致的但是会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制参考下文行锁的算法一节来避免幻读。 SERIALIZABLE序列化 在该隔离级别下事务都是串行顺序执行的MySQL 数据库的 InnoDB 引擎会给读操作隐式 加一把读共享锁从而避免了脏读、不可重读复读和幻读问题。 3事务的传播行为 1、PROPAGATION_REQUIRED如果当前没有事务就创建一个新事务如果当前存在事务 就加入该事务该设置是最常用的设置。 2、PROPAGATION_SUPPORTS支持当前事务如果当前存在事务就加入该事务如果当 前不存在事务就以非事务执行。 3、PROPAGATION_MANDATORY支持当前事务如果当前存在事务就加入该事务如果 当前不存在事务就抛出异常。 4、PROPAGATION_REQUIRES_NEW创建新事务无论当前存不存在事务都创建新事务。 5、PROPAGATION_NOT_SUPPORTED以非事务方式执行操作如果当前存在事务就把当 前事务挂起。 6、PROPAGATION_NEVER以非事务方式执行如果当前存在事务则抛出异常。 7、PROPAGATION_NESTED如果当前存在事务则在嵌套事务内执行。如果当前没有事务 则执行与 PROPAGATION_REQUIRED 类似的操作。 // b 和 a 共享一个事务。c是一个新事物Transactional(timeout 20)public void a() {b();c();}// 由于 b 和 a 共享一个事务因此 b 事务中的所有配置都没有用。Transactional(propagation Propagation.REQUIRED,timeout 2)public void b() {}Transactional(propagation Propagation.REQUIRES_NEW)public void c() {}SpringBoot 中事务的坑 在同一个类里面编写两个方法内部调用的时候会导致事务设置失效。原因是没有用到 代理对象的缘故。 比如 a、b、c都在一个类中b和c事务的设置不会起作用。相当于将 b、c 中的代码都合并到 a中 Transactional(timeout 20)public void a() {b();c();}Transactional(propagation Propagation.REQUIRED,timeout 2)public void b() {System.out.println(b);}Transactional(propagation Propagation.REQUIRES_NEW)public void c() {System.out.println(c);} 等价于Transactional(timeout 20)public void a() {System.out.println(b);System.out.println(c);}解决使用动态代理调用方法 0、导入 spring-boot-starter-aop 1、EnableTransactionManagement(proxyTargetClass true) 2、EnableAspectJAutoProxy(exposeProxytrue) 3、AopContext.currentProxy() Transactional(timeout 20)public void a() {// 可直接转换为任意对象OrderServiceImpl orderService (OrderServiceImpl) AopContext.currentProxy();orderService.b();orderService.c();}Transactional(propagation Propagation.REQUIRED,timeout 2)public void b() {System.out.println(b);}Transactional(propagation Propagation.REQUIRES_NEW)public void c() {System.out.println(c);}2、分布式事务 1为什么出现分布式事务 分布式系统经常出现的异常 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失… 分布式事务是企业集成中的一个技术难点也是每一个分布式系统架构中都会涉及到的一个 东西特别是在微服务架构中几乎可以说是无法避免。 2CAP 理论 CAP 原则又称 CAP 定理指的是在一个分布式系统中 一致性Consistency 在分布式系统中的所有数据备份在同一时刻是否同样的值。等同于所有节点访 问同一份最新的数据副本 可用性Availability 在集群中一部分节点故障后集群整体是否还能响应客户端的读写请求。对数据 更新具备高可用性 分区容错性Partition tolerance 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区partition。 分区容错的意思是区间通信可能失败。比如一台服务器放在中国另一台服务 器放在美国这就是两个区它们之间可能无法通信。 CAP 原则指的是这三个要素最多只能同时实现两点不可能三者兼顾。 一般来说分区容错无法避免因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们 剩下的 C 和 A 无法同时做到。 如果想要达到一致性就必须让C节点不可用 分布式事务下如何保证数据的一致性的 raft 算法、paxos算法 以 raft 算法为例 raft 算法主要是通过 领导选举、日志复制 的方式来达到一致性。节点的三种状态随从者、候选者、领导者 在所有节点中选取一个 领导者 选举的方式主要是通过 投票选举 , 领导者将客户端发送来的数据保存在日志当中, 向其他节点发送 日志 保存数据。 1、如何选举领导者 所有的节点在初始状态下都是 随从者如果随从者在 指定的时间 内没有监听到 领导者 发送的信息就会认为没有领导者其中一个随从者就会变成候选者, 候选者会向其他随从者送选票请求票数超过一半该候选者就会变成 领导者。所有系统中的请求都是通过领导者然后领导者发送给其他随从者。 2、raft 中的俩种超时时间 第一种选举超时时间随从者等待变成候选者的超时时间默认是 150~300ms。 第二种心跳超时时间领导者向随从者发送日志的间隔时间。 如果有多个候选者就会重新进行选举机制。 raft 动画演示http://thesecretlivesofdata.com/raft/ 3面临的额问题 对于多数大型互联网应用的场景主机众多、部署分散而且现在的集群规模越来越大所 以节点故障、网络故障是常态而且要保证服务可用性达到 99.99999%N 个 9即保证 P 和 A舍弃 C。 4BASE 理论 是对 CAP 理论的延伸思想是即使无法做到强一致性CAP 的一致性就是强一致性但可 以采用适当的采取弱一致性即最终一致性。 BASE 理论是指 基本可用Basically Available 基本可用是指分布式系统在出现故障的时候允许损失部分可用性例如响应时间、 功能上的可用性允许损失部分可用性。需要注意的是基本可用绝不等价于系 统不可用。 响应时间上的损失正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的 查询结果但由于出现故障比如系统部分机房发生断电或断网故障查询 结果的响应时间增加到了 1~2 秒。 功能上的损失购物网站在购物高峰如双十一时为了保护系统的稳定性 部分消费者可能会被引导到一个降级页面。 软状态 Soft State 软状态是指允许系统存在中间状态而该中间状态不会影响系统整体可用性。分布 式存储中一般一份数据会有多个副本允许不同副本同步的延时就是软状态的体 现。mysql replication 的异步复制也是一种体现。 最终一致性 Eventual Consistency 最终一致性是指系统中的所有数据副本经过一定时间后最终能够达到一致的状 态。弱一致性和强一致性相反最终一致性是弱一致性的一种特殊情况。 5 分布式事务的几种解决方案 2PC模式 数据库支持的 2PC【2 phase commit 二阶提交】又叫做 XA Transactions。 MySQL 从 5.5 版本开始支持SQL Server 2005 开始支持Oracle 7 开始支持。 其中XA 是一个两阶段提交协议该协议分为以下两个阶段 第一阶段事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作并反映是 否可以提交. 第二阶段事务协调器要求每个数据库提交数据。 其中如果有任何一个数据库否决此次提交那么所有数据库都会被要求回滚它们在此事务 中的那部分信息 XA 协议比较简单而且一旦商业数据库实现了 XA 协议使用分布式事务的成本也比较 低。XA 性能不理想特别是在交易下单链路往往并发量很高XA 无法满足高并发场景XA 目前在商业数据库支持的比较理想在 mysql 数据库中支持的不太理想mysql 的 XA 实现没有记录 prepare 阶段日志主备切换回导致主库与备库数据不一致。许多 nosql 也没有支持 XA这让 XA 的应用场景变得非常狭隘。也有 3PC引入了超时机制无论协调者还是参与者在向对方发送请求后若长时间 未收到回应则做出相应处理 柔性事务-TCC事务补偿型方案 刚性事务遵循 ACID 原则强一致性。 柔性事务遵循 BASE 理论最终一致性 与刚性事务不同柔性事务允许一定时间内不同节点的数据不一致但要求最终一致。 一阶段 prepare 行为调用 自定义 的 prepare 逻辑。 二阶段 commit 行为调用 自定义 的 commit 逻辑。 二阶段 rollback 行为调用 自定义 的 rollback 逻辑。 所谓 TCC 模式是指支持把 自定义 的分支事务纳入到全局事务的管理中。 柔性事务- 最大努力型通知方案 按规律进行通知不保证数据一定能通知成功但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时比如调用微信或支付宝支付后的支付结果通知。这种 方案也是结合 MQ 进行实现例如通过 MQ 发送 http 请求设置最大通知次数。达到通 知次数后即不再通知。 案例银行通知、商户通知等各大交易业务平台间的商户通知多次通知、查询校对、对 账文件支付宝的支付成功异步回调 柔性事务- 可靠消息 最终一致性方案异步确保型 实现业务处理服务在业务事务提交之前向实时消息服务请求发送消息实时消息服务只 记录消息数据而不是真正的发送。业务处理服务在业务事务提交之后向实时消息服务确 认发送。只有在得到确认发送指令后实时消息服务才会真正发送。 3、Seata Seata 是一款开源的分布式事务解决方案致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式为用户打造一站式的分布式解决方案。 Transaction Coordinator (TC事务协调器维护全局事务的运行状态负责协调并驱动全局事务的提交或回滚 Transaction Manager ™控制全局事务的边界负责开启一个全局事务并最终发起全局提交或全局回滚的决议 Resource Manager (RM控制分支事务负责分支注册、状态汇报并接收事务协调器的指令驱动分支本地事务的提交和回滚 TC 负责全局的事务TM 是控制所有的RM。RM 是每个服务中的本地事务。 首先 Business 服务会向 TC 请求开启一个大事务就着 Business 远程调用 Stock 服务Stock 服务会将自己的分支事务注册到 TC并实时通知 TC 自己事务的状态。调用 Order 服务同样也会通知 TC 自己的事务状态。 当调用Account 服务出现错误回滚时TC 会通知 Stock、Order 都进行回滚。 每个服务的数据库都应该有 undo_log 表用来记录本服务的数据库操作记录TC 也会根据这个日志进行回滚。 总结分布式事务的执行过程 TM 开启分布式事务TM 向 TC 注册全局事务记录按业务场景编排数据库、服务等事务内资源RM 向 TC 汇报资源准备状态 TM 结束分布式事务事务一阶段结束TM 通知 TC 提交/回滚分布式事务TC 汇总事务信息决定分布式事务是提交还是回滚TC 通知所有 RM 提交/回滚 资源事务二阶段结束。 11.4.2 Seata Nacos 搭建使用 seata 与 SpringCloudAlibaba、SpringBoot、SpringCloud 版本对照说明 seata 服务端配置 1、下载seata-server 1.4.2 Releases · seata/seata (github.com) 2、解压进入到 conf 目录会看到俩个核心配置文件file.conf 、registry.conf registry.conf : 配置 seata 的注册中心 : 支持 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 配置 seata 的配置中心支持 file、nacos 、apollo、zk、consul、etcd3 使用 nacos 作为配置中心注册中心 // 注册中心使用Nacos registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype nacosnacos {application seata-serverserverAddr 127.0.0.1:8848group SEATA_GROUPnamespace cluster defaultusername nacospassword nacos} // 配置中心使用 Nacos、自1.4.2版本开始seata支持使用 dataId的方式获取配置 config {# file、nacos 、apollo、zk、consul、etcd3type nacosnacos {serverAddr 127.0.0.1:8848// 配置文件命名空间默认是PUBLICnamespace // 配置文件组名group SEATA_GROUPusername nacospassword nacos// 配置文件的 dataIddataId seataServer.properties}file.conf : seata 的服务端配置只有 配置中心的类型是 file 时才会用用到 在 registry.conf 中已经配置了nacos作为配置中心。此文件不修改。 3、seata Server 端的存储模式有三种 file、db、redis , 我们使用 Mysql 存储 1建数据库建表 sql 获取地址seata/mysql.sql at develop · seata/seata · GitHub -- -------------------------------- The script used when storeMode is db -------------------------------- -- the table to store GlobalSession data CREATE DATABASE IF NOT EXISTS seata; USE seata; CREATE TABLE IF NOT EXISTS global_table (xid VARCHAR(128) NOT NULL,transaction_id BIGINT,status TINYINT NOT NULL,application_id VARCHAR(32),transaction_service_group VARCHAR(32),transaction_name VARCHAR(128),timeout INT,begin_time BIGINT,application_data VARCHAR(2000),gmt_create DATETIME,gmt_modified DATETIME,PRIMARY KEY (xid),KEY idx_status_gmt_modified (status , gmt_modified),KEY idx_transaction_id (transaction_id) ) ENGINE InnoDBDEFAULT CHARSET utf8mb4;-- the table to store BranchSession data CREATE TABLE IF NOT EXISTS branch_table (branch_id BIGINT NOT NULL,xid VARCHAR(128) NOT NULL,transaction_id BIGINT,resource_group_id VARCHAR(32),resource_id VARCHAR(256),branch_type VARCHAR(8),status TINYINT,client_id VARCHAR(64),application_data VARCHAR(2000),gmt_create DATETIME(6),gmt_modified DATETIME(6),PRIMARY KEY (branch_id),KEY idx_xid (xid) ) ENGINE InnoDBDEFAULT CHARSET utf8mb4;-- the table to store lock data CREATE TABLE IF NOT EXISTS lock_table (row_key VARCHAR(128) NOT NULL,xid VARCHAR(128),transaction_id BIGINT,branch_id BIGINT NOT NULL,resource_id VARCHAR(256),table_name VARCHAR(32),pk VARCHAR(36),status TINYINT NOT NULL DEFAULT 0 COMMENT 0:locked ,1:rollbacking,gmt_create DATETIME,gmt_modified DATETIME,PRIMARY KEY (row_key),KEY idx_status (status),KEY idx_branch_id (branch_id),KEY idx_xid (xid) ) ENGINE InnoDBDEFAULT CHARSET utf8mb4;CREATE TABLE IF NOT EXISTS distributed_lock (lock_key CHAR(20) NOT NULL,lock_value VARCHAR(20) NOT NULL,expire BIGINT,primary key (lock_key) ) ENGINE InnoDBDEFAULT CHARSET utf8mb4;INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES (AsyncCommitting, , 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES (RetryCommitting, , 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES (RetryRollbacking, , 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES (TxTimeoutCheck, , 0);2在 Nacos 中创建seataServer.properties配置文件分组名、dataId、命名空间 要和上面在 registry.conf 中配置 保持一致。 seata使用1.4.2版本新建的data id文件类型选择properties。若是使用seata1.4.2之前的版本以下的每个配置项在nacos中就是一个条目需要使用script/config-center/nacos/下的nacos-config.shlinux或者windows下装git或者nacos-config.pypython脚本执行上传注册 配置文件获取seata/config.txt at develop · seata/seata · GitHub #For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html #Transport configuration, for client and server transport.typeTCP transport.serverNIO transport.heartbeattrue transport.enableTmClientBatchSendRequestfalse transport.enableRmClientBatchSendRequesttrue transport.enableTcServerBatchSendResponsefalse transport.rpcRmRequestTimeout30000 transport.rpcTmRequestTimeout30000 transport.rpcTcRequestTimeout30000 transport.threadFactory.bossThreadPrefixNettyBoss transport.threadFactory.workerThreadPrefixNettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefixNettyServerBizHandler transport.threadFactory.shareBossWorkerfalse transport.threadFactory.clientSelectorThreadPrefixNettyClientSelector transport.threadFactory.clientSelectorThreadSize1 transport.threadFactory.clientWorkerThreadPrefixNettyClientWorkerThread transport.threadFactory.bossThreadSize1 transport.threadFactory.workerThreadSizedefault transport.shutdown.wait3 transport.serializationseata transport.compressornone# service 配置 # 分布式事务组的名称default_tx_group service.vgroupMapping.default_tx_groupdefault #If you use a registry, you can ignore it service.default.grouplist127.0.0.1:8091 service.enableDegradefalse service.disableGlobalTransactionfalse#Transaction rule configuration, only for the client client.rm.asyncCommitBufferLimit10000 client.rm.lock.retryInterval10 client.rm.lock.retryTimes30 client.rm.lock.retryPolicyBranchRollbackOnConflicttrue client.rm.reportRetryCount5 client.rm.tableMetaCheckEnabletrue client.rm.tableMetaCheckerInterval60000 client.rm.sqlParserTypedruid client.rm.reportSuccessEnablefalse client.rm.sagaBranchRegisterEnablefalse client.rm.sagaJsonParserfastjson client.rm.tccActionInterceptorOrder-2147482648 client.tm.commitRetryCount5 client.tm.rollbackRetryCount5 client.tm.defaultGlobalTransactionTimeout60000 client.tm.degradeCheckfalse client.tm.degradeCheckAllowTimes10 client.tm.degradeCheckPeriod2000 client.tm.interceptorOrder-2147482648 client.undo.dataValidationtrue client.undo.logSerializationjackson client.undo.onlyCareUpdateColumnstrue server.undo.logSaveDays7 server.undo.logDeletePeriod86400000 client.undo.logTableundo_log client.undo.compress.enabletrue client.undo.compress.typezip client.undo.compress.threshold64k #For TCC transaction mode tcc.fence.logTableNametcc_fence_log tcc.fence.cleanPeriod1h#Log rule configuration, for client and server log.exceptionRate100# 存储模式配置。 # 存储模式改为 db store.modedb store.lock.modefile store.session.modefile #Used for password encryption store.publicKey# 如果存储模式是file在这里配置 store.file.dirfile_store/data store.file.maxBranchSessionSize16384 store.file.maxGlobalSessionSize512 store.file.fileWriteBufferCacheSize16384 store.file.flushDiskModeasync store.file.sessionReloadReadSize100# 如果存储模式是db在这里配置 store.db.datasourcedruid store.db.dbTypemysql # 如果是 mysql5.7 使用 com.mysql.jdbc.Driver store.db.driverClassNamecom.mysql.cj.jdbc.Driver store.db.urljdbc:mysql://your ip:3306/seata?useUnicodetruerewriteBatchedStatementstrue store.db.useryour mysql username store.db.passwordyour mysql password store.db.minConn5 store.db.maxConn30 store.db.globalTableglobal_table store.db.branchTablebranch_table store.db.distributedLockTabledistributed_lock store.db.queryLimit100 store.db.lockTablelock_table store.db.maxWait5000# 如果存储模式是redis在这里配置 store.redis.modesingle store.redis.single.host127.0.0.1 store.redis.single.port6379 store.redis.sentinel.masterName store.redis.sentinel.sentinelHosts store.redis.maxConn10 store.redis.minConn1 store.redis.maxTotal100 store.redis.database0 store.redis.password store.redis.queryLimit100# server 端配置 server.recovery.committingRetryPeriod1000 server.recovery.asynCommittingRetryPeriod1000 server.recovery.rollbackingRetryPeriod1000 server.recovery.timeoutRetryPeriod1000 server.maxCommitRetryTimeout-1 server.maxRollbackRetryTimeout-1 server.rollbackRetryTimeoutUnlockEnablefalse server.distributedLockExpireTime10000 server.xaerNotaRetryTimeout60000 server.session.branchAsyncQueueSize5000 server.session.enableBranchAsyncRemovefalse server.enableParallelRequestHandlefalse# Metrics configuration, only for the server metrics.enabledfalse metrics.registryTypecompact metrics.exporterListprometheus metrics.exporterPrometheusPort98983双击 /bin/seata-server.bat 启动启动之前确保 nacos 启动成功。 启动成功seara-server 会注册到 nacos 中。 seata 客户端配置 1、在使用分布式事务的数据库中创建 undo_log , 用来记录数据库操作进行回滚 -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS undo_log (branch_id BIGINT NOT NULL COMMENT branch transaction id,xid VARCHAR(128) NOT NULL COMMENT global transaction id,context VARCHAR(128) NOT NULL COMMENT undo_log context,such as serialization,rollback_info LONGBLOB NOT NULL COMMENT rollback info,log_status INT(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created DATETIME(6) NOT NULL COMMENT create datetime,log_modified DATETIME(6) NOT NULL COMMENT modify datetime,UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDBAUTO_INCREMENT 1DEFAULT CHARSET utf8 COMMENT AT transaction mode undo table; 2、引入依赖 seata-spring-boot-starter 版本 要与 seata-server 版本一致 !--seata--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactIdexclusions!-- 要与seata服务端版本一直,所以把自带的替换掉 --exclusiongroupIdio.seata/groupIdartifactIdseata-spring-boot-starter/artifactId/exclusion/exclusions/dependencydependencygroupIdio.seata/groupIdartifactIdseata-spring-boot-starter/artifactIdversion1.4.2/version/dependency3、yaml 中配置 # seata 客户端配置 seata:enabled: trueenable-auto-data-source-proxy: true #是否开启数据源自动代理,默认为truetx-service-group: default_tx_group #要与 seataServer.properties 配置文件中的 vgroupMapping 一致registry: #registry根据seata服务端的registry配置type: nacos #默认为 filenacos:application: seata-server #配置自己的seata服务server-addr: localhost:8848username: nacospassword: nacosnamespace: PUBLICgroup: SEATA_GROUPcluster: default # 配置自己的seata服务cluster, 默认为 defaultconfig:type: nacos #默认file,如果使用 file不配置下面的nacos,直接配置 seata.servicenacos:server-addr: localhost:8848 #配置自己的nacos地址group: SEATA_GROUP #配置自己的devusername: nacospassword: nacosnamespace: PUBLICdataId: seataServer.properties##配置自己的dataId,由于搭建服务端时把客户端的配置也写在了seataServer.properties,所以这里用了和服务端一样的配置文件,实际客户端和服务端的配置文件分离出来更好 4、在启动类上使用自动代理 EnableAutoDataSourceProxy // seata自动数据源代理5、在业务的方法入口使用 GlobalTransactiona 。哪个微服务使用这个注解哪个服务就是 TM name给定全局事务实例的名称随便取唯一即可rollbackFor当发生什么样的异常时进行回滚noRollbackFor发生什么样的异常不进行回滚。 每个 RM也就是事务的参与者使用 Transactional 注解 20.9 版本 Seata 使用 该项目使用的是 0.9 版本的 seata 1、服务端 使用 file 方式存储file.conf 作为配置中心仅需要修改 registry.conf 中的注册中心地址。 2、客户端引入依赖 !--seata分布式事务--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactIdexclusionsexclusionartifactIdseata-all/artifactIdgroupIdio.seata/groupId/exclusion/exclusions/dependencydependencygroupIdio.seata/groupIdartifactIdseata-all/artifactIdversion0.9.0/version/dependency3、使用分布式事务的服务拷贝 conf 目录下的 file.conf、registry.conf 到服务的resources 目录下 4、配置分布式事务组名 # 配置seata事务组名称需要和 seata-sever中的file.conf中对应 spring.cloud.alibaba.seata.tx-service-groupmy_test_tx_group5、手动配置数据源seata1.4.2直接使用注解即可低版本需要手动注入 Configuration public class MySeataConfig {Autowiredprivate DataSourceProperties properties;/** 在 seata 0.9版本之前使用seata必须配置数据源* */Beanpublic DataSource dataSource(DataSourceProperties properties) {HikariDataSource dataSource properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(properties.getName())) {dataSource.setPoolName(properties.getName());}return new DataSourceProxy(dataSource);} }8、在业务的方法入口使用全局事务注解GlobalTransactiona 。 再看 TC、TM、RM 三大组件 TCseata服务器 我们电脑上启动的seata TM事物的发起者业务的入口。 哪个微服务使用了**GlobalTransactional**哪个就是TMRM事务的参与者一个数据库就是一个RM。 三、使用延迟队列自动解锁库存 在上面虽然使用 seata 的 AT模式 解决了 订单的事务问题但是在高并发下 seata 的性能并不是很高seata 的 AT 使用加锁方式使业务变成串行化比较适用于后台管理系统这种并发性不高的分布式事务问题。 通常的方式使用 消息延迟队列达到最终的一致的性。 当库存锁定成功时向交换机发送一条消息【订单号、锁定库存状态、锁了几个库存…】根据 路由键交换机将订单消息转发到 延迟队列中到达指定 TTL 之后会判断订单状态是否需要解锁库存。需要解锁库存将消息转发到解锁库存的服务即可 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pyQ51jiU-1675935821391)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/%E8%AE%A2%E5%8D%95%E6%B5%81%E7%A8%8B%E5%9B%BE.png)] 1、在 gulimall-ware 服务中整合 RabbitMQ 2、结合上图、创建出交换机、队列、绑定关系 Configuration public class MyRabbitConfig {/** 自定义消息转换器* */Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}RabbitListener(queues stock.release.stock.queue)public void consumer(){}/** 创建交换机、队列、绑定关系....* */Beanpublic Exchange stockEventExchange() {return new TopicExchange(stock-event-exchange,true,false,null);}Beanpublic Queue stockReleaseStockQueue() {return new Queue(stock.release.stock.queue,true,false,false,null);}Beanpublic Queue stockDelayQueue() {HashMapString, Object arguments new HashMap();// 设置与队列相连的死信交换机arguments.put(x-dead-letter-exchange,stock-event-exchange);// 转发死信的 路由键arguments.put(x-dead-letter-routing-key,stock.release);// 设置队列的 TTL。超过30s就表示未支付订单准备关闭arguments.put(x-message-ttl,12000);return new Queue(stock.delay.queue,true,false,false,arguments);}Beanpublic Binding stockFinish() {return new Binding(stock.delay.queue,Binding.DestinationType.QUEUE,stock-event-exchange,stock.locked,null);}Beanpublic Binding stockRelease() {return new Binding(stock.release.stock.queue,Binding.DestinationType.QUEUE,stock-event-exchange,stock.release.#,null);} }3、在锁定库存时需要保存库存的工作单表示该库存操作的状态是否成功是否需要回滚。 锁定库存成功后向 MQ 发送工作单详情信息。 /** 锁定库存* 库存解锁场景* 1、成功创建订单超过指定时间未支付或者用户手动取消订单* 2、成功创建订单锁定库存但是由于接下来的业务出现异常需要自动解锁库存。* */OverrideTransactional(rollbackFor Exception.class)public Boolean orderLockStock(WareSkuLockVo vo) {// 保存工作单信息WareOrderTaskEntity taskEntity new WareOrderTaskEntity();taskEntity.setOrderSn(vo.getOrderSn());wareOrderTaskService.save(taskEntity);// 1、找到商品在哪个仓库有库存ListOrderItemVo locks vo.getLocks();ListSkuWareHasStock hasStocks locks.stream().map(item - {SkuWareHasStock stock new SkuWareHasStock();stock.setSkuId(item.getSkuId());// 查询哪些仓库有库存。ListLong wareIds baseMapper.listWareIdHasStock(item.getSkuId());stock.setWareId(wareIds);stock.setNum(item.getCount());return stock;}).collect(Collectors.toList());// 2、锁定库存for (SkuWareHasStock hasStock : hasStocks) {// 标志位表示当前商品是否被锁住Boolean skuStocked false;Long skuId hasStock.getSkuId();ListLong wareIds hasStock.getWareId();if (wareIds null || wareIds.size() 0) {// 该商品没有库存,直接抛出异常throw new NoStockException(skuId);}for (Long wareId : wareIds) {// 锁定库存,成功返回 1失败返回0Long count baseMapper.lockSkuStock(skuId, wareId, hasStock.getNum());if (count 1) {// 当前商品锁定成功skuStocked true;// TODO 3、库存锁定成功向MQ发送消息// 保存工作单详情信息WareOrderTaskDetailEntity taskDetailEntity new WareOrderTaskDetailEntity();taskDetailEntity.setWareId(wareId);taskDetailEntity.setSkuId(skuId);taskDetailEntity.setSkuNum(hasStock.getNum());taskDetailEntity.setTaskId(taskEntity.getId());taskDetailEntity.setLockStatus(1);wareOrderTaskDetailService.save(taskDetailEntity);// 向MQ发送工作单详情信息StockLockedTo stockLockedTo new StockLockedTo();stockLockedTo.setTaskId(taskEntity.getId());// 只封装 TaskDetailId 是不行因为是一件商品锁定成功发送一次消息。// 如果一共有三件商品前俩件锁定成功第三件锁定失败。那么本地事务是会将这三件库存都会回滚。因此如果只保存id查不到任何信息。// stockLockedTo.setTaskDetailId(detailEntity.getId());StockDetailLockedTo stockDetailLockedTo new StockDetailLockedTo();BeanUtils.copyProperties(taskDetailEntity, stockDetailLockedTo);stockLockedTo.setTaskDetail(stockDetailLockedTo);rabbitTemplate.convertAndSend(stock-event-exchange, stock.locked, stockLockedTo);break;} else {// 当前商品锁定失败,}}// 当前商品所有仓库都没有锁定成功if (!skuStocked) {throw new NoStockException(skuId);}}return true;}4、MQ 会将 工作单详情信息保存到 延迟队列 中经过指定的 TTL 需要有消费者接收并进行自动解锁。并且使用手动确认机制解锁库存失败重新将消息放回队列。等待 解锁库存服务解锁。 Service RabbitListener(queues stock.release.stock.queue) public class StockReleaseListener {Autowiredprivate WareSkuService wareSkuService;RabbitHandlerpublic void handleStockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {try {System.out.println(收到库存解锁通知...);wareSkuService.unLock(to);// 手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 有任何异常都是解锁失败channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);e.printStackTrace();}} }/*** 消费者* description 自动释放库存* date 2023/2/4 22:51* param* return void* 1、根据工作单ID查询工作单详情信息* 1有还需要根据订单号。查询订单* - 如果没有这个订单需要解锁有可能订单创建成功后库存锁定成功接着创建订单又调用其他方法把自己搞回滚了。* - 如果有订单还需要判断订单的支付状态。如果支付成功也无需解锁。支付失败或者取消支付进行解锁。* 2没有这个工作单说明库存锁定失败已经自动回滚了无需解锁** 应该使用手动确认机制解锁失败重新将信息放回队列。*/Overridepublic void unLock(StockLockedTo to) {StockDetailLockedTo taskDetail to.getTaskDetail();// 查询是否有工作单详情信息WareOrderTaskDetailEntity orderTaskDetailEntity wareOrderTaskDetailService.getById(taskDetail.getId());if (orderTaskDetailEntity ! null) {//查出wms_ware_order_task工作单的信息WareOrderTaskEntity orderTaskInfo wareOrderTaskService.getById(to.getTaskId());// 获取订单号查询订单状态R r orderFeignService.getOrderByOrderSn(orderTaskInfo.getOrderSn());if (r.getCode() 0) {OrderVo order r.getData(data, new TypeReferenceOrderVo() {});if (order null || order.getStatus() OrderStatusEnum.CANCLED.getCode()) {// 当前工作单锁定状态为已锁定但是未解锁才可以解锁if (orderTaskDetailEntity.getLockStatus() 1) {// 没有订单或者取消订单需要自动解锁unLockStock(taskDetail.getSkuId(), taskDetail.getWareId(),taskDetail.getSkuNum(),taskDetail.getId());}}}else {// 解锁失败throw new RuntimeException(解锁失败...重新入队);}}// 如果没有这个工作单无需自动解锁}/*** description* date 2023/2/5 8:52* param skuId* param wareId* param num* param detailId* return void 库存解锁*/private void unLockStock(Long skuId, Long wareId, Integer num, Long detailId) {baseMapper.unLockStock(skuId, wareId, num);// 库存解锁后更新库存工作单状态WareOrderTaskDetailEntity orderTaskDetailEntity new WareOrderTaskDetailEntity();orderTaskDetailEntity.setId(detailId);// 变为已解锁orderTaskDetailEntity.setLockStatus(2);wareOrderTaskDetailService.updateById(orderTaskDetailEntity);}mapper 映射文件 update idunLockStockUPDATEwms_ware_sku SET stock_locked stock_locked - #{num}WHERE sku_id #{skuId} AND ware_id #{wareId}/update开启手动确认机制 # 手动确认 spring.rabbitmq.listener.simple.acknowledge-modemanual解决 远程调用问题 在解锁库存时远程调用查询订单信息会报错响应体无法转换为 R 对象。 R r orderFeignService.getOrderByOrderSn(orderTaskInfo.getOrderSn());Could not extract response: no suitable HttpMessageConverter found for response type [class com.atguigu.common.utils.R] and content type [text/html;charsetUTF-8] 这时因为远程调用给我们响应了一个 login.html 页面。而我们在 order 中设置了一个登录拦截器想要访问订单都必须登录才可以而我们远程调用无需登录。 老师的解决方案在 Order服务中的登录拦截器中做一个匹配映射如果是远程调用的请求直接放行。 // 解决远程调用 order/order/status 需要登录问题String requestURI request.getRequestURI();// 匹配映射boolean match new AntPathMatcher().match(order/order/status/**, requestURI);if (match) {return true;}我的解决方案不知道为什么我按照老师的解决方案没有管用依然报这个错误配置AntPathMatcher没有起到作用因此我直接在注册拦截器时将这个路径排除了。 四、定时关闭订单 1、创建的交换机、路由、绑定关系 Configuration public class MyMQConfig {/** 使用 Bean 的方式创建 Exchange、Queue、Binding...服务启动会自动向RabbitMQ创建。* 前提是RabbitMQ中没有这些 Exchange、Queue、Binding... 如果存在即使配置不一样也不会重新创建。* */// 延迟队列Beanpublic Queue orderDelayQueue() {// String name, boolean durable, boolean exclusive, boolean autoDelete, MapString, Object argumentsHashMapString, Object arguments new HashMap();// 设置与队列相连的死信交换机arguments.put(x-dead-letter-exchange,order-event-exchange);// 转发死信的 路由键arguments.put(x-dead-letter-routing-key,order.release.order);// 设置队列的 TTL。超过1min就表示未支付订单准备关闭arguments.put(x-message-ttl,60000);return new Queue(order.delay.queue,true,false,false,arguments);}// 普通队列Beanpublic Queue orderReleaseOrderQueue() {return new Queue(order.release.order.queue,true,false,false,null);}// 交换机Beanpublic TopicExchange orderEventExchange() {//String name, boolean durable, boolean autoDelete)return new TopicExchange(order-event-exchange,true,false);}// 设置绑定关系: order-event- exchange ——》order.delay.queueBeanpublic Binding orderCreateOrder() {//String destination, DestinationType destinationType, String exchange, String routingKey,MapString, Object arguments// 绑定目的地-绑定的队列绑定类型【交换机 OR 队列】交换机路由键其他参数信息return new Binding(order.delay.queue,Binding.DestinationType.QUEUE,order-event-exchange,order.create.order,null);}// 设置绑定关系: order-event- exchange ——》order.release.order.queueBeanpublic Binding orderReleaseOrder() {return new Binding(order.release.order.queue,Binding.DestinationType.QUEUE,order-event-exchange,order.release.order,null);} } 2、创建订单成功向MQ发送消息 // TODO 创建订单成功向RabbitMQ发送消息rabbitTemplate.convertAndSend(order-event-exchange,order.create.order,orderTo.getOrder());3、监听器接受消息 RabbitListener(queues order.release.order.queue) Component public class OrderCloseListener {Autowiredprivate OrderService orderService;RabbitHandlerpublic void consumer(OrderEntity order, Message message, Channel channel) throws IOException {System.out.println(订单超时未支付即将关闭订单: order.getOrderSn());try {orderService.closeOrder(order);// 手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);e.printStackTrace();}} }4、 /** 关闭订单* */Overridepublic void closeOrder(OrderEntity order) {Long orderId order.getId();// 为了保险起见在重新查一遍订单信息OrderEntity orderEntity this.getById(order);if (orderEntity.getStatus() OrderStatusEnum.CREATE_NEW.getCode()) {// 订单为代付款时允许取消订单OrderEntity entity new OrderEntity();entity.setId(orderEntity.getId());// 设置订单状态为取消entity.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(entity);}}当超过指定时间后修改订单的状态由于 关闭订单与 解锁库存 存在一定的时间差正常情况下在订单创建成功后会隔一段时间去检查订单状态解锁库存 但是这种方式还有可能会出现问题假如订单解锁有延迟解锁库存先执行完订单就不会被释放了。 因此可以再订单解锁时在向 MQ 发送一条消息通知 MQ 订单解锁完成让它解锁库存。 5、创建新的绑定关系 // 设置绑定关系: order-event- exchange ——》stock.release.stock.queueBeanpublic Binding orderReleaseOther() {return new Binding(stock.release.stock.queue,Binding.DestinationType.QUEUE,order-event-exchange,order.release.other.#,null);}6、关闭订单后向MQ在发送一次消息 /** 关闭订单* */Overridepublic void closeOrder(OrderEntity order) {Long orderId order.getId();// 为了保险起见在重新查一遍订单信息OrderEntity orderEntity this.getById(order);if (orderEntity.getStatus() OrderStatusEnum.CREATE_NEW.getCode()) {// 订单为代付款时允许取消订单OrderEntity entity new OrderEntity();entity.setId(orderEntity.getId());// 设置订单状态为取消entity.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(entity);// TODO 关闭订单后再次向MQ发送一条消息OrderTo orderTo new OrderTo();BeanUtils.copyProperties(orderEntity,orderTo);rabbitTemplate.convertAndSend(order-event-exchange,order.release.other,orderTo);}}7、增加监听器接收消息 /** 收到订单关闭通知解锁库存* */RabbitHandlerpublic void handleOrderCloseRelease(OrderTo order, Message message, Channel channel) throws IOException {try {System.out.println(订单关闭即将解锁库存....);wareSkuService.unLock(order);// 手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 有任何异常都是解锁失败channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);e.printStackTrace();}}8、库存解锁解锁时需要判断工作单的锁定状态防止重复解锁 /*** description* date 2023/2/5 15:59* param orderEntity* return void 订单关闭解锁库存*/Overridepublic void unLock(OrderTo orderEntity) {// 根据订单号查询工作单WareOrderTaskEntity taskEntity wareOrderTaskService.getOrderTaskByOrderSn(orderEntity.getOrderSn());// 只查询锁定的工作单详情防止重复解锁。ListWareOrderTaskDetailEntity taskDetailEntityList wareOrderTaskDetailService.list(new QueryWrapperWareOrderTaskDetailEntity().eq(task_id, taskEntity.getId()).eq(lock_status, 1));for (WareOrderTaskDetailEntity orderTaskDetailEntity : taskDetailEntityList) {// 解锁库存unLockStock(orderTaskDetailEntity.getSkuId(), orderTaskDetailEntity.getWareId(),orderTaskDetailEntity.getSkuNum(),orderTaskDetailEntity.getId());}}五、支付服务 1、一些概念说明 什么是公钥、私钥、加密、签名和验签 公钥私钥 公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后保存在生成者手里的就是私钥 生成者发布出去大家用的就是公钥 加密 我们使用一对公私钥中的一个密钥来对数据进行加密而使用另一个密钥来进行解 密的技术。公钥和私钥都可以用来加密也都可以用来解密。公钥 和私钥必须是一对的才能进行加密、解密 加密的目的是 为了确保数据传输过程中的不可读性就是不想让别人看到。 签名 给我们将要发送的数据做上一个唯一签名类似于指纹用来互相验证接收方和发送方的身份 验签 一个数据对应一个唯一的签名如果数据被修改签名也会更改验签就是验证原数据是否被修改过。 2、沙箱环境测试 Demo测试下载https://docs.open.alipay.com/270/106291/ 3、内网穿透 内网穿透功能可以允许我们使用外网的网址来访问主机 正常的外网需要访问我们项目的流程是 1、买服务器并且有公网固定 IP 2、买域名映射到服务器的 IP 3、域名需要进行备案和审核 使用内网穿透只需要安装内网穿透服务商的软件别人即可访问我们的电脑网站 1、下载 花生壳客户端: https://dl-cdn.oray.com/hsk/windows/HskDDNS_8.6.0.48614.exe 2、增加映射 3、搭建好可进行诊断 4、没有问题就可以进行使用了 4、整合阿里云支付服务 1、增加依赖 com.alipay.sdk : alipay-sdk-java - Maven Central Repository Search dependencygroupIdcom.alipay.sdk/groupIdartifactIdalipay-sdk-java/artifactIdversion4.35.37.ALL/version /dependency2、拷贝阿里云支付模板 ConfigurationProperties(prefix alipay) Component Data public class AlipayTemplate {//在支付宝创建的应用的idprivate String app_id 2016092200568607;// 商户私钥您的PKCS8格式RSA2私钥private String merchant_private_key MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCSgX/nTQ0lDS8ObaM5LGZ1hiz18GXnNpqPLhJCym4xOpn35FNPHrPkDGEoMKrZ5LJeA4cZulckD8AtpvBCpeyIkrj/i1WVmSg10hVX67MlVets4UecCHZv2hKAN0/iId76kozdqrd7Csp/YgXPquN9Np0NFotggTrmiBANkvcpTF9SCGrDq/isOoCvClfbvVJjApfLLOel3yECe5K/SZ8puiWILVm1NxEXAqJ8z0ipPZVGrXsT6Bo0pEyCPcEL0SqaC9WT0zdWQzdUknCzZV9W2wKjEXBJG9hqxay5kPaKm9leBatSkDAaDxH/N5g36HRfY7BmklwRZsp17lHinxAgMBAAECggEAfnnfck35WBKFc90a9D0FXlzrZGEV3uzKIIsb46UXFlrzC5HoVkvEWOCiJCjHiIpvbGr8xED43TZgk/IwLC/JxQLM0kVJGWo6fWoSVOIP2YSLNe620APBvaq3BdkFiMJfSYBBg2J7mkIR39SE8Nvu3j3QWmYzSNJbE2spINnwTzNBL1OPaB5h3hSjyI07KaUcOjhTBF0EZl83NlBDsxmQvy0NmuOIWAcIXXvGoIbwkA774J3LhwLVS4W2FpQj4FlxvDlPu24GeNWN7oO66T3Jp9bweO120ObhuKwZQosDGkJq0975zVSJX5QtUWHMM/QDPO8Pk24n2AoPcACQcQKBgQDS6kqDsK8dDBpkmxYopA1gJJATnur0RHFZJb5webOhnEZnePhB1hhhGvKFcrdY2hcYeQiUZkHMsnWItNUe9E9ccp4m6KKG0iV/BQda7zx1zMTTZUMvSbO282Q31YnQu7Yz6BSk4f/U5Qbu61AK53Tv1ejSAgQhXt1Pwq8KD7QKBgQCx0pkqW453tY2o4iPqFGjKYI2yk5bAH5etmOvW51OZ4Slsq/aUJKBVG6fOpRVKkiXulHhrp5csZH0/C7kaj4Hy7TjgUKSWvwlv7i7jgN0dq/bhVJz82yN9pENWvy5J0I8Kt67XH6JDEGWjlV58auifMRSx5mRJNn5pM6qrFlQKBgFyZWm/JV1fv1xVyoLjlXlTvBsbO7kMH/jpgqFwtAk1n/x3VEShJ1kayIbTOjotWSopMvCFJG9tqM0cyxWLatkELXWifAIsNpqRuYWah1FbZD2fukxLNtM0aYyCUUvZeg2cUnIOraWupxbp9e13eMpvdmWMiWXfhM18CRWEwdAoGAUwT0l076EhgUQJwm1JML0jY94eCfpmLbnNJgRe1qysEPrB1s2IslA7cOqC5we0kyRmmwsuoibQpZYwbRG7JmRAk2pZtgzDRSbpxv7a0rDoBLmbXMOU0Hraqw2Bf3v2SMc79/9FWnIvrC4EyBYZZPwGOpsNAZRSdEUQX9qrceUCgYB99OOtFFt1ixzyTCyUj3Fuiw7BsPhdI3nuMSoNTPIDNpzRBp/KFXyv/FNJ2CjTAsX3OR3D6KmEYihqUfrYeb0P5zoybcQLMxbXxKec6F2o6U2iqFIq0MKwHUqsb9X3pj4qE0ZHbFgRtIHnL2/QGV5PFJdmIZIBKZcvB8fW6ztDA;// 支付宝公钥,查看地址https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。private String alipay_public_key MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQQceVUChTJGtF/a8SXufhSxDTKporieTq9NO7yDZSpDlAX1zVPT/nf0KWAlxq1TYappWMIYtyrOABhJyn6flNP6vuSBiM5lYsepHvYrtRHqlFiJruEkiaCgEZBKL5aCfBHYj0oqgQn9MpNV/PEH4cBYAVaiI4VX8CBUQfeEGjgN6OkpLULZ3X0JUkmSnVvCNJ1m3PD68IIlbOfEZXJUKCqmZhzprGR5VWswjxAg87cMwvijL4gdkSy/daG62Bz5vApcmmMkuX1k1fMWP4ajZCASVw8HDMSLRhd8We9F97gd8CW0TavzbdRmTS5H4yEgO8F9HRAsbkhV9yu0yQIDAQAB;// 服务器[异步通知]页面路径 需http://格式的完整路径不能加?id123这类自定义参数必须外网可以正常访问// 支付宝会悄悄的给我们发送一个请求告诉我们支付成功的信息private String notify_url;// 页面跳转同步通知页面路径 需http://格式的完整路径不能加?id123这类自定义参数必须外网可以正常访问//同步通知支付成功一般跳转到成功页private String return_url;// 签名方式private String sign_type RSA2;// 字符编码格式private String charset utf-8;// 支付宝网关 https://openapi.alipaydev.com/gateway.doprivate String gatewayUrl https://openapi.alipaydev.com/gateway.do;public String pay(PayVo vo) throws AlipayApiException {//AlipayClient alipayClient new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, json, AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);//1、根据支付宝的配置生成一个支付客户端AlipayClient alipayClient new DefaultAlipayClient(gatewayUrl,app_id, merchant_private_key, json,charset, alipay_public_key, sign_type);//2、创建一个支付请求 //设置请求参数AlipayTradePagePayRequest alipayRequest new AlipayTradePagePayRequest();alipayRequest.setReturnUrl(return_url);alipayRequest.setNotifyUrl(notify_url);//商户订单号商户网站订单系统中唯一订单号必填String out_trade_no vo.getOut_trade_no();//付款金额必填String total_amount vo.getTotal_amount();//订单名称必填String subject vo.getSubject();//商品描述可空String body vo.getBody();alipayRequest.setBizContent({\out_trade_no\:\ out_trade_no \, \total_amount\:\ total_amount \, \subject\:\ subject \, \body\:\ body \, \product_code\:\FAST_INSTANT_TRADE_PAY\});String result alipayClient.pageExecute(alipayRequest).getBody();//会收到支付宝的响应响应的是一个页面只要浏览器显示这个页面就会自动来到支付宝的收银台页面System.out.println(支付宝的响应result);return result;} }私钥、公钥、网关、改成自己的 支付宝开放平台 (alipay.com) 返回 Vo Data public class PayVo {private String out_trade_no; // 商户订单号 必填private String subject; // 订单名称 必填private String total_amount; // 付款金额 必填private String body; // 商品描述 可空 } 3、创建controller 处理支付请求 Controller public class PayWebController {AutowiredAlipayTemplate alipayTemplate;AutowiredOrderService orderService;ResponseBodyGetMapping(/payOrder)public String payOrder(RequestParam(orderSn)String orderSn){PayVo payVo orderService.getOrderPay(orderSn);try{// 支付alipayTemplate.pay(payVo);} catch (AlipayApiException e) {e.printStackTrace();}return null;}}/*** description* date 2023/2/5 20:22* param orderSn* return com.atguigu.gulimall.order.vo.PayVo 支付订单*/Overridepublic PayVo getOrderPay(String orderSn) {OrderEntity orderEntity this.getOrderByOrderSn(orderSn);PayVo payVo new PayVo();payVo.setOut_trade_no(orderSn);// 订单号ListOrderItemEntity orderItemEntities orderItemService.list(new QueryWrapperOrderItemEntity().eq(order_sn, orderSn));payVo.setSubject(orderItemEntities.get(0).getSpuName());// 订单标题// 指定后面2位小数payVo.setTotal_amount(orderEntity.getPayAmount().setScale(2).toString());payVo.setBody(orderItemEntities.get(0).getSpuName()); // 备注return payVo;}响应的信息是一个支付页面。 支付宝的响应form namepunchout_form methodpost actionhttps://openapi.alipaydev.com/gateway.do?charsetutf-8methodalipay.trade.page.paysignf0ALyx8f46iNFZjfTjpFgSQRrEwbXjMy5LDVKbBVylxvr%2F8V%2FhOyTisGAtu7%2Bpo6RjVOi3NzbX%2FfAHRmjegly52mninirGTBLN5FntlPn4PGXa7Isi0sWGgAvfnb%2BoQ3IiefoN6Pt3BY7QdXywoE2BHfoz8bXkV%2F%2BfjZFjhi2W5uZeDaoIlS%2BBogF5B%2FbwcCM0AUrtjIoHmrngvzoPFj0exFQ2PP2FE4xrqJyfZxoEh7tcaUzDa37u4KgG7%2BU4luio9CZryuS29jLU%2B4rKOWE7LKKw2L5v1GvIBC6sGlpyNs%2Bbxj9LCGMaa5363EjecgudDHOdm2cVirvxb2u4wUnw%3D%3Dreturn_urlhttp%3A%2F%2Fmember.gulimall.com%2FmemberOrder.htmlnotify_urlhttp%3A%2F%2F63i857228t.goho.co%2Fpayed%2Fnotifyversion1.0app_id2021000122615169sign_typeRSA2timestamp2023-02-0520%3A31%3A56alipay_sdkalipay-sdk-java-4.35.37.ALLformatjson input typehidden namebiz_content value{quot;out_trade_noquot;:quot;202302052031434361622211365567483906quot;,quot;total_amountquot;:quot;104484.00quot;,quot;subjectquot;:quot;华为Mate30 Proquot;,quot;bodyquot;:quot;华为Mate30 Proquot;,quot;product_codequot;:quot;FAST_INSTANT_TRADE_PAYquot;} input typesubmit value立即支付 styledisplay:none /form scriptdocument.forms[0].submit();/script修改 controller Controller public class PayWebController {AutowiredAlipayTemplate alipayTemplate;AutowiredOrderService orderService;// produces MediaType.TEXT_HTML_VALUE 表示该方法返回的是一个 html 页面ResponseBodyGetMapping(value /payOrder, produces MediaType.TEXT_HTML_VALUE)public String payOrder(RequestParam(orderSn) String orderSn) throws AlipayApiException {PayVo payVo orderService.getOrderPay(orderSn);// 支付String pay alipayTemplate.pay(payVo);return pay;}}5、支付成功同步回调 1、修改支付成功回调 2、整合 member 服务 整合 springsession整合登录拦截器 —— 同样会有远程调用需要登录的问题直接放行即可。 // 解决远程调用 需要登录问题String requestURI request.getRequestURI();// 匹配映射AntPathMatcher pathMatcher new AntPathMatcher();boolean match pathMatcher.match(/member/**, requestURI);if (match) {return true;}3、创建 Controller 处理请求 Controller public class MemberWebController {GetMapping(/memberOrder.html)public String memberOrderPage() {return orderList;} }6、异步通知内网穿透环境搭建 支付宝在支付成功之后会向我们设置的异步通知地址 notify_url通过 POST 请求的形式将支付结果作为参数通知。异步通知说明 - 支付宝文档中心 (alipay.com) 1、修改异步通知回调 // 服务器[异步通知]页面路径 需http://格式的完整路径不能加?id123这类自定义参数必须外网可以正常访问// 支付宝会悄悄的给我们发送一个请求告诉我们支付成功的信息// http://63i857228t.goho.co/alipay.trade.page.pay-JAVA-UTF-8/notify_url.jspprivate String notify_url https://4287b772c5.imdo.co/payed/notify;2、修改 Nginx配置文件。由于使用 IP:80 访问 NginxHOST请求头不匹配找不到对应的服务网因此手动精确匹配如果是 /payed/ 下的 请求 直接转发到 order.gulimall.com location /payed/ {# 发送请求时携带host信息proxy_set_header Host order.gulimall.com;proxy_pass http://gulimall;}3、通过外网访问订单服务POST 请求https://4287b772c5.imdo.co/payed/notify 4、由于订单服务的所有请求都需要登录因此需要将 /payed/notify 排除掉 registry.addInterceptor(new LoginUserInterceptor()).addPathPatterns(/**).excludePathPatterns(/order/order/status/**).excludePathPatterns(/payed/notify); 7、支付完成 1、先验签、在支付 RestController public class OrderPayListener {Autowiredprivate OrderService orderService;Autowiredprivate AlipayTemplate alipayTemplate;PostMapping(/payed/notify)public String AliPayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {// 只要收到支付宝的异步通知返回 success 支付宝便不再通知// 获取支付宝POST过来反馈信息//TODO 需要验签MapString, String params new HashMap();MapString, String[] requestParams request.getParameterMap();for (String name : requestParams.keySet()) {String[] values requestParams.get(name);String valueStr ;for (int i 0; i values.length; i) {valueStr (i values.length - 1) ? valueStr values[i]: valueStr values[i] ,;}//乱码解决这段代码在出现乱码时使用// valueStr new String(valueStr.getBytes(ISO-8859-1), utf-8);params.put(name, valueStr);}boolean signVerified AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名if (signVerified) {System.out.println(签名验证成功...);//去修改订单状态String result orderService.handlePayResult(vo);return result;} else {System.out.println(签名验证失败...);return error;}} }/** 支付成功异步回调通知* */Overridepublic String handlePayResult(PayAsyncVo vo) {// 1、保存交易流水PaymentInfoEntity paymentInfo new PaymentInfoEntity();paymentInfo.setOrderSn(vo.getOut_trade_no());paymentInfo.setAlipayTradeNo(vo.getTrade_no());paymentInfo.setTotalAmount(new BigDecimal(vo.getBuyer_pay_amount()));paymentInfo.setSubject(vo.getBody());paymentInfo.setPaymentStatus(vo.getTrade_status());paymentInfo.setCreateTime(new Date());paymentInfo.setCallbackTime(vo.getNotify_time());// 添加到数据库中this.paymentInfoService.save(paymentInfo);// 修改订单状态// 获取当前状态String tradeStatus vo.getTrade_status();if (tradeStatus.equals(TRADE_SUCCESS) || tradeStatus.equals(TRADE_FINISHED)) {//支付成功状态String orderSn vo.getOut_trade_no(); //获取订单号this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode());}return success;}/*** 修改订单状态* param orderSn* param status 订单状态*/private void updateOrderStatus(String orderSn, Integer status) {this.baseMapper.updateOrderStatus(orderSn,status);}mapper 映射 update idupdateOrderStatusupdate oms_order set status #{status} where order_sn #{orderSn}/update8、收单 订单在支付页不支付一直刷新订单过期了才支付订单状态改为已支付了但是库 存解锁了 使用支付宝自动收单功能解决。只要一段时间不支付就不能支付了。 增加一个 timeout_express 参数表示支付过期时间 在配置文件中配置 # 支付订单超时时间 alipay.timeOut1m六、秒杀 1、定时任务与Cron表达式 语法秒 分 时 日 月 周 年Spring 不支持 http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html cron表达式在线生成: Cron - 在线Cron表达式生成器 (ciding.cc) 特殊字符 枚举 ​ (cron“7,9,23 * * * * ?”)任意时刻的 7,923 秒启动这个任务 -范围 ​ (cron“7-20 * * * * ?”):任意时刻的 7-20 秒之间每秒启动一次 *任意 ​ 指定位置的任意时刻都可以 /步长 ​ (cron“7/5 * * * * ?”)第 7 秒启动每 5 秒一次 ​ (cron“*/5 * * * * ?”)任意秒启动每 5 秒一次 出现在日和周几的位置为了防止日和周冲突在周和日上如果要写通配符使 用? ​ (cron“* * * 1 * ?”)每月的 1 号启动这个任务 L出现在日和周的位置” ​ last最后一个 ​ (cron“* * * ? * 3L”)每月的最后一个周二 WWork Day工作日 ​ (cron“* * * W * ?”)每个月的工作日触发 ​ (cron“* * * LW * ?”)每个月的最后一个工作日触发 #第几个 ​ (cron“* * * ? * 5#2”)每个月的第 2 个周 4 SpringBoot 使用 定时任务 EnableScheduling Component Slf4j EnableAsync public class HelloScheduled {/** 1、只允许6位秒分时日周月* 2、周的位置上 1-7、MON-SUN代表周一到周日* 3、定时任务不应该阻塞。* 1如果需要阻塞使用 CompletableFuture 异步的方式提交到线程池中* 2定时任务线程池. 配置 spring.task.scheduling.pool.size5 有的版本不管用* 4使用异步任务注解* EnableAsync 开启异步任务* Async 希望异步执行的方法上增加该注解* 配置项都在 TaskExecutionProperties 中可配置 corePoolMaxPool....* spring.task.execution....* */Scheduled(cron */1 * * * * *)Asyncpublic void printHello() {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}log.info(hello);} }2、秒杀商品上架 上架最近三天的所有秒杀商品、上架流程1、查询最近三天的所有秒杀场次 /* * 查询最近三天的秒杀场次 * */ GetMapping(/findLatest3DaysSessions) public R findLatest3DaysSessions() {ListSeckillSessionEntity list seckillSessionService.findLatest3DaysSessions();return R.ok().setData(list); }Overridepublic ListSeckillSessionEntity findLatest3DaysSessions() {// 查询最近三天的秒杀场次 2023-02-06 00:00:00 2023-02-08 11:59:59return this.list(new QueryWrapperSeckillSessionEntity().between(start_time,startTime(),endTime()));}// 秒杀场次开始时间private String startTime() {// 获取当前日期 2023-02-06LocalDate localDate LocalDate.now();// 获取时分秒: 00:00:00LocalTime now LocalTime.MIN;// 2023-02-06 00:00:00LocalDateTime dateTime LocalDateTime.of(localDate, now);// 指定时间格式格式化return dateTime.format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss));}// 秒杀场次结束时间private String endTime(){// 获取结束日期 2023-02-08LocalDate localDate LocalDate.now().plusDays(2);// 获取时分秒: 11:59:59LocalTime now LocalTime.MAX;// 2023-02-08 11:59:59LocalDateTime dateTime LocalDateTime.of(localDate, now);// 指定时间格式格式化return dateTime.format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss));}2、 1封装最新的秒杀商品信息保存到 redis 中 保存秒杀的场次信息保存秒杀商品的详细信息 2设置商品的随机码防止恶意攻击 3使用分布式锁的信号量 (秒杀的商品数量) 保存到 redis 中避免由于大量请求去访问数据库造成数据库压力过大。 Service public class SeckillServiceImpl implements SeckillService {AutowiredCouponFeignService couponFeignService;Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate ProductFeignService productFeignService;Autowiredprivate RedissonClient redissonClient;public final String SESSION_CACHE_PREFIX seckill:sessions:;public final String SKUKILL_CACHE_PREFIX seckill:skus;public final String SEMAPHORE_CACHE_PREFIX seckill:stocks:;Overridepublic void UploadSeckillSkuLatest3Days() {// 1、远程调用查询出最近三天的秒杀场次 2023-02-06 00:00:00 2023-02-08 11:59:59R r couponFeignService.findLatest3DaysSessions();if (r.getCode() 0) {ListSeckillSessionsWithSkus sessionsWithSkus r.getData(data, new TypeReferenceListSeckillSessionsWithSkus() {});if (sessionsWithSkus! null sessionsWithSkus.size() 0) {// 2、保存秒杀场次信息到 redis 中this.saveSessionsInfos(sessionsWithSkus);// 3、保存秒杀商品信息到 redis 中this.saveSessionsSkusInfos(sessionsWithSkus);}else {System.out.println(没有秒杀场次);}}}/** 保存秒杀场次信息到redis* */private void saveSessionsInfos( ListSeckillSessionsWithSkus sessionsWithSkus){sessionsWithSkus.stream().forEach(item -{// 保存的keylong start item.getStartTime().getTime();long end item.getEndTime().getTime();String key SESSION_CACHE_PREFIX start _ end;// 保存的valueListString skuIds item.getRelationSkus().stream().map(sku - sku.getSkuId().toString()).collect(Collectors.toList());redisTemplate.opsForList().leftPushAll(key,skuIds);});}/** 保存秒杀商品的详细信息* */private void saveSessionsSkusInfos( ListSeckillSessionsWithSkus sessionsWithSkus){sessionsWithSkus.stream().forEach(item -{// 准备hash操作BoundHashOperationsString, Object, Object ops redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);item.getRelationSkus().stream().forEach(relationSku -{// 将商品信息保存到redis中SeckillSkuRedisTo seckillSkuRedisTo new SeckillSkuRedisTo();// 1、保存秒杀商品的秒杀信息BeanUtils.copyProperties(relationSku,seckillSkuRedisTo);// 2、保存秒杀商品的详细信息R r productFeignService.info(relationSku.getSkuId());if (r.getCode() 0) {SkuInfoVo skuInfo r.getData(skuInfo, new TypeReferenceSkuInfoVo() {});seckillSkuRedisTo.setSkuInfoVo(skuInfo);}// 3、保存秒杀的开始、结束时间seckillSkuRedisTo.setStartTime(item.getStartTime().getTime());seckillSkuRedisTo.setEndTime(item.getEndTime().getTime());// 4、保存秒杀商品的随机码String token UUID.randomUUID().toString().replace(-, );seckillSkuRedisTo.setRandomCode(token);// 保存到 redis 中ops.put(relationSku.getSkuId().toString(), JSON.toJSONString(seckillSkuRedisTo));// 5、设置秒杀商品的信号量 —— 秒杀的数量. 当大量请求秒杀时不可能实时去查询数据库这样会给数据库造成很大的压力// 通过将redis中设置信号量来限制秒杀商品的数量RSemaphore semaphore redissonClient.getSemaphore(SEMAPHORE_CACHE_PREFIX token);// 将秒杀的数量作为信号量semaphore.trySetPermits(relationSku.getSeckillCount().intValue());});});} }公共返回类 / SeckillSkuRedisTo: 保存到redis中的秒杀商品信息 Data public class SeckillSkuRedisTo {private Long id;/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort;/** sku的详细信息* */private SkuInfoVo skuInfoVo;// 秒杀的开始、结束时间private Long startTime;private Long endTime;// 秒杀商品的随机码private String randomCode; }/ SeckillSessionsWithSkus保存秒杀的场次信息以及每场包含的skuId Data public class SeckillSessionsWithSkus {/*** id*/TableIdprivate Long id;/*** 场次名称*/private String name;/*** 每日开始时间*/private Date startTime;/*** 每日结束时间*/private Date endTime;/*** 启用状态*/private Integer status;/*** 创建时间*/private Date createTime;ListSeckillSkuVo relationSkus; }/ SeckillSkuVo保存商品的秒杀信息Data public class SeckillSkuVo {TableIdprivate Long id;/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort; }/ SkuInfoVo: 保存商品的sku基本信息 Data public class SkuInfoVo {private Long skuId;/*** spuId*/private Long spuId;/*** sku名称*/private String skuName;/*** sku介绍描述*/private String skuDesc;/*** 所属分类id*/private Long catelogId;/*** 品牌id*/private Long brandId;/*** 默认图片*/private String skuDefaultImg;/*** 标题*/private String skuTitle;/*** 副标题*/private String skuSubtitle;/*** 价格*/private BigDecimal price;/*** 销量*/private Long saleCount; }3、秒杀上架幂等性问题 如果定时任务在分布式的情况下可能会造成商品重复上架等问题。因此在上架商品时使用分布式锁解决保证同一时间只有一个服务执行定时任务 1、使用分布式锁 Service Slf4j public class SeckillSkuScheduled {Autowiredprivate SeckillService seckillService;Autowiredprivate RedissonClient redissonClient;private final String UPLOAD_LOCK skuKill:upload:lock;/*** description* date 2023/2/7 11:29* param* return void* 上架最近三天的秒杀商品*/Scheduled(cron */5 * * * * ?)public void UploadSeckillSkuLatest3Days() {// TODO: 保证幂等性重复上架商品。 解决分布式锁、并保证每个存储操作都是幂等性的RLock lock redissonClient.getLock(UPLOAD_LOCK);lock.lock(10, TimeUnit.SECONDS);try {log.info(准备上架商品...);seckillService.UploadSeckillSkuLatest3Days();log.info(上架成功...);} finally {lock.unlock();}}} 2、保证各个保存操作的幂等性 Service public class SeckillServiceImpl implements SeckillService {AutowiredCouponFeignService couponFeignService;Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate ProductFeignService productFeignService;Autowiredprivate RedissonClient redissonClient;public final String SESSION_CACHE_PREFIX seckill:sessions:;public final String SKUKILL_CACHE_PREFIX seckill:skus;public final String SKU_STOCK_SEMAPHORE seckill:stocks:;Overridepublic void UploadSeckillSkuLatest3Days() {// 1、远程调用查询出最近三天的秒杀场次 2023-02-06 00:00:00 2023-02-08 11:59:59R r couponFeignService.findLatest3DaysSessions();if (r.getCode() 0) {ListSeckillSessionsWithSkus sessionsWithSkus r.getData(data, new TypeReferenceListSeckillSessionsWithSkus() {});if (sessionsWithSkus ! null sessionsWithSkus.size() 0) {// 2、保存秒杀场次信息到 redis 中this.saveSessionsInfos(sessionsWithSkus);// 3、保存秒杀商品信息到 redis 中this.saveSessionsSkusInfos(sessionsWithSkus);} else {System.out.println(没有秒杀场次);}}}/** 保存秒杀场次信息到redis* */private void saveSessionsInfos(ListSeckillSessionsWithSkus sessionsWithSkus) {sessionsWithSkus.stream().forEach(item - {// 保存的keylong start item.getStartTime().getTime();long end item.getEndTime().getTime();String key SESSION_CACHE_PREFIX start _ end;// TODO 防止重复。保证幂等性if (!redisTemplate.hasKey(key)) {// 保存的value: sessionId_skuIdListString skuIds item.getRelationSkus().stream().map(sku - item.getId() _ sku.getSkuId().toString()).collect(Collectors.toList());redisTemplate.opsForList().leftPushAll(key, skuIds);}});}/** 保存秒杀商品的详细信息* */private void saveSessionsSkusInfos(ListSeckillSessionsWithSkus sessionsWithSkus) {sessionsWithSkus.stream().forEach(item - {// 准备hash操作BoundHashOperationsString, Object, Object ops redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);item.getRelationSkus().stream().forEach(relationSku - {// 商品随机码String token UUID.randomUUID().toString().replace(-, );// TODO: 防止重复上架保证幂等性if (!ops.hasKey(relationSku.getPromotionSessionId()_relationSku.getSkuId().toString())){// 将商品信息保存到redis中SeckillSkuRedisTo seckillSkuRedisTo new SeckillSkuRedisTo();// 1、保存秒杀商品的秒杀信息BeanUtils.copyProperties(relationSku, seckillSkuRedisTo);// 2、保存秒杀商品的详细信息R r productFeignService.info(relationSku.getSkuId());if (r.getCode() 0) {SkuInfoVo skuInfo r.getData(skuInfo, new TypeReferenceSkuInfoVo() {});seckillSkuRedisTo.setSkuInfoVo(skuInfo);}// 3、保存秒杀的开始、结束时间seckillSkuRedisTo.setStartTime(item.getStartTime().getTime());seckillSkuRedisTo.setEndTime(item.getEndTime().getTime());// 4、保存秒杀商品的随机码seckillSkuRedisTo.setRandomCode(token);// 保存到 redis 中。keysessionId_skuIdops.put(relationSku.getPromotionSessionId()_relationSku.getSkuId().toString(), JSON.toJSONString(seckillSkuRedisTo));// 5、设置秒杀商品的信号量 —— 秒杀的数量. 当大量请求秒杀时不可能实时去查询数据库这样会给数据库造成很大的压力// 通过将redis中设置信号量来限制秒杀商品的数量RSemaphore semaphore redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE token);// 将秒杀的数量作为信号量semaphore.trySetPermits(relationSku.getSeckillCount().intValue());}});});} }4、查询所有的秒杀商品 1、 RestController public class SeckillController {Autowiredprivate SeckillService seckillService;/** 获取当前时间的秒杀商品* */GetMapping(/getCurrentSeckillSkus)public R getCurrentSeckillSkus(){ListSeckillSkuRedisTo list seckillService.getCurrentSeckillSkus();return R.ok().setData(list);} }/** 获取当前时间的秒杀商品* */Overridepublic ListSeckillSkuRedisTo getCurrentSeckillSkus() {// 1、判断当前时间属于哪个秒杀场次long time System.currentTimeMillis();// 获取所有的场次keySetString keys redisTemplate.keys(SESSION_CACHE_PREFIX *);for (String key : keys) {// seckill:sessions:1675844252000_1675958400000 需要分割String[] s key.replace(SESSION_CACHE_PREFIX, ).split(_);Long start Long.parseLong(s[0]);Long end Long.parseLong(s[1]);if (start time end time) {// 获取商品信息的value [2_11,1_10]ListString range redisTemplate.opsForList().range(key, -100, 100);// k1:Mapk2,k3BoundHashOperationsString, String, String hashOps redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);// 2、获取该场次下的所有商品信息。// 由于场次信息的value与商品信息的key是一致的.可以通过场次信息的value作为商品信息的key取出valueListString skus hashOps.multiGet(range);if (skus ! null skus.size() 0) {ListSeckillSkuRedisTo result skus.stream().map(item - {SeckillSkuRedisTo seckillSkuRedisTo JSON.parseObject(item, SeckillSkuRedisTo.class);// seckillSkuRedisTo.setRandomCode(); 秒杀已经开始需要随机码秒杀。可以带着。return seckillSkuRedisTo;}).collect(Collectors.toList());return result;}break;}}return null;}时区BUG 从数据库封装到java中的实体类时时间可能会有差距数据库默认采用UTC建议改成东八区 SELECT NOW() # 查询当前时间是否与系统时间相吻合 show variables like %time_zone%; # 查询当前mysql的时区 临时配置 set global time_zone 8:00; # 临时配置重启mysql之后还会改成原来的 永久配置 找到 /mydata/mysql/conf/my.cnf默认是在 /etc/mysql/my.cnf ,追加以下 default-time_zone 8:005、查询某一个秒杀 商品 1、 /** 查询某一个秒杀商品* */Overridepublic SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {BoundHashOperationsString, String, String hashOps redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);// 1、获取所有的keySetString keys hashOps.keys();if (keys ! null keys.size() 0) {// 使用正则表达式判断某个key是否包含skuIdString regx \\d_ skuId;for (String key : keys) {// 2、将所有的key与skuId逐个匹配if (Pattern.matches(regx,key)) {String data hashOps.get(key);SeckillSkuRedisTo seckillSkuRedisTo JSON.parseObject(data, SeckillSkuRedisTo.class);// 如果该商品正在秒杀就将随机码发送给前端页面。如果没有就无需发送Long start seckillSkuRedisTo.getStartTime();Long end seckillSkuRedisTo.getEndTime();long current System.currentTimeMillis();if (!(current start current end)) {seckillSkuRedisTo.setRandomCode();}return seckillSkuRedisTo;}}}return null;}2、controller /** 查询某一个秒杀商品* */GetMapping(/sku/seckill/{skuId})public R getSkuSeckillInfo(PathVariable Long skuId){SeckillSkuRedisTo seckillSkuRedisTo seckillService.getSkuSeckillInfo(skuId);return R.ok().setData(seckillSkuRedisTo);} } 3、product 远程调用查询商品详情时判断该商品是否是秒杀商品 FeignClient(gulimall-seckill) public interface SeckillFeignService {/** 查询某一个商品的秒杀信息* */GetMapping(/sku/seckill/{skuId})public R getSkuSeckillInfo(PathVariable Long skuId);}4、SkuInfoServiceImpl CompletableFutureVoid seckillFuture CompletableFuture.runAsync(() - {// 6、查询该商品是否是秒杀商品R r seckillFeignService.getSkuSeckillInfo(skuId);if (r.getCode() 0) {SeckillInfoVo data r.getData(data, new TypeReferenceSeckillInfoVo() {});skuItemVo.setSeckillInfo(data);}}, threadPoolExecutor);6、秒杀系统设计 登录拦截器 Component public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocalMemberRespVo threadLocal new ThreadLocal();Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {MemberRespVo memberRespVo (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if (memberRespVo null) {request.getSession().setAttribute(msg,请先登录);// 没有进行登录跳转到登录界面登录response.sendRedirect(http://auth.gulimall.com/login.html);return false;}else{threadLocal.set(memberRespVo);return true;}} }注册拦截器只拦截秒杀请求即可 Configuration public class SeckillWebConfig implements WebMvcConfigurer {/** 增加拦截器* */Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginUserInterceptor()).addPathPatterns(/kill);} }7、秒杀流程 秒杀到创建订单没有操作数据库只是向MQ发送一条消息数据库的压力是非常轻松的。 controller /** 秒杀* http://seckill.gulimall.com/kill?killId2-11keyf7bde6d931e34b7f8677b3395157d426num1* */GetMapping(/kill)public R kill(RequestParam(killId) String killId,RequestParam(key) String key,RequestParam(num) Integer num) {String orderSn seckillService.kill(killId,key,num);return R.ok().setData(orderSn);}SeckillServiceImpl /** 秒杀* */Overridepublic String kill(String killId, String key, Integer num) {// 登录用户信息MemberRespVo respVo LoginUserInterceptor.threadLocal.get();BoundHashOperationsString, String, String hashOps redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);String json hashOps.get(killId);if (!StringUtils.isEmpty(json)) {SeckillSkuRedisTo skuRedisTo JSON.parseObject(json, SeckillSkuRedisTo.class);Long startTime skuRedisTo.getStartTime();Long endTime skuRedisTo.getEndTime();long currentTime System.currentTimeMillis();// 1、对秒杀时间进行校验if (currentTime startTime currentTime endTime) {// 随机码String randomCode skuRedisTo.getRandomCode();// 商品信息的keyString redisSkuKey skuRedisTo.getPromotionSessionId().toString() _ skuRedisTo.getSkuId().toString();// 2、对随机码、对应关系进行校验if (randomCode.equals(key) killId.equals(redisSkuKey)){// 3、验证购买数量是否超出限制if (skuRedisTo.getSeckillLimit() num){// 4、判断该用户是否秒杀过 ,如果没有秒杀过就去 redis 中占个位。String redisKey respVo.getId().toString() _ skuRedisTo.getSkuId().toString();// 设置过期时间: 秒杀结束就过期long expir endTime - currentTime;Boolean aBoolean redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), expir, TimeUnit.MILLISECONDS);if (aBoolean){// 如果为true说明存储成功用户没有买过// 5、使用信号量扣减库存RSemaphore semaphore redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE key);try {// tryAcquire 会尝试获取不成功返回 falseboolean b semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);if (b){// 6、抢购成功创建订单号并返回return IdWorker.getTimeId();}} catch (InterruptedException e) {e.printStackTrace();}}}}}}return null;}订单创建完毕向 MQ 发送消息 1、创建队列、绑定关系 /*** 商品秒杀队列* return*/Beanpublic Queue orderSecKillOrrderQueue() {Queue queue new Queue(order.seckill.order.queue, true, false, false);return queue;}Beanpublic Binding orderSecKillOrrderQueueBinding() {//String destination, DestinationType destinationType, String exchange, String routingKey,// MapString, Object argumentsBinding binding new Binding(order.seckill.order.queue,Binding.DestinationType.QUEUE,order-event-exchange,order.seckill.order,null);return binding;}2、秒杀成功向MQ发送消息 /** 秒杀* */Overridepublic String kill(String killId, String key, Integer num) {long s1 System.currentTimeMillis();// 登录用户信息MemberRespVo respVo LoginUserInterceptor.threadLocal.get();BoundHashOperationsString, String, String hashOps redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);String json hashOps.get(killId);if (!StringUtils.isEmpty(json)) {SeckillSkuRedisTo skuRedisTo JSON.parseObject(json, SeckillSkuRedisTo.class);Long startTime skuRedisTo.getStartTime();Long endTime skuRedisTo.getEndTime();long currentTime System.currentTimeMillis();// 1、对秒杀时间进行校验if (currentTime startTime currentTime endTime) {// 随机码String randomCode skuRedisTo.getRandomCode();// 商品信息的keyString redisSkuKey skuRedisTo.getPromotionSessionId().toString() _ skuRedisTo.getSkuId().toString();// 2、对随机码、对应关系进行校验if (randomCode.equals(key) killId.equals(redisSkuKey)){// 3、验证购买数量是否超出限制if (skuRedisTo.getSeckillLimit() num){// 4、判断该用户是否秒杀过 ,如果没有秒杀过就去 redis 中占个位。String redisKey respVo.getId().toString() _ skuRedisTo.getSkuId().toString();// 设置过期时间: 秒杀结束就过期long expir endTime - currentTime;Boolean aBoolean redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), expir, TimeUnit.MILLISECONDS);if (aBoolean){// 如果为true说明存储成功用户没有买过// 5、使用信号量扣减库存RSemaphore semaphore redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE key);try {// tryAcquire 会尝试获取不成功返回 falseboolean b semaphore.tryAcquire(num);if (b){// 6、抢购成功创建订单号并返回String timeId IdWorker.getTimeId();SeckillOrderTo orderTo new SeckillOrderTo();orderTo.setOrderSn(timeId);orderTo.setMemberId(respVo.getId());orderTo.setNum(num);orderTo.setPromotionSessionId(skuRedisTo.getPromotionSessionId());orderTo.setSkuId(skuRedisTo.getSkuId());orderTo.setSeckillPrice(skuRedisTo.getSeckillPrice());// TODO 向MQ发送消息rabbitTemplate.convertAndSend(order-event-exchange,order.seckill.order,orderTo);long s2 System.currentTimeMillis();log.info(耗时... (s2 - s1));return timeId;}} catch (Exception e) {e.printStackTrace();}}}}}}return null;}3、SeckillOrderTo Data public class SeckillOrderTo {/*** 订单号*/private String orderSn;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 购买数量*/private Integer num;/*** 会员ID*/private Long memberId;}4、消费者监听队列创建订单 Slf4j Component RabbitListener(queues order.seckill.order.queue) public class OrderSeckillListener {Autowiredprivate OrderService orderService;RabbitHandlerpublic void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {log.info(准备创建秒杀单的详细信息...);try {orderService.createSeckillOrder(orderTo);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}5、 /** 创建秒杀订单* */Overridepublic void createSeckillOrder(SeckillOrderTo orderTo) {//TODO 保存订单信息OrderEntity orderEntity new OrderEntity();orderEntity.setOrderSn(orderTo.getOrderSn());orderEntity.setMemberId(orderTo.getMemberId());orderEntity.setCreateTime(new Date());BigDecimal totalPrice orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));orderEntity.setPayAmount(totalPrice);orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());//保存订单this.save(orderEntity);//保存订单项信息OrderItemEntity orderItem new OrderItemEntity();orderItem.setOrderSn(orderTo.getOrderSn());orderItem.setRealAmount(totalPrice);orderItem.setSkuQuantity(orderTo.getNum());//保存订单项数据orderItemService.save(orderItem);}七、Sentinel 1、熔断降级限流 服务熔断 服务熔断会导致服务降级,但是和服务降级的区别就是服务熔断会恢复调用链路。 在互联网系统中当下游服务因访问压力过大而响应变慢或失败上游服务为了保护系统整体的可用性可以暂时切断对下游服务的调用。 这种牺牲局部保全整体的措施就叫做熔断。 一旦下游服务C因某些原因变得不可用积压了大量请求服务B的请求线程也随之阻塞。线程资源逐渐耗尽使得服务B也变得不可用。紧接着服务 A也变为不可用整个调用链路被拖垮。 因此需要服务熔断来确保整个系统的可用性 服务降级 整个网站处于流量高峰期服务器压力剧增根据当前业务情况及流量对一些服务和 页面进行有策略的降级[停止服务所有的调用直接返回降级数据]。以此缓解服务器资源的 的压力以保证核心业务的正常运行同时也保持了客户和大部分客户的得到正确的相应。 相同点 1、为了保证集群大部分服务的可用性和可靠性防止崩溃牺牲小我 2、用户最终都是体验到某个功能不可用 不同点 1、熔断是被调用方故障触发的系统主动规则 2、降级是基于全局考虑停止一些正常服务释放资源 限流 对打入服务的请求流量进行控制使服务能够承担不超过自己能力的流量压力 2、SpringBoot整合Sentinel sentinel官方文档: 主流框架的适配 · alibaba/Sentinel Wiki · GitHub 使用sentinel主要分为几个步骤: 定义资源定义规则检验规则是否生效 1、引入依赖 !--SpringCloud ailibaba sentinel --dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-sentinel/artifactId/dependency2、下载控制台Releases · alibaba/Sentinel (github.com) 3、配置服务与控制台的连接地址以及每个服务与控制台交互数据的端口 #sentinel # 控制台的地址 spring.cloud.sentinel.transport.dashboardlocalhost:8333 # 服务与控制台交互数据的端口 spring.cloud.sentinel.transport.port87193、在控制台中配置规则 增加流控规则 3、实时监控 在使用 Endpoint 特性之前需要在 Maven 中添加 spring-boot-starter-actuator 依赖并在配置中允许 Endpoints 的访问。 Spring Boot 1.x 中添加配置 management.security.enabledfalse。暴露的 endpoint 路径为 /sentinelSpring Boot 2.x 中添加配置 management.endpoints.web.exposure.include*。暴露的 endpoint 路径为 /actuator/sentinel 1、每一个微服务都导入 spring-boot-starter-actuator而不是在 common 模块 !--sentinel控制台实时监控信息审计--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependency2、暴露端点 # actuator management.endpoints.web.exposure.include*4、自定义sentinel的返回信息 默认的返回信息 自定义响应信息 Configuration public class SeckillSentinelConfig {/** 自定义sentinel流控失败信息, 俩种配置方式* */// public SeckillSentinelConfig() {// WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {// Override// public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {// // 响应编码// httpServletResponse.setCharacterEncoding(UTF-8);// // 数据格式// httpServletResponse.setContentType(application/json);// R error R.error(BizCodeEnum.TOO_MANY_REQUEST.getCode(), BizCodeEnum.TOO_MANY_REQUEST.getMessage());// // 响应到页面// httpServletResponse.getWriter().write(JSON.toJSONString(error));//// }// });//// }Beanpublic UrlBlockHandler urlBlockHandler(){UrlBlockHandler urlBlockHandler new UrlBlockHandler() {Overridepublic void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {// 响应编码httpServletResponse.setCharacterEncoding(UTF-8);// 数据格式httpServletResponse.setContentType(application/json);R error R.error(BizCodeEnum.TOO_MANY_REQUEST.getCode(), BizCodeEnum.TOO_MANY_REQUEST.getMessage());// 响应到页面httpServletResponse.getWriter().write(JSON.toJSONString(error));}};WebCallbackManager.setUrlBlockHandler(urlBlockHandler);return urlBlockHandler;} } 5、RabbitTemplate 与 MyRabbitConfig 循环依赖问题 原代码 Configuration public class MyRabbitConfig {/** 这里直接注入会有循环依赖的问题* rabbitConfig 需要rabbitTemplate,而rabbitTemplate又要使用到rabbitConfig 形成了循环依赖* */Autowiredprivate RabbitTemplate rabbitTemplate;Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}/** 设置发布确认机制* 1、ConfirmCallback只要 Broker 接收到消息就会执行此回调* spring.rabbitmq.publisher-confirmstrue* 2、ReturnCallback 只有交换机将消息转发到Queue失败时才会调用此回调* # 开启发送端确认机制。 Exchange -- Queue* spring.rabbitmq.publisher-returnstrue* # 只要消息成功发送到Queue就优先异步调用 ReturnCallback* spring.rabbitmq.template.mandatorytrue* */PostConstruct // MyRabbitConfig初始化之后执行public void InitRabbitTemplate() {rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** description* date 2023/1/31 18:55* param correlationData 保存消息的id以及相关信息可在发送消息时指定 new CorrelationData()* param ack 消息是否发送失败。trueBroke接收到消息 falseBroker没有接收到消息* param cause 消息发送失败的原因* return void*/Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {if (ack) {System.out.println(Broker接收消息成功, correlationData: correlationData ack: ack cause: cause);} else {System.out.println(Broker接收消息失败, correlationData: correlationData ack: ack cause: cause);}}});rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {/*** description* date 2023/1/31 22:25* param message 投递失败的消息* param replyCode 回复的状态码* param replyText 回复的文本* param exchange 投递失败的交换机* param routingKey 投递失败消息的 routing-key* return void*/Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {System.out.println(message: message replyCode: replyCode replyText: replyText exchange: exchange routingKey: routingKey);}});} } 报错 原因 构建 RabbitTemplate 时需要 MessageConverter而MessageConverter由依赖于 MyRabbitConfigMyRabbitConfig 中又注入了 RabbitTemplate 造成了循环依赖。 解决办法将 MessageConverter 移到外部类 Configuration public class MyRabbitConfig {Autowiredprivate RabbitTemplate rabbitTemplate;/** 这里直接注入MessageConverter会有循环依赖的问题* 构建 RabbitTemplate 时需要 MessageConverter* 而MessageConverter由依赖于 MyRabbitConfigMyRabbitConfig 中又注入了 RabbitTemplate 造成了循环依赖。* */// Bean// public MessageConverter messageConverter() {// return new Jackson2JsonMessageConverter();// }/** 设置发布确认机制* 1、ConfirmCallback只要 Broker 接收到消息就会执行此回调* spring.rabbitmq.publisher-confirmstrue* 2、ReturnCallback 只有交换机将消息转发到Queue失败时才会调用此回调* # 开启发送端确认机制。 Exchange -- Queue* spring.rabbitmq.publisher-returnstrue* # 只要消息成功发送到Queue就优先异步调用 ReturnCallback* spring.rabbitmq.template.mandatorytrue* */PostConstruct // MyRabbitConfig初始化之后执行public void InitRabbitTemplate() {rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** description* date 2023/1/31 18:55* param correlationData 保存消息的id以及相关信息可在发送消息时指定 new CorrelationData()* param ack 消息是否发送失败。trueBroke接收到消息 falseBroker没有接收到消息* param cause 消息发送失败的原因* return void*/Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {if (ack) {System.out.println(Broker接收消息成功, correlationData: correlationData ack: ack cause: cause);} else {System.out.println(Broker接收消息失败, correlationData: correlationData ack: ack cause: cause);}}});rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {/*** description* date 2023/1/31 22:25* param message 投递失败的消息* param replyCode 回复的状态码* param replyText 回复的文本* param exchange 投递失败的交换机* param routingKey 投递失败消息的 routing-key* return void*/Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {System.out.println(message: message replyCode: replyCode replyText: replyText exchange: exchange routingKey: routingKey);}});} }Configuration class messageConverterConfig{/** 自定义消息转换器* */Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();} }6、熔断降级 文档熔断降级 · alibaba/Sentinel Wiki · GitHub 现代微服务架构都是分布式的由非常多的服务组成。不同服务之间相互调用组成复杂的调用链路。复杂链路上的某一环不稳定就可能会层层级联最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级暂时切断不稳定调用避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段通常在**客户端调用端**进行配置。 熔断策略 开启 Feign 的熔断降级 1、引入 spring-cloud-starter-openfeign、 spring-cloud-starter-alibaba-sentinel 依赖 2、配置文件打开 Sentinel 对 Feign 的支持 feign.sentinel.enabledtrue3、简单实例 —— 远程服务被降级 就会触发此回调 FeignClient(value gulimall-seckill,fallback SeckillFeignFallback.class) public interface SeckillFeignService {/** 查询某一个商品的秒杀信息* */GetMapping(/sku/seckill/{skuId})public R getSkuSeckillInfo(PathVariable Long skuId);}Component Slf4j public class SeckillFeignFallback implements SeckillFeignService {Overridepublic R getSkuSeckillInfo(Long skuId) {log.info(进入到熔断降级....);return R.error(BizCodeEnum.TOO_MANY_REQUEST.getCode(), BizCodeEnum.VALID_EXCEPTION.getMessage());} }7、开启自定义的受保护资源 1、代码方式可以对任意资源进行保护可以进行流控、熔断… // 1.5.0 版本开始可以利用 try-with-resources 特性使用有限制 // 资源名可使用任意有业务语义的字符串比如方法名、接口名或其它可唯一标识的字符串。 try (Entry entry SphU.entry(resourceName)) {// 被保护的业务逻辑// do something here... } catch (BlockException ex) {// 资源访问阻止被限流或被降级// 在此处进行相应的处理操作 }2、注解方式 8、网关流控 只需要增加一个依赖即可 !--sentinel与Gateway整合--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-alibaba-sentinel-gateway/artifactIdversion2.1.0.RELEASE/version/dependency9、自定义网关流控回调 /*** 网关限流回调* author YZG* date 2023/2/9*/ Configuration public class SentinelGatewayConfig {public SentinelGatewayConfig() {GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {//网关限流了请求就会调用此回调Overridepublic MonoServerResponse handleRequest(ServerWebExchange exchange, Throwable t) {R error R.error(BizCodeEnum.TOO_MANY_REQUEST.getCode(), BizCodeEnum.TOO_MANY_REQUEST.getMessage());String errorJson JSON.toJSONString(error);MonoServerResponse body ServerResponse.ok().body(Mono.just(errorJson), String.class);return body;}});}}总结 高并发三宝
http://www.w-s-a.com/news/819537/

相关文章:

  • 网站维护具体做啥如何开发wap网站
  • 公司网站设计费计入什么科目潍坊公司网站制作
  • 拖拽式网站开发模具钢东莞网站建设
  • 彩票娱乐网站建设模块化网站开发
  • 孝感网站设计用自己的名字设计头像
  • 高明网站建设哪家好深圳vi设计公司全力设计
  • 工程技术cpu游戏优化加速软件
  • 一起做网店网站入驻收费wordpress 自定义评论样式
  • 深圳高端网站建设公司排名app软件开发sh365
  • 泰州网站整站优化惠州做网站多少钱
  • 做博客网站的php代码一建论坛建工教育网
  • 邢台网站制作费用单页营销网站后台
  • 红色网站建设的比较好的高校用vs2010做购物网站
  • 网站域名备案号查询网页设计实验报告总结模板
  • 什么软件 做短视频网站好大型论坛网站建设
  • 视频网站用什么cms网络运营与维护主要做什么
  • 设计网站主页要多少钱赣州制作网站百度
  • 什么叫高端网站定制网站收录大幅度下降
  • 汝城县网站建设公司aspx网站实例
  • 专业微网站营销diywap手机微网站内容管理系统
  • 盗版做的最好的网站温州logo设计公司
  • 网站建设 中山南充微网站建设
  • 企业网站更新什么内容免费设计软件下载
  • 夏天做哪些网站能致富做网站怎么每天更新内容
  • 个人网站的设计与开发网站建设流程中哪些部分比较重要
  • 招聘网站如何建设中国计算机网络公司排名
  • 工信部网站备案规定厦门在线制作网站
  • 商丘网站公司智联招聘手机app下载
  • 江西专业南昌网站建设中国专业的网站建设
  • 物流企业网站建设方案招标网站有哪些