0


谷粒商城之高级篇(3)

2 商城业务

2.7 订单服务

2.7.22 创建业务交换机&队列

这里承接 知识补充篇 6 RabbitMQ

订单分布式主体逻辑

  • 订单超时未支付触发订单过期状态修改与库存解锁

创建订单时消息会被发送至队列order.delay.queue,经过TTL的时间后消息会变成死信以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理

  • 如果该订单已支付,则无需处理
  • 否则说明该订单已过期,修改该订单的状态并通过路由键order.release.other发送消息至队列stock.release.stock.queue进行库存解锁
  • 库存锁定后延迟检查是否需要解锁库存

在库存锁定后通过路由键stock.locked发送至延迟队列stock.delay.queue,延迟时间到,死信通过路由键stock.release转发至stock.release.stock.queue,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁

  • 由于关闭订单和库存解锁都有可能被执行多次,因此要保证业务逻辑的幂等性,在执行业务是重新查询当前的状态进行判断
  • 订单关闭和库存解锁都会进行库存解锁的操作,来确保业务异常或者订单过期时库存会被可靠解锁

解锁库存的实现:

①库存服务导入RabbitMQ的依赖

<!--  RabbitMQ的依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency>

② RabbitMQ的配置

spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/

③****配置RabbitMQ的序列化机制

@ConfigurationpublicclassMyRabbitConfig{/**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */@BeanpublicMessageConvertermessageConverter(){returnnewJackson2JsonMessageConverter();}}

④ 开启RabbitMQ

1671248966918

⑤ 按照下图创建交换机、队列、绑定关系

1671204724682

1671205296599

统一使用 topic交换机:因为交换机需要绑定多个队列,不同的路由键,且具有模糊匹配功能。

@ConfigurationpublicclassMyRabbitConfig{/**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */@BeanpublicMessageConvertermessageConverter(){returnnewJackson2JsonMessageConverter();}/**
     出现问题: 并没创建交换机、队列、绑定关系

     出现问题的原因:只有当第一次连接上RabbitMQ时,发现没有这些东西才会创建

     解决方案:监听队列
     * @param message
     */@RabbitListener(queues ="stock.release.stock.queue")publicvoidhandle(Message message){}@BeanpublicExchangestockEventExchange(){//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsreturnnewTopicExchange("stock-event-exchange",true,false);}@BeanpublicQueuestockReleaseStockQueue(){//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsreturnnewQueue("stock.release.stock.queue",true,false,false,null);}/**
     * 延时队列
     * @return
     */@BeanpublicQueuestockDelayQueue(){Map<String,Object> arguments =newHashMap<>();

        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",120000);returnnewQueue("stock.delay.queue",true,false,false,arguments);}@BeanpublicBindingstockReleaseBinding(){//String destination, DestinationType destinationType, String exchange, String routingKey,//             Map<String, Object> argumentsreturnnewBinding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.release.#",null);}@BeanpublicBindingstockLockedBinding(){//String destination, DestinationType destinationType, String exchange, String routingKey,//             Map<String, Object> argumentsreturnnewBinding("stock.delay.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.locked",null);}}

出现问题:****并没创建交换机、队列、绑定关系

出现问题的原因:只有当第一次连接上RabbitMQ时,发现没有这些东西才会创建

解决方案:监听队列

1671249112991

交换机、队列、绑定关系创建成功后,将上述代码注释

2.7.23 监听库存解锁

库存解锁的两种场景:

①下单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁

②下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁

更改数据库表wms_ware_order_task_detail

添加两个字段,方便库存回滚

1671265700522

实体类中需要修改的:

WareOrderTaskDetailEntity 加上 全参和无参构造器方便消息传播该实体类数据。

1671265739895

WareOrderTaskDetailDao.xml

1671265832174

② 保存工作单详情方便回溯

1671265892152

1671266289442

③ Common服务中创建To,方便MQ发送消息

1671266016720

@DatapublicclassStockLockedTo{privateLong id;//库存工作单的idprivateLong detailId;//库存工作单详情id}

如果To仅仅保存这个两个数据的话,会存在一些问题, 当1号订单在1号仓库扣减1件商品成功,2号订单在2号仓库扣减2件商品成功,3号订单在3号仓库扣减3件商品失败时,库存工作单的数据将会回滚,此时,数据库中将查不到1号和2号订单的库存工作单的数据,但是库存扣减是成功的,导致无法解锁库存

解决方案: 保存库存工作详情To

@DatapublicclassStockDetailTo{privateLong id;/**
     * sku_id
     */privateLong skuId;/**
     * sku_name
     */privateString skuName;/**
     * 购买个数
     */privateInteger skuNum;/**
     * 工作单id
     */privateLong taskId;/**
     * 仓库id
     */privateLong wareId;/**
     * 库存锁定状态
     */privateInteger lockStatus;}

1671266179218

④ 向MQ发送库存锁定成功的消息

1671266316108

库存回滚解锁
1)库存锁定
在库存锁定是添加以下逻辑

  • 由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件…)
  • 在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息

逻辑:

  • 遍历订单项,遍历每个订单项的每个库存,直到锁到库存
  • 发消息后库存回滚也没关系,用id是查不到数据库的
  • 锁库存的sql

这里编写了发送消息队列的逻辑,下面写接收消息队列后还原库存的逻辑。

2.7.24 库存解锁逻辑&库存自动解锁完成&测试库存自动解锁

解锁场景:

1.下单成功,库存锁定成功,接下来的业务如果调用失败导致订单回滚。之前锁定的库存就要自动解锁。

2.锁库存失败无需解锁

解决方案:通过查询订单的锁库存信息,如果有则仅仅说明库存锁定成功,还需判断是否有订单信息,如果有订单信息则判断订单状态,若订单状态已取消则解锁库存,反之:不能解锁库存,如果没有订单信息则需要解锁库存,如果没有锁库存信息则无需任何操作。

1.编写Vo,通过拷贝订单实体----> OrderEntity,用于接收订单信息

1671282013683

2. 远程服务编写,获取订单状态

订单服务下编写:OrderController

@GetMapping("/status/{orderSn}")publicRgetOrderStatus(@PathVariable("orderSn")String orderSn){OrderEntity orderEntity =  orderService.getOrderByOrderSn(orderSn);returnR.ok().setData(orderEntity);}

实现:

@OverridepublicOrderEntitygetOrderByOrderSn(String orderSn){OrderEntity order_sn =this.getOne(newQueryWrapper<OrderEntity>().eq("order_sn", orderSn));return order_sn;}

3.监听事件

接收消息

  • 延迟队列会将过期的消息路由至"stock.release.stock.queue",通过监听该队列实现库存的解锁
  • 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队

库存服务 编写 StockReleaseListener

1671282189427

@Service@RabbitListener(queues ="stock.release.stock.queue")publicclassStockReleaseListener{@AutowiredWareSkuService wareSkuService;@RabbitHandlerpublicvoidhandleStockLockedRelease(StockLockedToto,Message message,Channel channel)throwsIOException{System.out.println("收到解锁库存的消息...");try{
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch(Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}

WareSkuServiceImpl

库存解锁

  • 如果工作单详情不为空,说明该库存锁定成功- 查询最新的订单状态,- 如果订单不存在,说明订单提交出现异常回滚,- 如果订单存在(但订单处于已取消的状态),我们要对已锁定的库存进行解锁
  • 如果工作单详情为空,说明库存未锁定,自然无需解锁
  • 为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁
@AutowiredWareSkuDao wareSkuDao;@AutowiredWareOrderTaskService orderTaskService;@AutowiredWareOrderTaskDetailService orderTaskDetailService;@AutowiredRabbitTemplate rabbitTemplate;@AutowiredOrderFeignService orderFeignService;/**
     * 1、库存自动解锁。
     * 下订单成功,库存锁定成功,接下来的业务如果调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
     * 2、订单失败。
     * 锁库存失败。
     * <p>
     * 只要解锁库存的消息失败。一定要告诉服务器解锁失败。
     *
     * @param to
     */@OverridepublicvoidunlockStock(StockLockedToto){StockDetailTo detail =to.getDetail();Long detailId = detail.getId();//解锁//1、查询数据库关于这个订单的锁定库存信息。//有:证明库存锁定成功了。//    解锁:查询订单情况。//          1、没有这个订单。必须解锁//          2、有这个订单。不是解锁库存。//                订单状态:已取消:解锁库存//                         没取消:不能解锁//没有:库存锁定失败了,库存回滚了。这种情况无需解锁WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);if(byId !=null){//解锁Long id =to.getId();WareOrderTaskEntity taskEntity = orderTaskService.getById(id);String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态R r = orderFeignService.getOrderStatus(orderSn);if(r.getCode()==0){//订单数据返回成功OrderVo data = r.getData(newTypeReference<OrderVo>(){});if(data ==null|| data.getStatus()==4){//订单不存在//订单已经被取消了。才能解锁库存// detailIdif(byId.getLockStatus()==1){//当前库存工作单详情,状态 是1 :已锁定但是未解锁才可以解锁unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);}}}else{//消息拒绝以后重新放到队里里面,让别人继续消费解锁。thrownewRuntimeException("远程服务失败");}}else{//无需解锁}}/**
     * 解锁方法
     */privatevoidunLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId){//库存解锁
        wareSkuDao.unlockStock(skuId, wareId, num);//更新库存工作单的状态WareOrderTaskDetailEntity entity =newWareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2);//变为已解锁
        orderTaskDetailService.updateById(entity);}

WareSkuDao.xml

SQL编写:

UPDATE `wms_ware_sku` SET stock_locked = stock_locked -1
WHERE sku_id = 1 AND ware_id = 2
<updateid="unlockStock">
        UPDATE `wms_ware_sku` SET stock_locked = stock_locked -#{num}
        WHERE sku_id = #{skuId} AND ware_id = #{wareId}
    </update>

库存服务下编写:调用远程服务 OrderFeignService

@FeignClient("gulimall-order")publicinterfaceOrderFeignService{@GetMapping("/order/order/status/{orderSn}")RgetOrderStatus(@PathVariable("orderSn")String orderSn);}

4. 远程服务调用可能会出现失败,需要设置手动ACK,确保其它服务能消费此消息

#手动ACK设置
spring.rabbitmq.listener.simple.acknowledge-mode=manual

出现问题: 远程调用订单服务时被拦截器拦截

解决方案:请求路径适配放行

订单服务下的 拦截器。

1671282561293

2.7.25 定时关单完成

1671283316624

1671287642621

将就配合着看。

1.定时关单代码编写

①订单创建成功,给MQ发送关单消息

订单服务下的 OrderServiceImpl 的 submitOrder提交订单方法

1671288070746

② 监听事件,进行关单

订单服务下

@Service@RabbitListener(queues ="order.release.order.queue")publicclassOrderCloseListener{@AutowiredOrderService orderService;/**
     * 定时关单
     * @param entity
     * @param channel
     * @param message
     * @throws IOException
     */@RabbitHandlerpublicvoidlistener(OrderEntity entity,Channel channel,Message message)throwsIOException{System.out.println("收到过期的订单消息:准备关闭订单"+ entity.getOrderSn());try{
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch(Exception e){//失败了重新放到队列中,让其他消费者能够消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}

OrderServiceImpl

/**
     * 关单操作
     * @param entity
     */@OverridepublicvoidcloseOrder(OrderEntity entity){//查询当前这个订单的最新状态OrderEntity orderEntity =this.getById(entity.getId());if(orderEntity.getStatus()==OrderStatusEnum.CREATE_NEW.getCode()){//关单OrderEntity update =newOrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(update);}}

1671288293945

订单释放和库存解锁逻辑: 当订单创建成功,1分钟之后,向MQ发送关单消息,2分钟后,向MQ发送解锁库存消息,关单操作完成之后,过了1分钟解锁库存操作。

存在问题:由于机器卡顿、消息延迟等导致关单消息延迟发送,解锁库存消息正常发送和监听,导致解锁库存消息被消费,当执行完关单操作后便无法再执行解锁库存操作,导致卡顿的订单永远无法解锁库存。

解决方案:采取主动补偿的策略。当关单操作正常完成之后,主动去发送解锁库存消息给MQ,监听解锁库存消息进行解锁。
1671288628987

③ 按上图创建绑定关系

订单服务MyMQConfig

1671288668918

④ common服务中,创建OrderTo(拷贝order实体)

1671288720098

⑤ 向MQ发送解锁库存消息

1671288876594

/**
     * 关单操作
     * @param entity
     */@OverridepublicvoidcloseOrder(OrderEntity entity){//查询当前这个订单的最新状态OrderEntity orderEntity =this.getById(entity.getId());if(orderEntity.getStatus()==OrderStatusEnum.CREATE_NEW.getCode()){//未付款状态,进行关单OrderEntity update =newOrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(update);OrderTo orderTo =newOrderTo();BeanUtils.copyProperties(orderEntity,orderTo);//发给MQ一个消息:解锁库存
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);}}

⑥ 解锁库存操作

库存服务下

1671288764555

@RabbitHandlerpublicvoidhandleOrderCloseRelease(OrderTo orderTo,Message message,Channel channel)throwsIOException{System.out.println("订单关闭准备解锁库存...");try{
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch(Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}

1671288950151

//防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。//导致卡顿的订单,永远不能解锁库存。@Transactional@OverridepublicvoidunlockStock(OrderTo orderTo){String orderSn = orderTo.getOrderSn();//查一下最新库存的状态,防止重复解锁库存WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);Long id = task.getOrderId();//按照工作单找到所有 没有解锁的库存,进行解锁List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(newQueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status",1));// Long skuId, Long wareId, Integer num, Long taskDetailIdfor(WareOrderTaskDetailEntity entity : entities){unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());}}

WareOrderTaskServiceImpl

@OverridepublicWareOrderTaskEntitygetOrderTaskByOrderSn(String orderSn){WareOrderTaskEntity one =this.getOne(newQueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));return one;}

2.7.26 消息丢失、积压、重复等解决方案

  • 如何保证消息可靠性-消息丢失

1、消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器- 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式- 做好日志记录,每个消息状态是否都被服务器收到都应该记录- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。- publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机- 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

情况一: 消息发送出去但是由于网络原因未到达服务器,解决方案:采用try-catch将发送失败的消息持久化到数据库中,采用定期扫描重发的方式。

drop table if exists mq_message;
CREATE TABLE `mq_message` (
    `message_id` CHAR(32) NOT NULL,
    `content` TEXT,#json
    `to_exchange` VARCHAR(255) DEFAULT NULL,
    `routing_key` VARCHAR(255) DEFAULT NULL,
    `class_type` VARCHAR(255) DEFAULT NULL,
    `message_status` INT(1) DEFAULT '0' COMMENT '0-新建  1-已发送  2-错误抵达  3-已抵达',
    `create_time` DATETIME DEFAULT NULL,
    `update_time` DATETIME DEFAULT NULL,
    PRIMARY KEY (`message_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4

情况二:消息抵达服务器的队列中才算完成消息的持久化,解决方案----->publish + consumer的两端的ack机制

1671291076371

情况三: 防止自动ack带来的缺陷,采用手动ack,解决方案上面都有这里不再细说

  • 如何保证消息可靠性-消息重复

2、消息重复

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送 - 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志- 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理- rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的

消息被成功消费,ack时宕机,消息由unack变成ready,Broker又重新发送。解决方案:将消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标志。

  • 如何保证消息可靠性-消息积压

3、消息积压

  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大 - 上线更多的消费者,进行正常消费- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

消息积压即消费者的消费能力不够, 上线更多的消费者进行正常的消费。

2.8 支付业务

支付宝开放平台传送门:支付宝开放平台

网站支付DEMO传送门:手机网站支付 DEMO | 网页&移动应用

网站支付DEMO是用Eclipse编写的,代码结构如下图所示:

1671354960765

1671354989025

2.8.1 RSA、加密加签、密钥等相关概念

1671355152168

对称加密:发送方和接收方用的是同一把密钥,存在问题:当某一方将密钥泄漏之后,发送的消息可以被截取获悉并且随意进行通信。

1671355199521

非对称加密:发送方和接收方使用的不是同一把密钥,发送方使用密钥A对明文进行加密,接收方使用密钥B对密文进行解密,然后接收方将回复的明文用密钥C进行加密,发送方使用密钥D进行解密。采用非对称加密的好处是:即使有密钥被泄漏也不能自由的通信

密钥的公私性是相对于生成者而言的。发送方通过密钥A对明文进行加密,密钥A是只有发送方自己知道的,接收方想要解密密文,就需要拿到发送方公布出来的密钥B。

公钥:生成者发布的密钥可供大家使用的

私钥:生成者自己持有的密钥

1671355422855

签名:为了防止中途传输的数据被篡改和使用的方便,发送方采用私钥生成明文对应的签名,此过程被成为加签。接收方使用公钥去核验明文和签名是否对应,此过程被成为验签。

配置支付宝的沙箱环境:

沙箱环境配置查看传送门:登录 - 支付宝

1671355503281

接口加签方式共有两种:

①采用系统默认生成的支付宝的公钥、应用的私钥和公钥:

1671355561472

② 采用自定义密钥

1671355636513

传送门:密钥工具下载

1671355723785

将支付宝密钥工具生成的应用公钥复制进加签内容配置中,会自动生成支付宝的公钥

沙箱账号:用于测试环境中的商品支付

沙箱账号

1671355890188

使用eclipse测试:

注意如果项目报红:因为老师给的 沙箱测试Demo 默认使用的 是 tomact7.0 ,所以这里需要将 tomact7.0移除掉,使用我们自己本机安装的 tomcat。

1671356066793

选择 tomcat 7.0移除 ,然后导入自己的 就行。这里我已经移除好了。

1671356110313

参考:当eclipse导入新项目后出现红叉解决办法


老师课件:

一、支付宝支付
1、进入“蚂蚁金服开放平台”
https://open.alipay.com/platform/home.htm

2、下载支付宝官方demo,进行配置和测试

文档地址
https://open.alipay.com/platform/home.htm 支付宝&蚂蚁金服开发者平台

1671356335327

https://docs.open.alipay.com/catalog 开发者文档
https://docs.open.alipay.com/270/106291/ 全部文档=>电脑网站支付文档;下载demo

1671356349583

3、配置使用沙箱进行测试
1、使用RSA 工具生成签名
2、下载沙箱版钱包
3、运行官方demo 进行测试

4、什么是公钥、私钥、加密、签名和验签?
1、公钥私钥
公钥和私钥是一个相对概念
它们的公私性是相对于生成者来说的。
一对密钥生成后,保存在生成者手里的就是私钥,
生成者发布出去大家用的就是公钥

2、加密和数字签名

  • 加密是指:- 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解 密的技术。- 公钥和私钥都可以用来加密,也都可以用来解密。- 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。- 加密的目的是: - 为了确保数据传输过程中的不可读性,就是不想让别人看到。
  • 签名:- 给我们将要发送的数据,做上一个唯一签名(类似于指纹)- 用来互相验证接收方和发送方的身份;- 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以 用来达到数据的明文传输。
  • 验签- 支付宝为了验证请求的数据是否商户本人发的,- 商户为了验证响应的数据是否支付宝发的

2.8.2 内网穿透

1671369972076

如果别人直接访问我们自己电脑的本地项目,是不能访问的。

1671370039988

内网穿透的原理: 内网穿透服务商是正常外网可以访问的ip地址,我们的电脑通过下载服务商软件客户端并与服务器建立起长连接,然后服务商就会给我们的电脑分配一个域名,别人的电脑访问hello.hello.com会先找到hello.com即一级域名,然后由服务商将请求转发给我们电脑的二级域名;从而实现了别人可以通过IP地址访问我们本地的项目。

下面是老师课件:

二、内网穿透

1、简介
内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:

  • 1、买服务器并且有公网固定IP
  • 2、买域名映射到服务器的IP
  • 3、域名需要进行备案和审核

2、使用场景

  • 1、开发测试(微信、支付宝)
  • 2、智慧互联
  • 3、远程控制
  • 4、私有云

3、内网穿透的几个常用软件
1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
3、花生壳:https://www.oray.com/

老师课件中使用的 是 续断进行测试的,这里我们使用免费的内网穿透工具进行测试。

内网穿免费工具下载地址:cpolar - 安全的内网穿透工具

使用教程: Win系统如何下载安装使用cpolar内网穿透工具?_Cpolar Lisa的博客-CSDN博客

超好用的内网穿透工具【永久免费不限制流量】

上面两个 教程都可以参考,个人推荐 第二个教程。

已经成功建立连接。

1671370917715

【需要注意的是,对于免费版本的cpolar随机URL地址是会在24小时之后变化的,如果需要进一步使用,可以将站点配置成二级子域名,或自定义域名(使用自己的域名)长期使用。】

配置修改:

修改url前缀:

1671370868841

测试:访问成功。

1671370825139

2.8.3 整合支付

注意,需要保证所有项目的编码格式都是 utf-8

1671432147193

1.导入支付宝支付SDK的依赖

阿里支付依赖传送门

订单服务

<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java --><!--导入支付宝的SDK--><dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.35.7.ALL</version></dependency>

2. 编写AlipayTemplate工具类和PayVo

直接复制老师给的课件。

1671376504843

1671376519219

更改一些,这里我使用的是 绑定配置文件的方式来声明一些变量的:因为老师使用了一个@ConfigurationProperties(prefix = “alipay”)。

AlipayTemplate

@ConfigurationProperties(prefix ="alipay")@Component@DatapublicclassAlipayTemplate{//在支付宝创建的应用的id@Value("${alipay.app_id}")//这里使用的是绑定配置文件的方式privateString app_id;// 商户私钥,您的PKCS8格式RSA2私钥@Value("${alipay.merchant_private_key}")privateString merchant_private_key;// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。@Value("${alipay.alipay_public_key}")privateString alipay_public_key;// 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息@Value("${alipay.notify_url}")privateString notify_url;// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问//同步通知,支付成功,一般跳转到成功页@Value("${alipay.return_url}")privateString return_url;// 签名方式privateString sign_type ="RSA2";// 字符编码格式privateString charset ="utf-8";// 支付宝网关; https://openapi.alipaydev.com/gateway.do@Value("${alipay.gatewayUrl}")privateString gatewayUrl;publicStringpay(PayVo vo)throwsAlipayApiException{//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 =newDefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key,"json",
                charset, alipay_public_key, sign_type);//2、创建一个支付请求 //设置请求参数AlipayTradePagePayRequest alipayRequest =newAlipayTradePagePayRequest();
        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;}}

application.properties

1671376692140PayVo

@DatapublicclassPayVo{privateString out_trade_no;// 商户订单号 必填privateString subject;// 订单名称 必填privateString total_amount;// 付款金额 必填privateString body;// 商品描述 可空}

3.访问支付接口

1671376749323

4. 编写支付接口

produces属性:用于设置返回的数据类型

AlipayTemplate的pay()方法返回的就是一个用于浏览器响应的付款页面

PayWebController

@ControllerpublicclassPayWebController{@AutowiredAlipayTemplate alipayTemplate;@AutowiredOrderService orderService;/**
     * 1、将支付页让浏览器展示。
     * 2、支付成功以后,我们要跳到用户的订单列表页。
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */@ResponseBody@GetMapping(value ="/payOrder",produces ="text/html")publicStringpayOrder(@RequestParam("orderSn")String orderSn)throwsAlipayApiException{// PayVo payVo = new PayVo();// payVo.setBody();//订单的备注// payVo.setOut_trade_no();//订单号// payVo.setSubject();//订单的主题// payVo.setTotal_amount();//订单的金额PayVo payVo =  orderService.getOrderPay(orderSn);//返回的是一个页面。将此页面直接交给浏览器就行String pay = alipayTemplate.pay(payVo);System.out.println(pay);return"hello";}}

实现:

OrderService

/**
     * 获取当前订单的支付信息
     * @param orderSn
     * @return
     */PayVogetOrderPay(String orderSn);

OrderServiceImpl

应付金额需要处理,支付宝只能支付保留两位小数的金额,采用ROUND_UP的进位模式

@OverridepublicPayVogetOrderPay(String orderSn){PayVo payVo =newPayVo();OrderEntity order =this.getOrderByOrderSn(orderSn);//数据库 应付金额显示的4位小数:1215.0000//支付宝要求是两位小数,此外我们设置有余数就进位:譬如:12.0001   变为 12.01BigDecimal bigDecimal = order.getPayAmount().setScale(2,BigDecimal.ROUND_UP);//设置金额
        payVo.setTotal_amount(bigDecimal.toString());//设置订单号
        payVo.setOut_trade_no(order.getOrderSn());List<OrderItemEntity> order_sn = orderItemService.list(newQueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));OrderItemEntity entity = order_sn.get(0);//得到订单中的第一个商品

        payVo.setSubject(entity.getSkuName());//这里我们将订单的第一个商品的名字设置为提示
        payVo.setBody(entity.getSkuAttrsVals());//销售属性设为 订单的备注return payVo;}

2.8.4 支付成功同步回调

1.会员服务导入thymeleaf的依赖并配置

<!--  Thymeleaf的依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>

开发环境下,关闭thymeleaf的缓存

spring.thymeleaf.cache=false

2.将订单页文件夹中的index.html复制到会员服务的templates下并更名为orderList.html,将静态资源复制到Nginx中并替换访问路径【动静分离】

1671432306700

1671432352124

Nginx 中 配置静态资源位置。

1671377923206

修改 orderList.html中的静态资源前缀:

1671377858345

1671377896721

3. 配置网关及域名映射

配置 域名映射:192.168.56.10 member.gulimall.com

1671378656983

配置网关:

1671432948555

5. 引入Spring-Session

①导入依赖

<!--引入 redis--><!--导入Spring Session with redis 依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><!--导入SpringBoot整合Redis的依赖--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>

② 配置

spring.session.store-type=redis

spring.redis.host=192.168.56.10

复制订单服务中有关session的配置

1671433129746

/**一个新系统引入需要以下配置
 * 1、spring-session依赖等导入
 * 2、spring-session配置
 * 3、引入 LoginUserInterceptor  WebMvcConfig等
 */@ConfigurationpublicclassGulimallSessionConfig{@BeanpublicCookieSerializercookieSerializer(){DefaultCookieSerializer cookieSerializer =newDefaultCookieSerializer();

        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@BeanpublicRedisSerializer<Object>springSessionDefaultRedisSerializer(){returnnewGenericJackson2JsonRedisSerializer();}}

③ 启用Spring-Session

主启动类 加入 @EnableRedisHttpSession注解

1671433170473

④ 配置拦截器

远程服务调用获取运费信息,都给放过

1671433282491

@ComponentpublicclassLoginUserInterceptorimplementsHandlerInterceptor{publicstaticThreadLocal<MemberRespVo> loginUser =newThreadLocal<>();@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{// 库存解锁需要远程调用---->需要登录:所以设置拦截器不拦截 order/order/status/{orderSn}。// 对于查询订单等请求直接放行。// /order/order/status/45648913346789494//远程服务调用获取运费信息,都给放过///member/memberreceiveaddress/info/{id}String uri = request.getRequestURI();boolean match =newAntPathMatcher().match("/member/**", uri);if(match){returntrue;}//获取登录用户MemberRespVo attribute =(MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if(attribute !=null){//登录成功后,将用户信息存储至ThreadLocal中方便其他服务获取用户信息://加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查
            loginUser.set(attribute);returntrue;}else{//没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");returnfalse;}}}

1671433341778

注册拦截器

1671433395938

@ConfigurationpublicclassMemberMvcConfigurerimplementsWebMvcConfigurer{@AutowiredLoginUserInterceptor loginUserInterceptor;@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){

        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}}

6. 前端页面跳转修改

首页将我的订单处修改

1671378871489

7. controller编写

@ControllerpublicclassMemberWebController{@GetMapping("/memberOrder.html")publicStringmemberOrderPage(){//查出当前登录的用户的所有订单列表数据return"orderList";}}

8.配置支付成功后的跳转页面

订单服务的 application.properties中修改 成功回调的地址。

1671433561370

2.8.5 订单列表页渲染完成

1.远程服务调用获取订单项详情

①订单服务中编写获取订单项详情接口

OrderController

/**
     * 分页查询当前登录用户的所有订单
     */@PostMapping("/listWithItem")//@RequiresPermissions("order:order:list")publicRlistWithItem(@RequestBodyMap<String,Object> params){PageUtils page = orderService.queryPageWithItem(params);returnR.ok().put("page", page);}

② 订单实体中编写订单项属性

OrderEntity

//表明不是数据库中的字段@TableField(exist =false)privateList<OrderItemEntity> itemEntities;

③ 分页查询订单项详情接口实现

OrderServiceImpl

@OverridepublicPageUtilsqueryPageWithItem(Map<String,Object> params){//获取用户信息MemberRespVo memberRespVo =LoginUserInterceptor.loginUser.get();//根据用户id获取最新的订单IPage<OrderEntity> page =this.page(newQuery<OrderEntity>().getPage(params),newQueryWrapper<OrderEntity>().eq("member_id",memberRespVo.getId()).orderByDesc("id"));List<OrderEntity> order_sn = page.getRecords().stream().map(order ->{//根据订单号获取订单项数据List<OrderItemEntity> itemEntities = orderItemService.list(newQueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));

            order.setItemEntities(itemEntities);return order;}).collect(Collectors.toList());

        page.setRecords(order_sn);returnnewPageUtils(page);}

④ 会员服务远程调用订单服务查询订单项详情接口编写

@FeignClient("gulimall-order")publicinterfaceOrderFeignService{@PostMapping("/order/order/listWithItem")//@RequiresPermissions("order:order:list")RlistWithItem(@RequestBodyMap<String,Object> params);}

MemberWebController

@ControllerpublicclassMemberWebController{@AutowiredOrderFeignService orderFeignService;@GetMapping("/memberOrder.html")publicStringmemberOrderPage(@RequestParam(value ="pageNum",defaultValue ="1")Integer pageNum,Model model){//查出当前登录的用户的所有订单列表数据HashMap<String,Object> page =newHashMap<>();
        page.put("page",pageNum.toString());R r = orderFeignService.listWithItem(page);System.out.println(JSON.toJSONString(r));
        model.addAttribute("orders",r);return"orderList";}}

会出现两个问题:

①远程服务调用未携带cookie信息被拦截器拦截需要登录

解决方案:远程调用时拦截器将老请求的请求头信息再次封装

@ConfigurationpublicclassGuliFeignConfig{@Bean("requestInterceptor")publicRequestInterceptorrequestInterceptor(){returnnewRequestInterceptor(){@Overridepublicvoidapply(RequestTemplate template){//1、RequestContextHolder拿到刚进来的这个请求ServletRequestAttributes attributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();// System.out.println("RequestInterceptor线程..." + Thread.currentThread().getId());if(attributes !=null){HttpServletRequest request = attributes.getRequest();//老请求if(request !=null){//同步请求头数据,CookieString cookie = request.getHeader("Cookie");//给新请求同步了老请求的cookie
                        template.header("Cookie", cookie);}}}};}}

②getPage()将String类型的page又强转为String

com.atguigu.common.utils.Query类:

1671459910022

解决方案:

1671459896688

2.前端页面展示

只保留一个table用于遍历

1671459976813

遍历订单

1671460004164

获取订单号

1671460025373

遍历订单项

1671460073164

固定照片大小,取出图片

1671460099325

获取商品描述

1671460125397

获取订单项数量、收货人姓名、应付总额

1671460179264

获取订单状态

1671460207670

改进:****这些信息只出现一次,所占行数依据订单项数而定

1671460232517

打印结果如下:

1671460297583

只遍历一次,有几个商品占几行

1671460274686

缺失

1671460323294

1671460355359

<tableclass="table"th:each="order : ${orders.page.list}"><tr><tdcolspan="7"style="background:#F7F7F7"><spanstyle="color:#AAAAAA">2017-12-09 20:50:10</span><span><rubystyle="color:#AAAAAA">订单号:</ruby> [[${order.orderSn}]]</span><span>谷粒商城<iclass="table_i"></i></span><iclass="table_i5 isShow"></i></td></tr><trclass="tr"th:each=" item,itemStat : ${order.itemEntities}"><tdcolspan="3"style="border-right: 1px solid #ccc;"><imgstyle="height: 60px;width: 60px"th:src="${item.skuPic}"alt=""class="img"><div><pstyle="width: 242px;height: auto;overflow: auto">
                  [[${item.skuName}]]
                </p><div><iclass="table_i4"></i>找搭配</div></div><divstyle="margin-left:15px;">x[[${item.skuQuantity}]]</div><divstyle="clear:both"></div></td><tdth:if="${itemStat.index==0}"th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><iclass="table_i1"></i></i></td><tdth:if="${itemStat.index==0}"th:rowspan="${itemStat.size}"style="padding-left:10px;color:#AAAAB1;"><pstyle="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p><hrstyle="width:90%;"><p>在线支付</p></td><tdth:if="${itemStat.index==0}"th:rowspan="${itemStat.size}"><ul><listyle="color:#71B247;"th:if="${order.status==0}">待付款</li><listyle="color:#71B247;"th:if="${order.status==1}">已付款</li><listyle="color:#71B247;"th:if="${order.status==2}">已发货</li><listyle="color:#71B247;"th:if="${order.status==3}">已完成</li><listyle="color:#71B247;"th:if="${order.status==4}">已取消</li><listyle="color:#71B247;"th:if="${order.status==5}">售后中</li><listyle="color:#71B247;"th:if="${order.status==6}">售后完成</li><listyle="margin:4px 0;"class="hide"><iclass="table_i2"></i>跟踪<iclass="table_i3"></i><divclass="hi"><divclass="p-tit">
                        普通快递   运单号:390085324974
                      </div><divclass="hideList"><ul><li>
                            [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                            的快件已签收,感谢您使用韵达快递)签收
                          </li><li>
                            [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                            的快件已签收,感谢您使用韵达快递)签收
                          </li><li>
                            [北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
                          </li><li>
                            [北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
                          </li></ul></div></div></li><liclass="tdLi">订单详情</li></ul></td><tdth:if="${itemStat.index==0}"th:rowspan="${itemStat.size}"><button>确认收货</button><pstyle="margin:4px 0;">取消订单</p><p>催单</p></td></tr></table>

效果展示:

1671460415218

ps:注意:如果使用的 cpolar进行内网穿透测试,每隔24小时需要更换一下 url地址。

获取隧道地址:http://127.0.0.1:9200/#/status/online

更换这个 地址:

服务器[异步通知]页面路径

1671460555477

2.8.6 异步通知内网穿透环境搭建

支付回调异步通知:异步通知参数说明 | 网页&移动应用

支付宝采用的是最终一致性中的最大努力通知策略

1671463109185

①搭建隧道

图形管理界面方式:

1671463141924

或者使用命令行方式:

cpolar http 192.168.56.10:80

测试:需要成功访问到Nginx

1671463246345

配置支付成功后的回调请求路径

alipay.notify_url=http://81953a3.vip.cpolar.cn/payed/notify

订单服务下回调接口编写,成功响应后必须返回给支付宝success

@RestControllerpublicclassOrderPayedListener{@PostMapping("/payed/notify")publicStringhandleAlipayed(HttpServletRequest request){//只要我们收到了支付宝给我们的异步通知,告诉我们订单支付成功。//我们返回 success,支付宝就再也不会通知。Map<String,String[]> map = request.getParameterMap();System.out.println("支付宝通知到位了...数据:"+ map);return"success";}

③ 配置拦截器放过

支付宝异步通知不需要进行登录。

1671463376459

④ 配置Nginx

1671463417115

1671463432725

注意细节:

1.配置域名,否则将会路由给静态页面

2.精确匹配要在模糊匹配的上面

在 gulimall.conf中进行配置:

1671463455242

1671463536934

重启Nginx

docker restart nginx

postman 客户端中测试:

1671463586326

2.8.7 支付完成

1.将支付宝支付成功后的异步通知信息抽取成vo

复制老师课件。

1671466395582

@ToString@DatapublicclassPayAsyncVo{privateString gmt_create;privateString charset;privateString gmt_payment;privateDate notify_time;privateString subject;privateString sign;privateString buyer_id;//支付者的idprivateString body;//订单的信息privateString invoice_amount;//支付金额privateString version;privateString notify_id;//通知idprivateString fund_bill_list;privateString notify_type;//通知类型; trade_status_syncprivateString out_trade_no;//订单号privateString total_amount;//支付的总额privateString trade_status;//交易状态  TRADE_SUCCESSprivateString trade_no;//流水号privateString auth_app_id;//privateString receipt_amount;//商家收到的款privateString point_amount;//privateString app_id;//应用idprivateString buyer_pay_amount;//最终支付的金额privateString sign_type;//签名类型privateString seller_id;//商家的id}

2. 配置SpringMVC日期转化格式

spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

3. 验签,确保是支付宝返回的信息

1671466449104

1671466461066

验签核心代码

//获取支付宝POST过来反馈信息Map<String,String> params =newHashMap<String,String>();Map<String,String[]> requestParams = request.getParameterMap();for(Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();){String name =(String) iter.next();String[] values =(String[]) 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 =newString(valueStr.getBytes("ISO-8859-1"),"utf-8");
        params.put(name, valueStr);}boolean signVerified =AlipaySignature.rsaCheckV1(params,AlipayConfig.alipay_public_key,AlipayConfig.charset,AlipayConfig.sign_type);//调用SDK验证签名//——请在这里编写您的程序(以下代码仅作参考)——/* 实际验证过程建议商户务必添加以下校验:
    1、需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
    2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
    3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email)
    4、验证app_id是否为该商户本身。
    */if(signVerified){//验证成功//商户订单号String out_trade_no =newString(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");//支付宝交易号String trade_no =newString(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");//交易状态String trade_status =newString(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");if(trade_status.equals("TRADE_FINISHED")){//判断该笔订单是否在商户网站中已经做过处理//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序//如果有做过处理,不执行商户的业务程序//注意://退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知}elseif(trade_status.equals("TRADE_SUCCESS")){//判断该笔订单是否在商户网站中已经做过处理//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序//如果有做过处理,不执行商户的业务程序//注意://付款完成后,支付宝系统发送该交易状态通知}
        
        out.println("success");}else{//验证失败
        out.println("fail");//调试用,写文本函数记录程序运行情况是否正常//String sWord = AlipaySignature.getSignCheckContentV1(params);//AlipayConfig.logResult(sWord);}

4. 业务处理(①保存交易流水号②修改订单状态)

确保流水号的唯一性,添加索引

1671466529311

将订单号设置的长度变长一点,防止订单号设置错误。

1671465799047

以下两种状态都是支付成功状态

1671466596920

代码实现:

OrderPayedListener

@RestControllerpublicclassOrderPayedListener{@AutowiredOrderService orderService;@AutowiredAlipayTemplate alipayTemplate;@PostMapping("/payed/notify")publicStringhandleAlipayed(PayAsyncVo vo,HttpServletRequest request)throwsUnsupportedEncodingException,AlipayApiException{//只要我们收到了支付宝给我们的异步通知,告诉我们订单支付成功。//我们返回 success,支付宝就再也不会通知。// Map<String, String[]> map = request.getParameterMap();// for (String key : map.keySet()) {//     String value = request.getParameter(key);//     System.out.println("参数名:"+ key +"==>参数值:"+ value);// }// System.out.println("支付宝通知到位了...数据:"+ map);//验签//获取支付宝POST过来反馈信息Map<String,String> params =newHashMap<String,String>();Map<String,String[]> requestParams = request.getParameterMap();for(Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();){String name =(String) iter.next();String[] values =(String[]) 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";}}}

实现:

OrderServiceImpl

@AutowiredPaymentInfoService paymentInfoService;/**
     * 处理支付宝支付成功修改订单状态
     *
     * @param vo
     * @return
     */@OverridepublicStringhandlePayResult(PayAsyncVo vo){//1、保存交易流水PaymentInfoEntity infoEntity =newPaymentInfoEntity();
        infoEntity.setAlipayTradeNo(vo.getTrade_no());
        infoEntity.setOrderSn(vo.getOut_trade_no());
        infoEntity.setPaymentStatus(vo.getTrade_status());
        infoEntity.setCallbackTime(vo.getNotify_time());

        paymentInfoService.save(infoEntity);//修改订单的状态信息if(vo.getTrade_status().equals("TRADE_SUCCESS")|| vo.getTrade_status().equals("TRADE_FINISHED")){//支付成功状态String outTradeNo = vo.getOut_trade_no();this.baseMapper.updataOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());}return"success";}

OrderDao

voidupdataOrderStatus(@Param("outTradeNo")String outTradeNo,@Param("code")Integer code);

OrderDao.xml

<updateid="updataOrderStatus">
        UPDATE `oms_order` SET `status` = #{code} WHERE order_sn = #{outTradeNo}
    </update>

sql代码:

UPDATE `oms_order` SET `status` = ? WHERE order_sn = ?

测试:

控制台成功打印:

1671466818278

页面显示:已付款

1671466838561

2.8.8 收单

1、订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库 存解锁了。

  • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。

2、由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到

  • 订单解锁,手动调用收单

3、网络阻塞问题,订单支付成功的异步通知一直不到达

  • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝 此订单的状态

4、其他各种问题

  • 每天晚上闲时下载支付宝对账单,一一进行对账

情况一:订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态已经改为已付款但是库存解锁了

解决方案:自动关单

1671506118692

AlipayTemplate

1671506154649

情况二:****由于网络延时原因,订单解锁完成,正要解锁库存时,异步通知才到

解决方案:订单解锁,手动关单

1671506286930

2.9 秒杀业务

2.9.1 后台管理系统完善

启动人人开源后台和前端,点击 优惠营销-每日秒杀。

1671519301277

完善这个每日秒杀业务,当点击每日秒杀,发送的请求:

1671519759095

①网关配置:

1671519795038

②访问成功后,添加两个秒杀场次:

1671507599684

1671507652851

③当点击 每个场次 的关联商品

1671519935238

发送如下请求:1671519965167

修改 后台代码:

首先,这个请求是在 优惠券服务的 SeckillSkuRelationController 的 list方法下,所以修改查询时的参数。

SeckillSkuRelationServiceImpl

@Service("seckillSkuRelationService")publicclassSeckillSkuRelationServiceImplextendsServiceImpl<SeckillSkuRelationDao,SeckillSkuRelationEntity>implementsSeckillSkuRelationService{@OverridepublicPageUtilsqueryPage(Map<String,Object> params){QueryWrapper<SeckillSkuRelationEntity> queryWrapper =newQueryWrapper<SeckillSkuRelationEntity>();//场次 Id不为 nullString promotionSessionId =(String) params.get("promotionSessionId");if(!StringUtils.isEmpty(promotionSessionId)){
            queryWrapper.eq("promotion_session_id",promotionSessionId);}IPage<SeckillSkuRelationEntity> page =this.page(newQuery<SeckillSkuRelationEntity>().getPage(params),
                queryWrapper

        );returnnewPageUtils(page);}}

数据库中按照场次 id 添加一些测试数据。

sms_seckill_session表

1671520114902

sms_seckill_sku_relation表

1671520100126

后台管理系统查看效果:

1671520163823

2.9.2 秒杀微服务搭建

因为 秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+ 异步+ 缓存(页面静态化) + 独立部署;

所以我们需要新建一个微服务来编写 秒杀业务,如果放到其他业务下,譬如放到 商品系统下,可能会因为秒杀业务带来的高并发将数据库或者商品系统压垮。

①新建 gulimall-seckill秒杀服务

1671517122325

暂时配置的依赖:

1671517212785

② 导入公共服务依赖,并排除 seata 依赖。

<dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></exclusion></exclusions></dependency>

③application.properties配置

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.56.10

④主启动类加上 @EnableDiscoveryClient 服务发现注解,因为 common服务引入了 mybatis 的数据库设置,这里排除数据源设置。

1671520506545

2.9.3 SpringBoot整合定时任务&异步任务

1、cron 表达式
语法:秒分时日月周年(Spring 不支持)
官网文档

1671520950888

中文文档:

介绍

cron`是一个已经存在很长时间的 UNIX 工具,因此它的调度功能非常强大且经过验证。CronTrigger类基于 cron 的调度功能`。
CronTrigger

使用“cron 表达式”,它能够创建触发时间表,例如:“每周一至周五上午 8:00”或“每月最后一个周五凌晨 1:30”。

Cron 表达式很强大,但也很容易混淆。本教程旨在揭开创建 cron 表达式的神秘面纱,为用户提供一个资源,他们可以在不必在论坛或邮件列表中提问之前访问该资源。

格式

cron 表达式是由 6 或 7 个字段组成的字符串,由空格分隔。字段可以包含任何允许的值,以及该字段允许的特殊字符的各种组合。字段如下:
字段名称强制的允许值允许的特殊字符秒是的0-59, - * /分钟是的0-59, - * /小时是的0-23, - * /一个月中的第几天是的1-31, - * ?/ 长宽月是的1-12 或 1 月至 12 月, - * /星期几是的1-7 或 SUN-SAT, - * ?/大号#年不空,1970-2099, - * /
1671520965394

  • *“所有值”)- 用于选择字段中的所有值。例如,分钟字段中的“ * ”表示“每分钟”
  • “无特定值”)- 当您需要在允许字符的两个字段之一中指定某些内容而不是另一个时很有用。例如,如果我希望我的触发器在一个月中的特定一天(比如 10 号)触发,但不关心碰巧是星期几,我会在日期中输入“10” -月字段,和“?” 在星期字段中。请参阅下面的示例以进行说明。
  • - - 用于指定范围。例如,小时字段中的“10-12”表示“第 10、11 和 12 小时”
  • , - 用于指定附加值。例如,星期几字段中的“MON,WED,FRI”表示“星期一、星期三和星期五”
  • / - 用于指定增量。例如,秒字段中的“0/15”表示“第 0、15、30 和 45 秒”。秒字段中的“5/15”表示“第 5、20、35 和 50 秒”。您还可以在“ ”字符之后指定“/”——在这种情况下,“ ”相当于在“/”之前添加“0”。day-of-month 字段中的 ‘1/3’ 表示“从该月的第一天开始每 3 天触发一次”
  • L“last”)——在允许使用的两个字段中各有不同的含义。例如,day-of-month 字段中的值“L”表示“该月的最后一天” - 1 月的第 31 天,非闰年的 2 月的第 28 天。如果单独用于星期几字段,它仅表示“7”或“SAT”。但如果在星期几字段中用在另一个值之后,则表示“该月的最后一个 xxx 日” ——例如“6L”表示“该月的最后一个星期五”。您还可以指定从该月最后一天开始的偏移量,例如“L-3”,这表示该日历月的倒数第三天。 使用“L”选项时,重要的是不要指定列表或值范围,因为您会得到令人困惑/意外的结果。
  • W“工作日”)- 用于指定离给定日期最近的工作日(周一至周五)。例如,如果您指定“15W”作为日期字段的值,则含义是: “离该月 15 日最近的工作日”。因此,如果 15 日是星期六,触发器将在 14 日星期五触发。如果 15 号是星期天,触发器将在 16 号星期一触发。如果 15 号是星期二,那么它将在 15 号星期二触发。但是,如果您指定“1W”作为日期的值,并且 1 号是星期六,触发器将在 3 号星期一触发,因为它不会“跳过”一个月的日期边界。“W”字符只能在日期是一天而不是日期范围或列表时指定。

‘L’ 和 ‘W’ 字符也可以在日期字段中组合以产生 ‘LW’,转换为 “该月的最后一个工作日”

  • # - 用于指定该月的“第 n 个”XXX 日。例如,day-of-week 字段中的值“6#3”表示“该月的第三个星期五”(第 6 天 = 星期五,“#3”= 该月的第 3 个星期五)。其他示例:“2#1”= 每月的第一个星期一,“4#5”= 每月的第五个星期三。请注意,如果您指定“#5”并且该月中给定的星期几不是第 5 天,那么该月将不会触发。

合法字符以及月份和星期几的名称不区分大小写。

MON与``mon

相同。

2、cron 示例
表达****意义

0 0 12 * * ?

每天中午 12 点(中午)触发

0 15 10 ?* *

每天上午 10:15 触发

0 15 10 * * ?

每天上午 10:15 触发

0 15 10 * * ?*

每天上午 10:15 触发

0 15 10 * * ?2005年

2005 年每天上午 10:15 触发

0 * 14 * * ?

每天从下午 2 点开始到下午 2:59 结束,每分钟触发一次

0 0/5 14 * * ?

每天从下午 2 点开始到下午 2:55 结束,每 5 分钟触发一次

0 0/5 14,18 * * ?

从下午 2 点开始到下午 2:55 结束,每 5 分钟触发一次,从下午 6 点开始到下午 6:55 结束,每 5 分钟触发一次,每天

0 0-5 14 * * ?

每天从下午 2 点开始到下午 2:05 结束,每分钟触发一次

0 10,44 14 ?3 星期三

在 3 月份的每个星期三的下午 2:10 和下午 2:44 触发。

0 15 10 ?* 周一至周五

每周一、周二、周三、周四和周五上午 10:15 触发

0 15 10 15 * ?

每月第 15 天上午 10:15 触发

0 15 10 升 * ?

每月最后一天上午 10:15 触发

0 15 10 L-2 * ?

在每个月的倒数第二天上午 10:15 触发

0 15 10 ?* 6L

在每个月的最后一个星期五上午 10:15 触发

0 15 10 ?* 6L

在每个月的最后一个星期五上午 10:15 触发

0 15 10 ?* 6L 2002-2005

在 2002、2003、2004 和 2005 年的每个月的最后一个星期五上午 10:15 触发

0 15 10 ?* 6#3

在每个月的第三个星期五上午 10:15 触发

0 0 12 1/5 * ?

从每月的第一天开始,每月每 5 天在中午 12 点(中午)触发。

0 11 11 11 11 ?

每年 11 月 11 日上午 11:11 触发。

注意’?'的影响 在星期几和星期几字段中使用“*”!

可以使用在线工具进行表达式的快速编写:https://cron.qqe2.com/

3、SpringBoot 整合

秒杀 服务下编写 HelloSchedule

/**
 * 定时任务
 *   1、@EnableScheduling //开启定时任务
 *   2、@Scheduled  开启一个定时任务
 *   3、自动配置类 TaskSchedulingAutoConfiguration   属性绑定在  TaskSchedulingProperties
 *
 * 异步任务
 *   1、@EnableAsync 开启异步任务功能
 *   2、@Async 给希望异步执行的方法上标注
 *   3、自动配置类  TaskExecutionAutoConfiguration  属性绑定在  TaskExecutionProperties
 */@EnableAsync//开启异步任务@EnableScheduling//开启定时任务@Slf4j@ComponentpublicclassHelloSchedule{/**
     * 1、Spring中6位组成,不允许第7位的年
     * 2、在周几的位置,1-7代表周一到周日;也可以使用  MON-SUN
     * 3、定时任务不应该阻塞。默认是阻塞的。
     *   1)、可以让业务运行以异步的方式,自己提交到线程池
     *       CompletableFuture.runAsync(()->{
     *             xxxxService.hello();
     *         },executor);
     *  2)、支持定时任务线程池:设置 TaskSchedulingProperties;
     *      spring.task.scheduling.pool.size=5
     *
     * 3)、让定时任务异步执行
     *      异步任务;
     *
     * 解决:使用异步 + 定时任务来完成定时任务不阻塞的功能。
     *
     */@Async@Scheduled(cron ="* * * ? * 2")publicvoidhello()throwsInterruptedException{
        log.info("hello...");Thread.sleep(3000);}}

application.properties

#定时任务线程池设置
#不同版本下,该配置有时候生效,有时候不生效
#spring.task.scheduling.pool.size=5

#异步任务线程池设置
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50

最终方案:使用异步 + 定时任务来完成定时任务不阻塞的功能。

因为 @Scheduled默认是一个单线程,如果开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

2.9.4 时间日期处理

1、创建 SeckillSkuScheduled

@EnableAsync// 3.开启异步任务:防止定时任务之间相互阻塞@EnableScheduling// 2.开启定时任务@Configuration//1.主要用于标记配置类,兼备Component的效果。publicclassScheduledConfig{}

2、编写 接口

SeckillSkuScheduled

/**
 * 秒杀商品 的定时上架:
 *   每天晚上3点:上架最近3天需要秒杀的商品
 *   当天 00:00:00  -  23:59:59
 *   明天 00:00:00  -  23:59:59
 *   后天 00:00:00  -  23:59:59
 */@Slf4j@ServicepublicclassSeckillSkuScheduled{@AutowiredSeckillService seckillServicel;@Scheduled(cron ="0 0 3 * * ?")publicvoiduploadSeckillSkuLatest3Days(){//1、重复上架无需处理
        seckillServicel.uploadSeckillSkuLatest3Days();}}

实现:

SeckillService

publicinterfaceSeckillService{voiduploadSeckillSkuLatest3Days();}

SeckillServiceImpl

@ServicepublicclassSeckillServiceImplimplementsSeckillService{@OverridepublicvoiduploadSeckillSkuLatest3Days(){//1、扫描最近3天需要参与秒杀的活动}}

远程查询 最近3天需要参与秒杀的活动商品

@FeignClient("gulimall-coupon")publicinterfaceCouponFeignService{}

3、远程服务编写

SeckillSessionController

@GetMapping("/lates3DaySession")publicRgetLates3DaySession(){List<SeckillSessionEntity> sessions = seckillSessionService.getLates3DaysSession();returnR.ok().setData(sessions);}

时间日期处理:

获取最近 3天的时间范围测试:

@TestpublicvoidcontextLoads(){LocalDate now =LocalDate.now();LocalDate plus = now.plusDays(1);LocalDate plus2 = now.plusDays(2);/**
         * 2022-12-20
         * 2022-12-21
         * 2022-12-22
         */System.out.println(now);System.out.println(plus);System.out.println(plus2);LocalTime min =LocalTime.MIN;LocalTime max =LocalTime.MAX;/**
         * 00:00
         * 23:59:59.999999999
         */System.out.println(min);System.out.println(max);/**
         * 2022-12-20T00:00
         * 2022-12-22T23:59:59.999999999
         */LocalDateTime start =LocalDateTime.of(now, min);LocalDateTime end =LocalDateTime.of(plus2, max);System.out.println(start);System.out.println(end);}

SeckillSessionServiceImpl

@OverridepublicList<SeckillSessionEntity>getLates3DaysSession(){//计算最近3天// Date date = new Date();// 2022-12-20 13:59:16List<SeckillSessionEntity> list =this.list(newQueryWrapper<SeckillSessionEntity>().between("start_time",startTime(),endTime()));return list;}//当前天数的 00:00:00privateStringstartTime(){LocalDate now =LocalDate.now();LocalTime min =LocalTime.MIN;LocalDateTime start =LocalDateTime.of(now, min);String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return format;}//当前天数+2 23:59:59privateStringendTime(){LocalDate now =LocalDate.now();//加 2天LocalDate localDate = now.plusDays(2);LocalTime max =LocalTime.MAX;LocalDateTime end =LocalDateTime.of(localDate, max);String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return format;}

2.9.5 秒杀商品上架

1、秒杀系统关注的问题

  • 1、服务单一职责+独立部署- 秒杀服务即使自己扛不住压力,挂掉。不要影响别人
  • 2、秒杀链接加密- 防止恶意攻击,模拟秒杀请求,1000次/s攻击- 防止链接暴露,自己工作人员,提前秒杀商品。
  • 3、库存预热+快速扣减- 秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
  • 4、动静分离- nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。 使用cDN网络,分担本集群压力
  • 5、恶意请求拦截- 识别非法攻击请求并进行拦截,网关层
  • 6、流量错峰- 使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
  • 7、限流&熔断&降级- 前端限流+后端限流- 限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
  • 8、队列削峰- 1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。

人人开源后台vue的后台管理系统里上架秒杀,打开F12看url然后去编写逻辑

  • 秒杀名称
  • 开始时间
  • 结束时间
  • 启用状态

2、秒杀架构设计
(1) 秒杀架构
nginx–>gateway–>redis分布式信号量–> 秒杀服务

  • 项目独立部署,独立秒杀模块gulimall-seckill
  • 使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
  • 秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
  • 库存预热,先从数据库中扣除一部分库存以redisson 信号量的形式存储在redis中
  • 队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单

秒杀活动:存在在scekill:sesssions这个redis-key里,。value为 skyIds[]

秒杀活动里具体商品项:是一个map,redis-key是seckill:skus,map-key是skuId+商品随机码

(2) redis存储模型设计

img

  • 秒杀场次存储的List可以当做hash key在SECKILL_CHARE_PREFIX中获得对应的商品数据
  • 随机码防止人在秒杀一开放就秒杀完,必须携带上随机码才能秒杀
  • 结束时间
  • 设置分布式信号量,这样就不会每个请求都访问数据库。seckill:stock:#(商品随机码)
  • session里存了session-sku[]的列表,而seckill:skus的key也是session-sku,不要只存储sku,不能区分场次
//存储的秒杀场次对应数据//K: SESSION_CACHE_PREFIX + startTime + "_" + endTime//V: sessionId+"-"+skuId的ListprivatefinalStringSESSION_CACHE_PREFIX="seckill:sessions:";//存储的秒杀商品数据//K: 固定值SECKILL_CHARE_PREFIX//V: hash,k为sessionId+"-"+skuId,v为对应的商品信息SeckillSkuRedisToprivatefinalStringSECKILL_CHARE_PREFIX="seckill:skus";//K: SKU_STOCK_SEMAPHORE+商品随机码//V: 秒杀的库存件数privatefinalStringSKU_STOCK_SEMAPHORE="seckill:stock:";//+商品随机码

接下来完善 秒杀商品上架业务。

获取最近三天的秒杀信息

  • 获取最近三天的秒杀场次信息,再通过秒杀场次id查询对应的商品信息
  • 防止集群多次上架

SeckillSessionServiceImpl

@OverridepublicList<SeckillSessionEntity>getLates3DaysSession(){//计算最近3天// Date date = new Date();// 2022-12-20 13:59:16//获取最近3天的秒杀活动List<SeckillSessionEntity> list =this.list(newQueryWrapper<SeckillSessionEntity>().between("start_time",startTime(),endTime()));//获设置秒杀活动里面的秒杀商品if(list !=null&& list.size()>0){List<SeckillSessionEntity> collect = list.stream().map(session ->{//给每一个活动写入他们的秒杀项Long id = session.getId();//根据活动场次 id 获取每个 sku项List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(newQueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                session.setRelationSkus(relationEntities);return session;}).collect(Collectors.toList());return collect;}returnnull;}
  • SeckillSessionEntity中添加字段
@TableField(exist =false)//表示不是数据库中存在的字段privateList<SeckillSkuRelationEntity> relationSkus;
  • 秒杀服务编写调用库存远程服务

CouponFeignService

@FeignClient("gulimall-coupon")publicinterfaceCouponFeignService{@GetMapping("/coupon/seckillsession/lates3DaySession")RgetLates3DaySession();}

完善 SeckillSkuScheduled 的 uploadSeckillSkuLatest3Days方法

SeckillServiceImpl

秒杀商品上架

@AutowiredCouponFeignService couponFeignService;/**
     * 秒杀商品上架
     */@OverridepublicvoiduploadSeckillSkuLatest3Days(){//1、扫描最近3天需要参与秒杀的活动R session = couponFeignService.getLates3DaySession();if(session.getCode()==0){//上架商品List<SeckillSessionsWithSkus> sessionData = session.getData(newTypeReference<List<SeckillSessionsWithSkus>>(){});//缓存到redis//1、缓存活动信息saveSessionInfo(sessionData);//2、缓存活动的关联商品信息saveSessionSkuInfos(sessionData);}}

SeckillSessionsWithSkus:复制SeckillSessionEntity

@DatapublicclassSeckillSessionsWithSkus{/**
     * id
     */privateLong id;/**
     * 场次名称
     */privateString name;/**
     * 每日开始时间
     */privateDate startTime;/**
     * 每日结束时间
     */privateDate endTime;/**
     * 启用状态
     */privateInteger status;/**
     * 创建时间
     */privateDate createTime;privateList<SeckillSkuVo>  relationSkus;}

SeckillSkuVo:复制 SeckillSkuRelationEntity

@DatapublicclassSeckillSkuVo{/**
     * id
     */privateLong id;/**
     * 活动id
     */privateLong promotionId;/**
     * 活动场次id
     */privateLong promotionSessionId;/**
     * 商品id
     */privateLong skuId;/**
     * 秒杀价格
     */privateBigDecimal seckillPrice;/**
     * 秒杀总量
     */privateBigDecimal seckillCount;/**
     * 每人限购数量
     */privateBigDecimal seckillLimit;/**
     * 排序
     */privateInteger seckillSort;}

Redis保存秒杀场次信息

@AutowiredStringRedisTemplate redisTemplate;privatefinalStringSESSIONS_CACHE_PREFIX="seckill:sessions:";/**
     * 1 缓存活动信息
     * @param sessions
     */privatevoidsaveSessionInfo(List<SeckillSessionsWithSkus> sessions){

        sessions.stream().forEach(session ->{Long startTime = session.getStartTime().getTime();Long endTime = session.getEndTime().getTime();String key =SESSIONS_CACHE_PREFIX+ startTime +"_"+ endTime;List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());//缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,collect);});}

redis保存秒杀商品信息

前面已经缓存了sku项的活动信息,但是只有活动id和skuID,接下来我们要保存完整是sku信息到redis中

@AutowiredProductFeignService productFeignService;@AutowiredRedissonClient redissonClient;privatefinalStringSKUKILL_CACHE_PREFIX="seckill:skus";privatefinalStringSKU_STOCK_SEMAPHORE="seckill:stock:";// + 商品随机码/**
     * 2 缓存商品信息
     * @param sessions
     */privatevoidsaveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions){

        sessions.stream().forEach(session ->{//准备hash操作BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo ->{//缓存商品SeckillSkuRedisTo redisTo =newSeckillSkuRedisTo();//1、sku的基本数据R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());if(skuInfo.getCode()==0){SkuInfoVo info = skuInfo.getData("skuInfo",newTypeReference<SkuInfoVo>(){});
                    redisTo.setSkuInfo(info);}//2、sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo,redisTo);//3、设置上当前商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());//4、随机码   seckill?skuId=1&key=jaojgoajgoa;String token =UUID.randomUUID().toString().replace("-","");
                redisTo.setRandomCode(token);//5、使用库存作为分布式的信号量  :限流RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+ token);//商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());String jsonString =JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId().toString(),jsonString);});});}

这里需要远程调用 商品服务 下 SkuInfoController的 info()方法查询sku信息。

编写 ProductFeignService

@FeignClient("gulimall-product")publicinterfaceProductFeignService{@RequestMapping("/product/skuinfo/info/{skuId}")//@RequiresPermissions("product:skuinfo:info")RgetSkuInfo(@PathVariable("skuId")Long skuId);}

编写 to 封装数据

@DatapublicclassSeckillSkuRedisTo{/**
     * 活动id
     */privateLong promotionId;/**
     * 活动场次id
     */privateLong promotionSessionId;/**
     * 商品id
     */privateLong skuId;/**
     * 秒杀价格
     */privateBigDecimal seckillPrice;/**
     * 秒杀总量
     */privateBigDecimal seckillCount;/**
     * 每人限购数量
     */privateBigDecimal seckillLimit;/**
     * 排序
     */privateInteger seckillSort;//sku详细信息privateSkuInfoVo skuInfo;//当前sku的秒杀开始时间privateLong startTime;//当前sku的秒杀结束时间privateLong endTime;//商品秒杀随机码privateString randomCode;}

SkuInfoVo:复制SkuInfoEntity

@DatapublicclassSkuInfoVo{/**
     * skuId
     */privateLong skuId;/**
     * spuId
     */privateLong spuId;/**
     * sku名称
     */privateString skuName;/**
     * sku介绍描述
     */privateString skuDesc;/**
     * 所属分类id
     */privateLong catalogId;/**
     * 品牌id
     */privateLong brandId;/**
     * 默认图片
     */privateString skuDefaultImg;/**
     * 标题
     */privateString skuTitle;/**
     * 副标题
     */privateString skuSubtitle;/**
     * 价格
     */privateBigDecimal price;/**
     * 销量
     */privateLong saleCount;}

需要引入 redisson依赖

<!-- 以后使用Redisson作为所有分布式锁,分布式对象等功能框架 --><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.12.0</version></dependency>

复制MyRedissonConfig

@ConfigurationpublicclassMyRedissonConfig{/**
     * 所有对 Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */@Bean(destroyMethod ="shutdown")publicRedissonClientredisson()throwsIOException{//1、创建配置Config config =newConfig();// Redis url should start with redis:// or rediss:// (for SSL connection)
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");//2、根据Config 创建出 RedissonClient实例RedissonClient redissonClient =Redisson.create(config);return redissonClient;}}

2.9.6 幂等性处理

1671552813799

避免高并发下多机器同时上架情况。

修改代码:

SeckillSkuScheduled

/**
     * 定时任务
     * 每天三点上架最近三天的秒杀商品
     *///TODO 幂等性处理@Scheduled(cron ="*/3 * * * * ?")publicvoiduploadSeckillSkuLatest3Days(){//1、重复上架无需处理
        log.info("上架秒杀的商品信息...");//分布式锁:拿到锁的机器才执行:锁的业务执行完成,状态已经更新完成,释放锁以后,其他人获取到就会拿到最新的状态。//为避免分布式情况下多服务同时上架的情况,使用分布式锁RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10,TimeUnit.SECONDS);//锁住try{
            seckillServicel.uploadSeckillSkuLatest3Days();}finally{
            lock.unlock();//解锁}}

ps:这里为了开发测试效果,改为了每 3秒 做一次上架商品的定时任务。

SeckillServiceImpl

1671553012158

/**
     * 1 缓存活动信息
     * @param sessions
     */privatevoidsaveSessionInfo(List<SeckillSessionsWithSkus> sessions){
        sessions.stream().forEach(session ->{Long startTime = session.getStartTime().getTime();Long endTime = session.getEndTime().getTime();String key =SESSIONS_CACHE_PREFIX+ startTime +"_"+ endTime;Boolean hasKey = redisTemplate.hasKey(key);//幂等性处理// 防止重复添加活动到redis中if(!hasKey){// 获取所有商品id // 格式:活动id-skuIdList<String> collect = session.getRelationSkus().stream().map(item ->
                        item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());//缓存活动信息
                redisTemplate.opsForList().leftPushAll(key,collect);}});}

1671552964345

/**
     * 2 缓存商品信息
     * @param sessions
     */privatevoidsaveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions){// 遍历session
        sessions.stream().forEach(session ->{//准备hash操作BoundHashOperations<String,Object,Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);// 遍历sku
            session.getRelationSkus().stream().forEach(seckillSkuVo ->{//4、随机码   seckill?skuId=1&key=jaojgoajgoa;String token =UUID.randomUUID().toString().replace("-","");//幂等性处理//只需要上架一次,如果已经上架,就不需要再上架// 缓存中没有再添加if(!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+ seckillSkuVo.getSkuId().toString())){//缓存商品SeckillSkuRedisTo redisTo =newSeckillSkuRedisTo();//1、sku的基本数据   sku的秒杀信息R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());if(skuInfo.getCode()==0){SkuInfoVo info = skuInfo.getData("skuInfo",newTypeReference<SkuInfoVo>(){});
                        redisTo.setSkuInfo(info);}//2、sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo,redisTo);//3、设置上当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());// 设置随机码
                    redisTo.setRandomCode(token);String jsonString =JSON.toJSONString(redisTo);// 活动id-skuID   秒杀sku信息
                    ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(),jsonString);//5、使用库存作为分布式的信号量  :限流RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+ token);//商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());}});});}

redis中效果图:

1671553150197

1671553165732

1671553174405

1671553186420

1671553194532

1671553205595

2.9.7 查询秒杀商品

前面已经在redis中缓存了秒杀活动的各种信息,现在写获取缓存中当前时间段在秒杀的sku,用户点击页面后发送请求。

  • 接口方法编写

SeckillController

@RestControllerpublicclassSeckillController{@AutowiredSeckillService seckillService;/**
     * 返回当前时间可以参与的秒杀商品信息
     * @return
     */@GetMapping("/currentSeckillSkus")publicRgetCurrentSeckillSkus(){List<SeckillSkuRedisTo>  vos  =  seckillService.getCurrentSeckillSkus();returnR.ok().setData(vos);}}

SeckillServiceImpl

//返回当前时间可以参与的秒杀商品信息@OverridepublicList<SeckillSkuRedisTo>getCurrentSeckillSkus(){//1、确定当前时间属于哪个秒杀场次long time =newDate().getTime();Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX+"*");for(String key : keys){//seckill:sessions:1671674400000_1671678000000String replace = key.replace(SESSIONS_CACHE_PREFIX,"");//截串String[] s = replace.split("_");//分割Long start =Long.parseLong(s[0]);Long end =Long.parseLong(s[1]);if(time >= start  && time <= end){//2、获取这个秒杀场次需要的所有商品信息List<String> range = redisTemplate.opsForList().range(key,-100,100);//获取到hash   key----seckill:skusBoundHashOperations<String,String,String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);List<String> list = hashOps.multiGet(range);if(list !=null){List<SeckillSkuRedisTo> collect = list.stream().map(item ->{SeckillSkuRedisTo redis =JSON.parseObject((String) item,SeckillSkuRedisTo.class);// redis.setRandomCode(null);//当前秒杀开始就需要随机码return redis;}).collect(Collectors.toList());return collect;}break;}}returnnull;}
  • 网关配置

域名映射

1671600685353

网关服务

-id: gulimall_seckill_route
          uri: lb://gulimall-seckill
          predicates:- Host=seckill.gulimall.com

首页获取并渲染

ul中的各个 li标签全部删除掉,使用 Ajax 局部获取刷新。

1671602421785

ajax请求:

 $.get("http://seckill.gulimall.com/currentSeckillSkus",function(resp){if(resp.data.length >0){
      resp.data.forEach(function(item){$("<li></li>").append($("<img style='width: 130px;height: 130px' src='"+item.skuInfo.skuDefaultImg+"'/>")).append($("<p>"+ item.skuInfo.skuTitle+"</p>")).append($("<span>"+ item.seckillPrice+"</span>")).append($("<s>"+ item.skuInfo.price+"</s>")).appendTo("#seckillSkuContent");})}//append 添加元素//appendTo 添加到哪个位置去// <li>//   <img src="/static/index/img/section_second_list_img1.jpg" alt="">//     <p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p>//     <span>¥83.9</span><s>¥99.9</s>// </li>});

效果展示:

1671602637012

2.9.8 秒杀页面渲染

  • 秒杀服务下的SeckillController
/**
     * 获取当前商品的秒杀信息
     * @param skuId
     * @return
     */@GetMapping("/sku/seckill/{skuId}")publicRgetSkuSeckillInfo(@PathVariable("skuId")Long skuId){SeckillSkuRedisToto=   seckillService.getSkuSeckillInfo(skuId);returnR.ok().setData(to);}

SeckillServiceImpl

// 获取当前商品的秒杀信息@OverridepublicSeckillSkuRedisTogetSkuSeckillInfo(Long skuId){//1、找到所有需要参与秒杀的商品的keyBoundHashOperations<String,String,String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);Set<String> keys = hashOps.keys();if(keys !=null&& keys.size()>0){String regx ="\\d_"+ skuId;//6_4   正则匹配for(String key : keys){if(Pattern.matches(regx, key)){String json = hashOps.get(key);SeckillSkuRedisTo skuRedisTo =JSON.parseObject(json,SeckillSkuRedisTo.class);//随机码long current =newDate().getTime();if(current >= skuRedisTo.getStartTime()&& current <= skuRedisTo.getEndTime()){}else{
                        skuRedisTo.setRandomCode(null);}return skuRedisTo;};}}returnnull;}

商品服务

编写 SeckillFeignService远程调用

@FeignClient("gulimall-seckill")publicinterfaceSeckillFeignService{@GetMapping("/sku/seckill/{skuId}")RgetSkuSeckillInfo(@PathVariable("skuId")Long skuId);}

在查询商品详情页的接口中查询秒杀对应信息:

SkuInfoServiceImpl 下的 item方法完善

1671609720053

@AutowiredSeckillFeignService seckillFeignService;@OverridepublicSkuItemVoitem(Long skuId)throwsExecutionException,InterruptedException{SkuItemVo skuItemVo =newSkuItemVo();// 第一步获得的数据,第3步、4步、5步也要使用CompletableFuture<SkuInfoEntity> infoFuture =CompletableFuture.supplyAsync(()->{//1、sku基本信息获取  pms_sku_infoSkuInfoEntity info =getById(skuId);
            skuItemVo.setInfo(info);return info;}, executor);CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res)->{//3、获取的spu的销售属性组合。List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);}, executor);CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res)->{//4、获取spu的介绍 pms_spu_info_descSpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);}, executor);CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res)->{//5、获取spu的规格参数信息。List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);}, executor);CompletableFuture<Void> imageFuture =CompletableFuture.runAsync(()->{//2、sku的图片信息  pms_sku_imagesList<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);}, executor);CompletableFuture<Void> secKillFuture =CompletableFuture.runAsync(()->{//3、查询当前sku是否参与秒杀优惠R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);if(seckillInfo.getCode()==0){SeckillInfoVo seckillInfoVo = seckillInfo.getData(newTypeReference<SeckillInfoVo>(){});
                skuItemVo.setSeckillInfoVo(seckillInfoVo);}}, executor);//等待所有任务都完成CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).get();return skuItemVo;}

注意所有的时间都是距离1970的差值

  • 更改商品详情页的显示效果:显示秒杀预告、秒杀价等

1671609840381

<listyle="color: red;"th:if="${item.seckillInfoVo != null}"><spanth:if="${#dates.createNow().getTime() < item.seckillInfoVo.startTime}">
                                    商品将会在[[${#dates.format(new java.util.Date(item.seckillInfoVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行抢购
                                    </span><spanth:if="${#dates.createNow().getTime() >= item.seckillInfoVo.startTime && #dates.createNow().getTime() <= item.seckillInfoVo.endTime}">
                                        秒杀价:[[${#numbers.formatDecimal(item.seckillInfoVo.seckillPrice,1,2)}]]
                                    </span></li>
  • 首页点击秒杀商品,跳转到商品详情页

1671609891333

functionto_href(skuId){
    location.href ="http://item.gulimall.com/"+ skuId +".html";}

  $.get("http://seckill.gulimall.com/currentSeckillSkus",function(resp){if(resp.data.length >0){
      resp.data.forEach(function(item){$("<li οnclick='to_href("+item.skuId+")'></li>").append($("<img style='width: 130px;height: 130px' src='"+item.skuInfo.skuDefaultImg+"'/>")).append($("<p>"+ item.skuInfo.skuTitle+"</p>")).append($("<span>"+ item.seckillPrice+"</span>")).append($("<s>"+ item.skuInfo.price+"</s>")).appendTo("#seckillSkuContent");})}

效果展示:

该商品在秒杀时间中,显示秒杀价格

1671609920252

如果该商品不在秒杀时间内,显示 秒杀预告。

1671609935735

2.9.9 秒杀系统设计

1671629279699

02:使用随机码

03:本次秒杀使用 redis预热库存(信号量)

1671629414402

05:本次秒杀使用登录拦截器

08:给MQ发送消息

之前我们已经做了 前面 4步,接下来我们要完善后面4步。

2.9.10 登录检查

秒杀的最终的处理

  • 秒杀商品定时上架

img

  • 秒杀业务开始

img

①晚上 详情页的页面效果:如果不在秒杀时间范围内,显示 “加入购物车”;如果在秒杀时间范围内,显示“立即抢购”,并进行跳转,跳转的路径带上 商品 id 和 场次 id 和 随机码、以及数量。

item.html

<divclass="box-btns-two"th:if="${#dates.createNow().getTime() >= item.seckillInfoVo.startTime && #dates.createNow().getTime() <= item.seckillInfoVo.endTime}"><ahref="#"id="secKillA"th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfoVo.promotionSessionId},code=${item.seckillInfoVo.randomCode}">
                                    立即抢购
                                </a></div><divclass="box-btns-two"th:if="${#dates.createNow().getTime() < item.seckillInfoVo.startTime || #dates.createNow().getTime() > item.seckillInfoVo.endTime}"><ahref="#"id="addToCartA"th:attr="skuId=${item.info.skuId}">
                                    加入购物车
                                </a></div>

跳转函数。

$("#secKillA").click(function(){var isLogin =[[${session.loginUser !=null}]];//trueif(isLogin){var killId =$(this).attr("sessionId")+"_"+$(this).attr("skuId");var key =$(this).attr("code");var num =$("#numInput").val();
                location.href ="http://seckill.gulimall.com/kill?killId="+killId+"&key="+key+"&num="+num;}else{alert("秒杀请先登录");}returnfalse;})

②编写去秒杀时的登录检查

  • 加入session有关等依赖:spring-boot-starter-data-redis排除 lettuce-core,使用jedis
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>
  • application.properties配置
spring.session.store-type=redis
  • 主启动类加上 开启 @EnableRedisHttpSession 注解

1671706979769

复制有关session配置和拦截器配置:

GulimallSessionConfig:全局 session配置

@ConfigurationpublicclassGulimallSessionConfig{@BeanpublicCookieSerializercookieSerializer(){DefaultCookieSerializer cookieSerializer =newDefaultCookieSerializer();

        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@BeanpublicRedisSerializer<Object>springSessionDefaultRedisSerializer(){returnnewGenericJackson2JsonRedisSerializer();}}

LoginUserInterceptor:拦截器

@ComponentpublicclassLoginUserInterceptorimplementsHandlerInterceptor{publicstaticThreadLocal<MemberRespVo> loginUser =newThreadLocal<>();@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{// 库存解锁需要远程调用---->需要登录:所以设置拦截器不拦截 order/order/status/{orderSn}。// 对于查询订单等请求直接放行。// /order/order/status/45648913346789494String uri = request.getRequestURI();AntPathMatcher antPathMatcher =newAntPathMatcher();//不是真正点击去秒杀【立即抢购】的都放行boolean match = antPathMatcher.match("/kill", uri);if(match){//获取登录用户MemberRespVo attribute =(MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if(attribute !=null){//登录成功后,将用户信息存储至ThreadLocal中方便其他服务获取用户信息://加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查
                loginUser.set(attribute);returntrue;}else{//没登录就去登录
                request.getSession().setAttribute("msg","请先进行登录");
                response.sendRedirect("http://auth.gulimall.com/login.html");returnfalse;}}returntrue;}}

SecKillWebController:注册拦截器

@ConfigurationpublicclassSecKillWebControllerimplementsWebMvcConfigurer{@AutowiredLoginUserInterceptor loginUserInterceptor;@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){

        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}}
  • 接口方法编写

SeckillController

/**
     * 秒杀商品
     * @param killId
     * @param key
     * @param code
     * @return
     */@GetMapping("/kill")publicRsecKill(@RequestParam("killId")String killId,@RequestParam("key")String key,@RequestParam("code")String code){//1、判断是否登录returnnull;}

2.9.11 秒杀流程

  • 秒杀方案:

第一种:

img

优点:分散流量;缺点:流量会级联映射到其他系统里面,极限情况下,造成各个系统崩溃

第二种

img

优点:在秒杀开始到快速创建订单过程中,没有进行一次数据库操作或者远程调用,只需要校验数据的合法性,因为所有数据都在缓存中放着。
缺点:如果订单服务已经崩溃了,那秒杀服务发出的消息一直不能消费,订单一直支付不成功。

这里我们采用 第二种方案。

消息队列:

img

img

秒杀controller:

/**
     * 秒杀商品
     * @param killId
     * @param key
     * @param code
     * @return
     */@GetMapping("/kill")publicRsecKill(@RequestParam("killId")String killId,@RequestParam("key")String key,@RequestParam("num")Integer num){//秒杀成功就创建一个订单号String orderSn = seckillService.kill(killId,key,num);//1、判断是否登录returnR.ok().setData(orderSn);}

秒杀service:创建订单、发消息

@OverridepublicStringkill(String killId,String key,Integer num){//从拦截器中获取当前用户信息MemberRespVo respVo =LoginUserInterceptor.loginUser.get();//1、获取当前秒杀商品的详细信息BoundHashOperations<String,String,String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);String json = hashOps.get(killId);if(StringUtils.isEmpty(json)){returnnull;}else{SeckillSkuRedisTo redis =JSON.parseObject(json,SeckillSkuRedisTo.class);//校验合法性Long startTime = redis.getStartTime();Long endTime = redis.getEndTime();long time =newDate().getTime();//1、校验时间的合法性if(time >= startTime && time <= endTime){//2、校验随机码和商品idString randomCode = redis.getRandomCode();String skuId = redis.getPromotionSessionId()+"_"+ redis.getSkuId();if(randomCode.equals(key)&& killId.equals(skuId)){//3、验证购物数量是否合理if(num <= redis.getSeckillLimit().intValue()){//4、验证这个人是否已经购买过。幂等性:只要秒杀成功,就去占位。  userId_SessionId_skuId//SETNXString redisKey = respVo.getId()+"_"+ skuId;// 让数据自动过期long ttl = redis.getEndTime()- redis.getStartTime();//自动过期Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl,TimeUnit.MILLISECONDS);if(aBoolean){//占位成功说明从来没有买过RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+ randomCode);try{// 120 msboolean b = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);//秒杀成功;// 快速下单。发送MQ消息  10msString timeId =IdWorker.getTimeId();return timeId;}catch(InterruptedException e){returnnull;}}else{//说明已经买过了returnnull;}}}else{returnnull;}}else{returnnull;}}returnnull;}
  • 给 MQ发消息:流程如下:

1671636963761

以上秒杀流程最大的特点就是:流量削峰,不是每个请求过来都要去调用订单服务。

①引入 rabbitmq依赖

<!--引入 操作Rabbitmq依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency>

② application.properties配置

spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10

③ 关于 rabbitmq 的配置类

@ConfigurationpublicclassMyRabbitConfig{/**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */@BeanpublicMessageConvertermessageConverter(){returnnewJackson2JsonMessageConverter();}}

④秒杀服务给 MQ发消息

秒杀的to数据准备(直接放在公共服务中)

SeckillOrderTo

@DatapublicclassSeckillOrderTo{privateString orderSn;//订单号/**
     * 活动场次id
     */privateLong promotionSessionId;/**
     * 商品id
     */privateLong skuId;/**
     * 秒杀价格
     */privateBigDecimal seckillPrice;/**
     * 秒杀总量
     */privateInteger num;//会员idprivateLong memberId;}

发送消息

1671707675941

@OverridepublicStringkill(String killId,String key,Integer num){long s1 =System.currentTimeMillis();//从拦截器中获取当前用户信息MemberRespVo respVo =LoginUserInterceptor.loginUser.get();//1、获取当前秒杀商品的详细信息BoundHashOperations<String,String,String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);String json = hashOps.get(killId);if(StringUtils.isEmpty(json)){returnnull;}else{SeckillSkuRedisTo redis =JSON.parseObject(json,SeckillSkuRedisTo.class);//校验合法性Long startTime = redis.getStartTime();Long endTime = redis.getEndTime();long time =newDate().getTime();//过期时间long ttl = endTime - startTime;//1、校验时间的合法性if(time >= startTime && time <= endTime){//2、校验随机码和商品idString randomCode = redis.getRandomCode();String skuId = redis.getPromotionSessionId()+"_"+ redis.getSkuId();if(randomCode.equals(key)&& killId.equals(skuId)){//3、验证购物数量是否合理if(num <= redis.getSeckillLimit().intValue()){//4、验证这个人是否已经购买过。幂等性:只要秒杀成功,就去占位。  userId_SessionId_skuId//SETNXString redisKey = respVo.getId()+"_"+ skuId;//自动过期Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl,TimeUnit.MILLISECONDS);if(aBoolean){//占位成功说明从来没有买过RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+ randomCode);// 120 msboolean b = semaphore.tryAcquire(num);if(b){//秒杀成功;// 快速下单。发送MQ消息  10msString timeId =IdWorker.getTimeId();SeckillOrderTo orderTo =newSeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(respVo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo);long s2 =System.currentTimeMillis();
                                log.info("耗时...{},(s2-s1)");return timeId;}returnnull;}else{//说明已经买过了returnnull;}}}else{returnnull;}}else{returnnull;}}returnnull;}

在订单服务设置一个队列和一个绑定关系:创建秒杀所需队列

订单服务中的

MyMQConfig
@BeanpublicQueueorderSeckillOrderQueue(){//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsQueue queue =newQueue("order.seckill.order.queue",true,false,false);return queue;}@BeanpublicBindingorderSeckillOrderBinding(){//String destination, DestinationType destinationType, String exchange, String routingKey,//             Map<String, Object> argumentsreturnnewBinding("order.seckill.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.seckill.order",null);}

监听队列:接收消息

SeckilOrderListener

@Slf4j@RabbitListener(queues ="order.seckill.order.queue")@ComponentpublicclassSeckilOrderListener{@AutowiredOrderService orderService;/**
     * 创建秒杀单
     * @param seckillOrder
     * @param channel
     * @param message
     * @throws IOException
     */@RabbitHandlerpublicvoidlistener(SeckillOrderTo seckillOrder,Channel channel,Message message)throwsIOException{try{
            log.info("准备创建秒杀单的详细信息."+""+"..");
            orderService.createSeckillOrder(seckillOrder);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch(Exception e){//失败了重新放到队列中,让其他消费者能够消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}

创建订单: createSeckillOrder

@OverridepublicvoidcreateSeckillOrder(SeckillOrderTo seckillOrder){//TODO 保存订单信息OrderEntity orderEntity =newOrderEntity();
        orderEntity.setOrderSn(seckillOrder.getOrderSn());
        orderEntity.setMemberId(seckillOrder.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(newBigDecimal(""+ seckillOrder.getNum()));
        orderEntity.setPayAmount(multiply);this.save(orderEntity);//TODO 保存订单项信息OrderItemEntity orderItemEntity =newOrderItemEntity();
        orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
        orderItemEntity.setRealAmount(multiply);//TODO 获取当前sku的详细信息进行设置  productFeignService.getSpuInfoBySkuId()

        orderItemEntity.setSkuQuantity(seckillOrder.getNum());

        orderItemService.save(orderItemEntity);}

测试:点击 “立即抢购”:第一次成功

1671714779281

第二次直接返回null。

2.9.12 秒杀页面完成

这里先修改一下 商品详情页的显示 “立即抢购” 和 “加入购物车”的逻辑。

1671716959524

如果不是秒杀商品也要显示

加入购物车

①复制 购物车页面 的 success.html 页面

1671717047627

②引入 thymeleaf依赖

<!--模板引擎:thymeleaf--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>

application.properties配置

spring.thymeleaf.cache=false

③修改 业务逻辑

SeckillController

1671717137500

1671717168991

1671717189245

**除了

/kill

这个请求外,其他都是返回 JSON数据,而

/kill

跳转到 指定页面。**

④编写秒杀成功页面

修改前缀:静态资源改为从购物车服务直接获取。

1671716116011

1671716143284

编写秒杀成功显示:

秒杀成功,显示订单号,以及跳转的支付页面;

秒杀失败,温馨提示。

1671717336429

<divclass="main"><divclass="success-wrap"><divclass="w"id="result"><divclass="m succeed-box"><divth:if="${orderSn != null}"class="mc success-cont"><h1>恭喜,秒杀成功,订单号[[${orderSn}]]</h1><h2>正在准备订单数据,10s以后自动跳转支付
                                <astyle="color: red"th:href="${'http://order.gulimall.com/payOrder?orderSn=' + orderSn}">去支付</a></h2></div><divth:if="${orderSn == null}"class="mc success-cont"><h2>手气不好,秒杀失败,下次再来</h2></div></div></div></div></div>

成功:

1671717428419

失败:

1671717397284

上面我们已经完成了秒杀( 高并发) 系统关注的问题:

  • 01 服务单一职责**+**独立部署
  • 02 秒杀链接加密:使用随机码
  • 03 库存预热+快速扣减:使用redis缓存,预热库存(信号量)
  • 04 动静分离
  • 05 恶意请求拦截:登录拦截器
  • 08 队列削峰:秒杀成功给MQ发送消息(给订单服务发送消息,让其慢慢消费)

06 流量削峰 和 07 限流&熔断&降级没有完成。

以上可以保证高并发系统下 能够快速处理,但是不能保证稳定。

下面我们使用 阿里巴巴 的sentinel进行 第7步:限流&熔断&降级。

参考 谷粒商城高级篇之额外知识补充篇 7 SpringCloud Alibaba-Sentinel

3 高级篇总结

基础篇注重的是 基本的增删改查的能力;搭建的是一个前后分离的环境来做整个后台管理系统的增删改查,其中穿插了一些技术:数据校验、对象存储、一些VO数据的封装等等…

高级篇:实现的是整个商城功能,而不是后台管理系统;其中业务有购物车、结账、详情、检索;将所有的功能都抽取成了一个微服务:也就是说在整个商城系统中,将每个业务拆分成了微服务,由许多微服务共同组合成商城;其中用到的技术和难点:分布式开发期间:核心掌握 SpringCloud组件:包括SpringCloud Alibaba、SpringCloud。

在Springcloud组件中使用的最为频繁的就是 feign远程调用:开启接口,声明feign客户端&&复制目标方法及其签名。

  • 课件详解1671895330266

01 响应式编程

只在最后网关SentinelGatewayConfig类中有使用:网关的sentinel的容错回调中使用了一下;

1671895266958

02 接口幂等性

分布式开发中最需要关注的就是接口幂等性问题:无论服务怎么调用,都希望接口是幂等的;这样我们无论调用多少次,结果都是一样的;这个是我们分布式系统里面业务功能接口的保证;可以通过加锁、数据库的乐观锁、悲观锁字段、加事务等都可以实现接口幂等性;我们在系统中使用最多的就是令牌机制【类似于验证码】:我给你发一个令牌,你下一次给我带上,用过一次就销毁。

03 事务

在分布式系统中,除了每个系统要解决自己的事务问题之外,分布式系统中调用还要有分布式事务的存在;在这个过程中,还顺带讲解了 springcloud alibaba 中的

seata

组件:

它可以解决分布式事务,但是它最好用的地方在于 后台管理系统中使用:比如我们在后台管理系统中添加 一个商品,保存的时候需要将 它的优惠信息、库存等在系统中保存,要成功都成功,要失败都失败,【对于并发性能要求不高的,例如后台管理系统,我们可以使用seata 来做分布式事务】

;
但是对于高并发系统,我们使用的是最终一致性,所以分布式事务的最终解决方案就是 RabbitMQ:首先发送一个消息:【可靠消息的保证:发送端和消费端的两端确认机制】,将消息发送出去后,最终的系统只需要监听这些消息,完了之后根据消息改变我们系统的数据;【不能达到强一致,达到的是柔性事务的最终一致:最终看到的数据是我们想要的就行。】

04 性能与压力测试

对于高并发系统,使用Jmeter进行压力测试;在压测期间,还可以监控JVM的性能,【

使用jvisualvm控制台,或者Jconsole

】。

05 缓存和分布式锁
特别在分布式系统中,我们要保证接口的吞吐量,性能的提升【缓存是必须的】,所以在高并发系统中,缓存是一个很重要的提升性能的手段,但是使用缓存期间,也有一些问题:

缓存的击穿【缓存的单点问题】,缓存雪崩【缓存的大面积问题】,缓存穿透【一直从数据库查询null值】:解决方案:使用分布式

当大并发量全部要来查询数据库,大家先来查询缓存,使用分布式锁来锁住,只有一个请求进来,查询不到缓存再来查询数据库,最终的效果就是只会给数据库放一条查询,而其他人得到锁之后,下次只会来查询缓存,也就不用再去查询数据库了,【分布式锁除了缓存问题使用外,接口幂等性的保证也可以使用】
比如现在有 几个并发的请求,都来调用了一个接口,这个接口只能调用一次,这一次数据是怎么样,那就是怎么样:我们释放锁之后,如果再来调用,就判断一下数据的状态,如果已经被处理了,那就不再处理。
定时任务中也有使用:比如定时启动了3台机器,这3台机器定时同时启动了一个任务,比如都来上架我们的秒杀商品,只要我们使用了分布式锁,我们上架过了,就不需要再次 上架;分布式锁可以来锁住我们所有的机器,让
一个机器来执行这个事情即可。

06 ElasticSearch

我们商品的检索肯定不能放在mysql中进行,通过写一些SQL执行,这样性能会大幅度下降;我们的检索功能:无论是分类检索,还是按照名字检索;所有的检索数据都保存在ElasticSearch中【一个非常专业的检索引擎】,注意它的安装使用都需要非常清楚。

07 异步和线程池

在高并发系统中,异步是非常必须的:我们复习了以前那种简单的

new Thread start

这种简单的异步,如果在高并发系统中,我们每一个请求进来都

new Thread start

,这样资源将很快耗尽;所以为了控制住系统的资源,我们使用线程池:以后所有的异步任务都要提交给线程池;这样的话,线程池中就有一个最大量,比如核心是20个线程,最大是200个,排队可以排500个、800个、乃至更多,我们通过线程池控制住资源,峰值也就是200个正在运行的,以及800个正在排队的,再也不可能占用更多的资源。这样有了资源的统一管控后,我们就不用害怕因为系统的某些资源耗尽导致系统崩溃
我们使用异步,将任务提交给线程池,但是可能异步任务中有顺序,我们可以使用异步编排CompetableFuture

08 单点登录和社交登录

我们着重演示了使用微博进行社交登录,此外还演示了单点登录,比如我们在不同域名下如何登录。但是我们后来是相同域名的,所以我们暂时可以不用使用单点登录。
但是在相同域名下,登录一处,在处处都可以实现单点登录效果,这时我们就整合了 spring session:一处登录,处处可用。【将所有微服务的session进行了同步,只要登录成功之后,去任何微服务,都可以获取到session】—> 使用spring session 解决了分布式系统session不一致问题。

09 商城业务

特别是购物车、订单、秒杀等,所有的业务都做了实现,但是一些细节需要我们自己填充。核心业务:商品上架【后台管理系统】、商品检索、商品详情、购物车、订单、秒杀。其中在商品详情里面,使用最多的是缓存技术:除了整合redis,还整合了

springCache

整合springCache来方便的使用缓存,以后我们的全系统都应该使用这种方式来进行缓存。

缓存出现的不一致问题该如何解决

缓存的清空以及缓存的更新,使用spring cache 都可以很方便的解决这些问题。

10 RabbitMQ

做分布式事务,来实现最终一致性的时候,rabbitmq是一个非常必要的工具。
我们只需要给A服务给B服务发送一个消息,A服务不用关心怎么做。【在订单系统中,我们使用rabbitmq来完成分布式事务】;【在秒杀系统中,加入rabbitmq队列来进行削峰处理:将所有的流量排队放到队列中,由后台慢慢的进行消费】。此外,通过rabbitmq,A服务和B服务不用关心接口调用了,A都不需要调用B,相当于我们将应用之间进行了解耦。
在订单的关单中关于rabbitmq的使用:我们通过死信队列,保证关单的数据都能被关掉。

11 支付

我们整合了支付宝的沙箱来进行支付。

12 定时任务与分布式调度

我们秒杀系统的所有上架,都需要定时任务来做

13 ShardingSphere

对于 13 分库分表 ShardingSphere在高可用做mysql集群时在使用。

14 SpringCloud组件

SpringCloud中的组件都需要进行掌握:除了nacos作为注册中心我们在一直使用外,配置中心只进行了演示;以后我们需要将所有微服务的配置都放给配置中心,【通过nacos的控制台也可以修改配置,修改完了之后微服务可以在不下线的情况下,应用到最新的配置】
最后,我们为了保护整个系统,引入了

sentinel

【作为一个流量哨兵】:从流量入口开始,保护我们任何想要保护的资源;当然,我们最好结合

Sleuth + Zipkin进行服务链路追踪

,这样可以看到我们整个系统的运行状况,链路追踪。可以找出一些异常问题,通过sentinel流量哨兵进行服务的降级,保护整个系统。

技术中最重要的是使用缓存来加快速度。异步也是来加快速度和控制资源。此外通过消息队列,无论是流量削峰,还是分布式做最终一致也好,我们都可以通过消息队列来减轻某一个服务的压力【所有消息存入队列中,闲的时候再来慢慢消费】。

1671896555408

在高级篇构建一个高并发系统,除了

引入springcloud组件或者是SpringCloud Alibaba来作为周边设施

外,高并发有三宝:
缓存、异步、队排好

缓存就是使用redis作为缓存,保证它的缓存一致性,保证缓存的击穿、穿透等这些问题不会出现;

异步结合线程池,不能只是new Tread start;结合线程池以及异步编排来作为整个异步功能。

队排好:使用rabbitmq将所有的高峰流量,或者要做分布式事务的这些消息,全都放进rabbitmq消息队列中,让他们排好队,后台服务一个个处理。

font>

  • 课件详解[外链图片转存中…(img-pm0w0T8a-1672324216619)]

01 响应式编程

只在最后网关SentinelGatewayConfig类中有使用:网关的sentinel的容错回调中使用了一下;

[外链图片转存中…(img-JVcQRMQa-1672324216619)]

02 接口幂等性

分布式开发中最需要关注的就是接口幂等性问题:无论服务怎么调用,都希望接口是幂等的;这样我们无论调用多少次,结果都是一样的;这个是我们分布式系统里面业务功能接口的保证;可以通过加锁、数据库的乐观锁、悲观锁字段、加事务等都可以实现接口幂等性;我们在系统中使用最多的就是令牌机制【类似于验证码】:我给你发一个令牌,你下一次给我带上,用过一次就销毁。

03 事务

在分布式系统中,除了每个系统要解决自己的事务问题之外,分布式系统中调用还要有分布式事务的存在;在这个过程中,还顺带讲解了 springcloud alibaba 中的

seata

组件:

它可以解决分布式事务,但是它最好用的地方在于 后台管理系统中使用:比如我们在后台管理系统中添加 一个商品,保存的时候需要将 它的优惠信息、库存等在系统中保存,要成功都成功,要失败都失败,【对于并发性能要求不高的,例如后台管理系统,我们可以使用seata 来做分布式事务】

;
但是对于高并发系统,我们使用的是最终一致性,所以分布式事务的最终解决方案就是 RabbitMQ:首先发送一个消息:【可靠消息的保证:发送端和消费端的两端确认机制】,将消息发送出去后,最终的系统只需要监听这些消息,完了之后根据消息改变我们系统的数据;【不能达到强一致,达到的是柔性事务的最终一致:最终看到的数据是我们想要的就行。】

04 性能与压力测试

对于高并发系统,使用Jmeter进行压力测试;在压测期间,还可以监控JVM的性能,【

使用jvisualvm控制台,或者Jconsole

】。

05 缓存和分布式锁
特别在分布式系统中,我们要保证接口的吞吐量,性能的提升【缓存是必须的】,所以在高并发系统中,缓存是一个很重要的提升性能的手段,但是使用缓存期间,也有一些问题:

缓存的击穿【缓存的单点问题】,缓存雪崩【缓存的大面积问题】,缓存穿透【一直从数据库查询null值】:解决方案:使用分布式

当大并发量全部要来查询数据库,大家先来查询缓存,使用分布式锁来锁住,只有一个请求进来,查询不到缓存再来查询数据库,最终的效果就是只会给数据库放一条查询,而其他人得到锁之后,下次只会来查询缓存,也就不用再去查询数据库了,【分布式锁除了缓存问题使用外,接口幂等性的保证也可以使用】
比如现在有 几个并发的请求,都来调用了一个接口,这个接口只能调用一次,这一次数据是怎么样,那就是怎么样:我们释放锁之后,如果再来调用,就判断一下数据的状态,如果已经被处理了,那就不再处理。
定时任务中也有使用:比如定时启动了3台机器,这3台机器定时同时启动了一个任务,比如都来上架我们的秒杀商品,只要我们使用了分布式锁,我们上架过了,就不需要再次 上架;分布式锁可以来锁住我们所有的机器,让
一个机器来执行这个事情即可。

06 ElasticSearch

我们商品的检索肯定不能放在mysql中进行,通过写一些SQL执行,这样性能会大幅度下降;我们的检索功能:无论是分类检索,还是按照名字检索;所有的检索数据都保存在ElasticSearch中【一个非常专业的检索引擎】,注意它的安装使用都需要非常清楚。

07 异步和线程池

在高并发系统中,异步是非常必须的:我们复习了以前那种简单的

new Thread start

这种简单的异步,如果在高并发系统中,我们每一个请求进来都

new Thread start

,这样资源将很快耗尽;所以为了控制住系统的资源,我们使用线程池:以后所有的异步任务都要提交给线程池;这样的话,线程池中就有一个最大量,比如核心是20个线程,最大是200个,排队可以排500个、800个、乃至更多,我们通过线程池控制住资源,峰值也就是200个正在运行的,以及800个正在排队的,再也不可能占用更多的资源。这样有了资源的统一管控后,我们就不用害怕因为系统的某些资源耗尽导致系统崩溃
我们使用异步,将任务提交给线程池,但是可能异步任务中有顺序,我们可以使用异步编排CompetableFuture

08 单点登录和社交登录

我们着重演示了使用微博进行社交登录,此外还演示了单点登录,比如我们在不同域名下如何登录。但是我们后来是相同域名的,所以我们暂时可以不用使用单点登录。
但是在相同域名下,登录一处,在处处都可以实现单点登录效果,这时我们就整合了 spring session:一处登录,处处可用。【将所有微服务的session进行了同步,只要登录成功之后,去任何微服务,都可以获取到session】—> 使用spring session 解决了分布式系统session不一致问题。

09 商城业务

特别是购物车、订单、秒杀等,所有的业务都做了实现,但是一些细节需要我们自己填充。核心业务:商品上架【后台管理系统】、商品检索、商品详情、购物车、订单、秒杀。其中在商品详情里面,使用最多的是缓存技术:除了整合redis,还整合了

springCache

整合springCache来方便的使用缓存,以后我们的全系统都应该使用这种方式来进行缓存。

缓存出现的不一致问题该如何解决

缓存的清空以及缓存的更新,使用spring cache 都可以很方便的解决这些问题。

10 RabbitMQ

做分布式事务,来实现最终一致性的时候,rabbitmq是一个非常必要的工具。
我们只需要给A服务给B服务发送一个消息,A服务不用关心怎么做。【在订单系统中,我们使用rabbitmq来完成分布式事务】;【在秒杀系统中,加入rabbitmq队列来进行削峰处理:将所有的流量排队放到队列中,由后台慢慢的进行消费】。此外,通过rabbitmq,A服务和B服务不用关心接口调用了,A都不需要调用B,相当于我们将应用之间进行了解耦。
在订单的关单中关于rabbitmq的使用:我们通过死信队列,保证关单的数据都能被关掉。

11 支付

我们整合了支付宝的沙箱来进行支付。

12 定时任务与分布式调度

我们秒杀系统的所有上架,都需要定时任务来做

13 ShardingSphere

对于 13 分库分表 ShardingSphere在高可用做mysql集群时在使用。

14 SpringCloud组件

SpringCloud中的组件都需要进行掌握:除了nacos作为注册中心我们在一直使用外,配置中心只进行了演示;以后我们需要将所有微服务的配置都放给配置中心,【通过nacos的控制台也可以修改配置,修改完了之后微服务可以在不下线的情况下,应用到最新的配置】
最后,我们为了保护整个系统,引入了

sentinel

【作为一个流量哨兵】:从流量入口开始,保护我们任何想要保护的资源;当然,我们最好结合

Sleuth + Zipkin进行服务链路追踪

,这样可以看到我们整个系统的运行状况,链路追踪。可以找出一些异常问题,通过sentinel流量哨兵进行服务的降级,保护整个系统。

技术中最重要的是使用缓存来加快速度。异步也是来加快速度和控制资源。此外通过消息队列,无论是流量削峰,还是分布式做最终一致也好,我们都可以通过消息队列来减轻某一个服务的压力【所有消息存入队列中,闲的时候再来慢慢消费】。

[外链图片转存中…(img-AMRqMfb3-1672324216619)]

在高级篇构建一个高并发系统,除了

引入springcloud组件或者是SpringCloud Alibaba来作为周边设施

外,高并发有三宝:
缓存、异步、队排好

缓存就是使用redis作为缓存,保证它的缓存一致性,保证缓存的击穿、穿透等这些问题不会出现;

异步结合线程池,不能只是new Tread start;结合线程池以及异步编排来作为整个异步功能。

队排好:使用rabbitmq将所有的高峰流量,或者要做分布式事务的这些消息,全都放进rabbitmq消息队列中,让他们排好队,后台服务一个个处理。

加上这3个手段,构建高并发系统并不难。


本文转载自: https://blog.csdn.net/weixin_70577348/article/details/128488946
版权归原作者 Golden State Warrior 所有, 如有侵权,请联系我们删除。

“谷粒商城之高级篇(3)”的评论:

还没有评论