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

尚品汇总结十:秒杀模块(面试专用)

1、需求分析

所谓“秒杀”,就是商家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。

秒杀商品通常有三种限制:库存限制、时间限制、购买量限制

  并发问题的解决!

1)库存限制:商家只拿出限量的商品来秒杀。比如某商品实际库存是200件,但只拿出50件来参与秒杀。我们可以将其称为“秒杀库存”。

2)时间限制:通常秒杀都是有特定的时间段,只能在设定时间段进行秒杀活动;

3)购买量限制:同一个商品只允许用户最多购买几件。比如某手机限购1件。张某第一次买个1件,那么在该次秒杀活动中就不能再次抢购。

需求:

  1. 商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
  2. 运营商审核秒杀申请
  3. 秒杀频道首页列出当天的秒杀商品,点击秒杀商品图片跳转到秒杀商品详细页。
  4. 商品详细页显示秒杀商品信息,点击立即抢购进入秒杀,抢购成功时预减库存。当库存为0或不在活动期范围内时无法秒杀。
  5. 秒杀成功,进入下单页填写收货地址、电话、收件人等信息,完成下订单,然后跳转到支付页面,支付成功,跳转到成功页,完成秒杀。
  6. 当用户秒杀下单30分钟内未支付,取消订单,调用支付宝的关闭订单接口。

2、秒杀功能分析

列表页

详情页

排队页

下单页

支付页

3、数据库表

秒杀商品表seckill_goods

4、秒杀实现思路

  1. 秒杀的商品要提前放入到redis中(缓存预热),什么时间放入?凌晨放入当天的秒杀商品数据。
  2. 状态位控制访问请求,何为状态位?就是我们在内存中保存一个状态,当抢购开始时状态为1,可以抢购,当库存为0时,状态位0,不能抢购;状态位的好处,他是在内存中判断,压力很小,可以阻止很多不必要的请求
  3. 用户提交秒杀请求,将秒杀商品与用户id关联发送给mq,然后返回,秒杀页面通过轮询接口查看是否秒杀成功
  4. 我们秒杀只是为了获取一个秒杀资格,获取秒杀资格就可以到下单页下订单,后续业务与正常订单一样
  5. 下单我们需要注意的问题:

状态位如何同步到集群中的其他节点?

如何控制一个用户只下一个订单?

如何控制库存超卖?

如何控制访问压力?

业务流程图:

  • 秒杀商品导入缓存

缓存数据实现思路:前面的业务中我们把定时任务写在了service-task模块中,为了统一管理我们的定时任务,在秒杀业务中也是一样,为了减少service-task模块的耦合度,我们可以在定时任务模块只发送mq消息,需要执行定时任务的模块监听该消息即可,这样有利于我们后期动态控制,

例如:每天凌晨一点我们发送定时任务信息到mq交换机,如果秒杀业务凌晨一点需要导入数据到缓存,那么秒杀业务绑定队列到交换机就可以了,其他业务也是一样,这样就做到了很好的扩展。

上面提到我们要控制库存数量,不能超卖,那么如何控制呢?在这里我们提供一种解决方案,那就我们在导入商品缓存数据时,同时将商品库存信息导入队列,利用redis队列的原子性,保证库存不超卖

库存加入队列实施方案

  1. 如果秒杀商品有N个库存,那么我就循环往队列放入N个队列数据
  2. 秒杀开始时,用户进入,然后就从队列里面出队,只要队列里面有数据,说明就有库存(redis队列保证了原子性),队列为空了说明商品售罄

1、编写定时任务

在service-task模块发送消息

编写定时任务

http://cron.ciding.cc/

/*** 每天凌晨1点执行*///@Scheduled(cron = "0/30 * * * * ?")@Scheduled(cron = "0 0 1 * * ?")public void task1() {rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_1, "");}

2、监听定时任务信息

在service-activity模块绑定与监听消息,处理缓存逻辑,更新状态位

2.1、数据导入缓存

监听消息

package com.atguigu.gmall.activity.receiver;@Componentpublic class SeckillReceiver {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_TASK_1),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),key = {MqConst.ROUTING_TASK_1}))public void importItemToRedis(Message message, Channel channel) throws IOException {QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>();// 查询审核状态1 并且库存数量大于0,当天的商品queryWrapper.eq("status",1).gt("stock_count",0);queryWrapper.eq("DATE_FORMAT(start_time,'%Y-%m-%d')", DateUtil.formatDate(new Date()));List<SeckillGoods> list = seckillGoodsMapper.selectList(queryWrapper);// 将集合数据放入缓存中if (list!=null && list.size()>0){for (SeckillGoods seckillGoods : list) {// 使用hash 数据类型保存商品// key = seckill:goods field = skuId// 判断缓存中是否有当前keyBoolean flag = redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).hasKey(seckillGoods.getSkuId().toString());if (flag){// 当前商品已经在缓存中有了! 所以不需要在放入缓存!continue;}// 商品id为field ,对象为value 放入缓存  key = seckill:goods field = skuId value=商品字符串           redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(), seckillGoods);// hset(seckill:goods,1,{" skuNum 10"})// hset(seckill:goods,2,{" skuNum 10"})//根据每一个商品的数量把商品按队列的形式放进redis中for (Integer i = 0; i < seckillGoods.getStockCount(); i++) {// key = seckill:stock:skuId// lpush key valueredisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX+seckillGoods.getSkuId()).leftPush(seckillGoods.getSkuId().toString());}}// 手动确认接收消息成功channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}}}

2.2、更新状态位

由于我们的秒杀服务是要集群部署service-activity的,我们面临一个问题?RabbitMQ 如何实现对同一个应用的多个节点进行广播呢?

RabbitMQ 只能对绑定到交换机上面的不同队列实现广播,对于同一队列的消费者他们存在竞争关系,同一个消息只会被同一个队列下其中一个消费者接收,达不到广播效果;

我们目前的需求是定时任务发送消息,我们将秒杀商品导入缓存,同事更新集群的状态位,既然RabbitMQ 达不到广播的效果,我们就放弃吗?当然不是,我们想到一种解决方案,通过redis的发布订阅模式来通知其他兄弟节点,这不问题就解决了吗?

过程大致如下

    应用启动,多个节点监听同一个队列(此时多个节点是竞争关系,一条消息只会发到其中一个节点上)

    消息生产者发送消息,同一条消息只被其中一个节点收到

收到消息的节点通过redis的发布订阅模式来通知其他兄弟节点

接下来配置redis发布与订阅

2.2.1、redis发布与订阅实现

package com.atguigu.gmall.activity.redis;@Configurationpublic class RedisChannelConfig {/*docker exec -it  bc92 redis-clisubscribe seckillpush // 订阅 接收消息publish seckillpush admin // 发布消息*//*** 注入订阅主题* @param connectionFactory redis 链接工厂* @param listenerAdapter 消息监听适配器* @return 订阅主题对象*/@BeanRedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,MessageListenerAdapter listenerAdapter) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);//订阅主题container.addMessageListener(listenerAdapter, new PatternTopic("seckillpush"));//这个container 可以添加多个 messageListenerreturn container;}/*** 返回消息监听器* @param receiver 创建接收消息对象* @return*/@BeanMessageListenerAdapter listenerAdapter(MessageReceive receiver) {//这个地方 是给 messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”//也有好几个重载方法,这边默认调用处理器的方法 叫handleMessage 可以自己到源码里面看return new MessageListenerAdapter(receiver, "receiveMessage");}@Bean //注入操作数据的templateStringRedisTemplate template(RedisConnectionFactory connectionFactory) {return new StringRedisTemplate(connectionFactory);}}
 
 
package com.atguigu.gmall.activity.redis;@Componentpublic class MessageReceive {/**接收消息的方法*/public void receiveMessage(String message){System.out.println("----------收到消息了message:"+message);if(!StringUtils.isEmpty(message)) {/*消息格式skuId:0 表示没有商品skuId:1 表示有商品*/// 因为传递过来的数据为 “”6:1””
message = message.replaceAll("\"","");String[] split = StringUtils.split(message, ":");if (split == null || split.length == 2) {CacheHelper.put(split[0], split[1]);}}}}

 

CacheHelper类本地缓存类

package com.atguigu.gmall.activity.util;/*** 系统缓存类*/public class CacheHelper {/*** 缓存容器*/private final static Map<String, Object> cacheMap = new ConcurrentHashMap<String, Object>();/*** 加入缓存** @param key* @param cacheObject*/public static void put(String key, Object cacheObject) {cacheMap.put(key, cacheObject);}/*** 获取缓存** @param key* @return*/public static Object get(String key) {return cacheMap.get(key);}/*** 清除缓存** @param key* @return*/public static void remove(String key) {cacheMap.remove(key);}public static synchronized void removeAll() {cacheMap.clear();}}

说明:

  1. RedisChannelConfig 类配置redis监听的主题和消息处理器
  2. MessageReceive 类为消息处理器,消息message为:商品id与状态位,如:1:1 表示商品id1,状态位为1

2.2.2、redis发布消息

监听已经配置好,接下来我就发布消息,更改秒杀监听器{ SeckillReceiver },如下

完整代码如下

@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_TASK_1, durable = "true"),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK, type = ExchangeTypes.DIRECT, durable = "true"),key = {MqConst.ROUTING_TASK_1}))public void importItemToRedis(Message message, Channel channel) throws IOException {//Log.info("importItemToRedis:");QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>();queryWrapper.eq("status", 1);queryWrapper.gt("stock_count", 0);//当天的秒杀商品导入缓存queryWrapper.eq("DATE_FORMAT(start_time,'%Y-%m-%d')", DateUtil.formatDate(new Date()));List<SeckillGoods> list = seckillGoodsMapper.selectList(queryWrapper);//把数据放在redis中for (SeckillGoods seckillGoods : list) {if (redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).hasKey(seckillGoods.getSkuId().toString()))continue;redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(), seckillGoods);//根据每一个商品的数量把商品按队列的形式放进redis中for (int i = 0; i < seckillGoods.getStockCount(); i++) {redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + seckillGoods.getSkuId()).leftPush(seckillGoods.getSkuId().toString());}//通知添加与更新状态位,更新为开启redisTemplate.convertAndSend("seckillpush", seckillGoods.getSkuId()+":1");}channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}

说明:到目前我们就实现了商品信息导入缓存,同时更新状态位的工作

  • 秒杀列表与详情

1、封装秒杀列表与详情接口

实现类

 
package com.atguigu.gmall.activity.service.impl;/*** 服务实现层** @author Administrator*/@Service@Transactionalpublic class SeckillGoodsServiceImpl implements SeckillGoodsService {@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;@Autowiredprivate RedisTemplate redisTemplate;/*** 查询全部*/@Overridepublic List<SeckillGoods> findAll() {List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).values();return seckillGoodsList;}/*** 根据ID获取实体** @param id* @return*/@Overridepublic SeckillGoods getSeckillGoods(Long id) {return (SeckillGoods) redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(id.toString());}
}

SeckillGoodsControllerpackage com.atguigu.gmall.activity.controller;/*** controller**/@RestController@RequestMapping("/api/activity/seckill")public class SeckillGoodsController {@Autowiredprivate SeckillGoodsService seckillGoodsService;@Autowiredprivate UserFeignClient userFeignClient;@Autowiredprivate ProductFeignClient productFeignClient;/*** 返回全部列表** @return*/@GetMapping("/findAll")public Result findAll() {return Result.ok(seckillGoodsService.findAll());}/*** 获取实体** @param skuId* @return*/@GetMapping("/getSeckillGoods/{skuId}")public Result getSeckillGoods(@PathVariable("skuId") Long skuId) {return Result.ok(seckillGoodsService.getSeckillGoods(skuId));}
}

2、页面渲染

2.1、列表页

在 web-all 项目中添加控制器

 
package com.atguigu.gmall.item.controller;/*** 秒杀**/@Controllerpublic class SeckilController {@Autowiredprivate ActivityFeignClient activityFeignClient;/*** 秒杀列表* @param model* @return*/@GetMapping("seckill.html")public String index(Model model) {Result result = activityFeignClient.findAll();model.addAttribute("list", result.getData());return "seckill/index";}
}

列表

页面资源: \templates\seckill\index.html

<div class="goods-list" id="item"><ul class="seckill" id="seckill"><li class="seckill-item" th:each="item: ${list}"><div class="pic" th:@click="|detail(${item.skuId})|"><img th:src="${item.skuDefaultImg}" alt=''></div><div class="intro"><span th:text="${item.skuName}">手机</span></div><div class='price'><b class='sec-price' th:text="'¥'+${item.costPrice}">¥0</b><b class='ever-price' th:text="'¥'+${item.price}">¥0</b></div><div class='num'><div th:text="'已售'+${item.num}">已售1</div><div class='progress'><div class='sui-progress progress-danger'><span style='width: 70%;' class='bar'></span></div></div><div>剩余<b class='owned' th:text="${item.stockCount}">0</b>件</div></div><a class='sui-btn btn-block btn-buy' th:href="'/seckill/'+${item.skuId}+'.html'" target='_blank'>立即抢购</a></li></ul></div>

2.2、详情页

说明:

  1. 为了减轻访问压力,秒杀详情我们可以生成静态页面
  2. 立即购买,该按钮我们要加以控制,该按钮就是一个链接,页面只是控制能不能点击,一般用户可以绕过去,直接点击秒杀下单,所以我们要加以控制,在秒杀没有开始前,不能进入秒杀页面

2.2.1、详情页

SeckilController添加方法
/*** 秒杀详情* @param skuId* @param model* @return*/@GetMapping("seckill/{skuId}.html")public String getItem(@PathVariable Long skuId, Model model){// 通过skuId 查询skuInfoResult result = activityFeignClient.getSeckillGoods(skuId);model.addAttribute("item", result.getData());return "seckill/item";}
 
 

详情页面

页面资源: \templates\seckill\item.html

基本信息渲染

<div class="product-info"><div class="fl preview-wrap"><!--放大镜效果--><div class="zoom"><!--默认第一个预览--><div id="preview" class="spec-preview"><span class="jqzoom"><img th:jqimg="${item.skuDefaultImg}" th:src="${item.skuDefaultImg}" width="400" height="400"/></span></div></div></div><div class="fr itemInfo-wrap"><div class="sku-name"><h4 th:text="${item.skuName}">三星</h4></div><div class="news"><span><img src="/img/_/clock.png"/>品优秒杀</span><span class="overtime">{{timeTitle}}:{{timeString}}</span></div><div class="summary"><div class="summary-wrap"><div class="fl title"><i>秒杀价</i></div><div class="fl price"><i>¥</i><em th:text="${item.costPrice}">0</em><span th:text="'原价:'+${item.price}">原价:0</span></div><div class="fr remark">剩余库存:<span th:text="${item.stockCount}">0</span></div></div><div class="summary-wrap"><div class="fl title"><i>促  销</i></div><div class="fl fix-width"><i class="red-bg">加价购</i><em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em></div></div></div><div class="support"><div class="summary-wrap"><div class="fl title"><i>支  持</i></div><div class="fl fix-width"><em class="t-gray">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</em></div></div><div class="summary-wrap"><div class="fl title"><i>配 送 至</i></div><div class="fl fix-width"><em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em></div></div></div><div class="clearfix choose"><div class="summary-wrap"><div class="fl title"></div><div class="fl"><ul class="btn-choose unstyled"><li><a href="javascript:" v-if="isBuy" @click="queue()" class="sui-btn  btn-danger addshopcar">立即抢购</a><a href="javascript:" v-if="!isBuy" class="sui-btn  btn-danger addshopcar" disabled="disabled">立即抢购</a></li></ul></div></div></div></div></div>

倒计时处理

思路:页面初始化时,拿到商品秒杀开始时间和结束时间等信息,实现距离开始时间和活动倒计时。

活动未开始时,显示距离开始时间倒计时;

活动开始后,显示活动结束时间倒计时。

倒计时代码片段

init() {// debugger// 计算出剩余时间var startTime = new Date(this.data.startTime).getTime();var endTime = new Date(this.data.endTime).getTime();var nowTime = new Date().getTime();var secondes = 0;// 还未开始抢购if(startTime > nowTime) {this.timeTitle = '距离开始'secondes = Math.floor((startTime - nowTime) / 1000);}if(nowTime > startTime && nowTime < endTime) {this.isBuy = truethis.timeTitle = '距离结束'secondes = Math.floor((endTime - nowTime) / 1000);}if(nowTime > endTime) {this.timeTitle = '抢购结束'secondes = 0;}const timer = setInterval(() => {secondes = secondes - 1this.timeString = this.convertTimeString(secondes)}, 1000);// 通过$once来监听定时器,在beforeDestroy可以被清除。this.$once('hook:beforeDestroy', () => {clearInterval(timer);})},

时间转换方法

convertTimeString(allseconds) {if(allseconds <= 0) return '00:00:00'// 计算天数var days = Math.floor(allseconds / (60 * 60 * 24));// 小时var hours = Math.floor((allseconds - (days * 60 * 60 * 24)) / (60 * 60));// 分钟var minutes = Math.floor((allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60)) / 60);// 秒var seconds = allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60) - (minutes * 60);//拼接时间var timString = "";if (days > 0) {timString = days + "天:";}return timString += hours + ":" + minutes + ":" + seconds;}

2.2.2、秒杀按钮控制

1,我们通过前面页面时间控制

2,通过服务器端控制,如何控制呢?

在进入秒杀功能前,我们加一个下单码,只有你获取到该下单码,才能够进入秒杀方法进行秒杀

获取秒杀码

SeckillGoodsController
/*** 获取下单码* @param skuId* @return*/@GetMapping("auth/getSeckillSkuIdStr/{skuId}")public Result getSeckillSkuIdStr(@PathVariable("skuId") Long skuId, HttpServletRequest request) {String userId = AuthContextHolder.getUserId(request);SeckillGoods seckillGoods = seckillGoodsService.getSeckillGoods(skuId);if (null != seckillGoods) {Date curTime = new Date();if (DateUtil.dateCompare(seckillGoods.getStartTime(), curTime) && DateUtil.dateCompare(curTime, seckillGoods.getEndTime())) {//可以动态生成,放在redis缓存String skuIdStr = MD5.encrypt(userId);return Result.ok(skuIdStr);}}return Result.fail().message("获取下单码失败");}

说明:只有在商品秒杀时间范围内,才能获取下单码,这样我们就有效控制了用户非法秒杀,下单码我们可以根据业务自定义规则,目前我们定义为当前用户id MD5加密。

前端页面

页面获取下单码,进入秒杀场景

queue() {debuggerseckill.getSeckillSkuIdStr(this.skuId).then(response => {var skuIdStr = response.data.datawindow.location.href = '/seckill/queue.html?skuId='+this.skuId+'&skuIdStr='+skuIdStr})},

前端js完整代码如下

<script src="/js/api/seckill.js"></script><script th:inline="javascript">var item = new Vue({el: '#item',data: {skuId: [[${item.skuId}]],data: [[${item}]],timeTitle: '距离开始',timeString: '00:00:00',isBuy: false},created() {this.init()},methods: {init() {// debugger// 计算出剩余时间var startTime = new Date(this.data.startTime).getTime();var endTime = new Date(this.data.endTime).getTime();var nowTime = new Date().getTime();var secondes = 0;// 还未开始抢购if(startTime > nowTime) {this.timeTitle = '距离开始'secondes = Math.floor((startTime - nowTime) / 1000);}if(nowTime > startTime && nowTime < endTime) {this.isBuy = truethis.timeTitle = '距离结束'secondes = Math.floor((endTime - nowTime) / 1000);}if(nowTime > endTime) {this.timeTitle = '抢购结束'secondes = 0;}const timer = setInterval(() => {secondes = secondes - 1this.timeString = this.convertTimeString(secondes)}, 1000);// 通过$once来监听定时器,在beforeDestroy钩子可以被清除。this.$once('hook:beforeDestroy', () => {clearInterval(timer);})},queue() {debuggerseckill.getSeckillSkuIdStr(this.skuId).then(response => {var skuIdStr = response.data.datawindow.location.href = '/seckill/queue.html?skuId='+this.skuId+'&skuIdStr='+skuIdStr})},convertTimeString(allseconds) {if(allseconds <= 0) return '00:00:00'// 计算天数var days = Math.floor(allseconds / (60 * 60 * 24));// 小时var hours = Math.floor((allseconds - (days * 60 * 60 * 24)) / (60 * 60));// 分钟var minutes = Math.floor((allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60)) / 60);// 秒var seconds = allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60) - (minutes * 60);//拼接时间var timString = "";if (days > 0) {timString = days + "天:";}return timString += hours + ":" + minutes + ":" + seconds;}}})</script>

3.3、进入秒杀

SeckilController
/*** 秒杀排队* @param skuId* @param skuIdStr* @param request* @return*/@GetMapping("seckill/queue.html")public String queue(@RequestParam(name = "skuId") Long skuId,@RequestParam(name = "skuIdStr") String skuIdStr,HttpServletRequest request){request.setAttribute("skuId", skuId);request.setAttribute("skuIdStr", skuIdStr);return "seckill/queue";}

页面

页面资源: \templates\seckill\queue.html

<div class="cart py-container" id="item"><div class="seckill_dev" v-if="show == 1">排队中...</div><div class="seckill_dev" v-if="show == 2">{{message}}</div><div class="seckill_dev" v-if="show == 3">抢购成功&nbsp;&nbsp;<a href="/seckill/trade.html" target="_blank">去下单</a></div><div class="seckill_dev" v-if="show == 4">抢购成功&nbsp;&nbsp;<a href="/myOrder.html" target="_blank">我的订单</a></div></div>

Js部分

<script src="/js/api/seckill.js"></script><script th:inline="javascript">var item = new Vue({el: '#item',data: {skuId: [[${skuId}]],skuIdStr: [[${skuIdStr}]],data: {},show: 1,code: 211,message: '',isCheckOrder: false},mounted() {const timer = setInterval(() => {if(this.code != 211) {clearInterval(timer);}this.checkOrder()}, 3000);// 通过$once来监听定时器,在beforeDestroy钩子可以被清除。this.$once('hook:beforeDestroy', () => {clearInterval(timer);})},created() {this.saveOrder();},methods: {saveOrder() {seckill.seckillOrder(this.skuId, this.skuIdStr).then(response => {debuggerconsole.log(JSON.stringify(response))if(response.data.code == 200) {this.isCheckOrder = true} else {this.show = 2this.message = response.data.message}})},checkOrder() {if(!this.isCheckOrder) returnseckill.checkOrder(this.skuId).then(response => {debuggerthis.data = response.data.datathis.code = response.data.codeconsole.log(JSON.stringify(this.data))//排队中if(response.data.code == 211) {this.show = 1} else {//秒杀成功if(response.data.code == 215) {this.show = 3this.message = response.data.message} else {if(response.data.code == 218) {this.show = 4this.message = response.data.message} else {this.show = 2this.message = response.data.message}}}})}}})</script>

说明:该页面直接通过controller返回页面,进入页面后显示排队中,然后通过异步执行秒杀下单,提交成功,页面通过轮询后台方法查询秒杀状态

秒杀业务

  秒杀的主要目的就是获取一个下单资格,拥有下单资格就可以去下单支付,获取下单资格后的流程就与正常下单流程一样,只是没有购物车这一步,总结起来就是,秒杀根据库存获取下单资格,拥有下单资格进入下单页面(选择地址,支付方式,提交订单,然后支付订单)

步骤:

  1. 校验下单码,只有正确获得下单码的请求才是合法请求
  2. 校验状态位state

State为null,说明请求非法;

State0说明已经售罄;

State为1,说明可以抢购

状态位的好处,他是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力

  1. 前面条件都成立,将秒杀用户加入队列,然后直接返回
  2. 前端轮询秒杀状态,查询秒杀结果

1、秒杀下单

SeckillGoodsController添加方法
/*** 根据用户和商品ID实现秒杀下单** @param skuId* @return*/@PostMapping("auth/seckillOrder/{skuId}")public Result seckillOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) throws Exception {//校验下单码(抢购码规则可以自定义)String userId = AuthContextHolder.getUserId(request);String skuIdStr = request.getParameter("skuIdStr");if (!skuIdStr.equals(MD5.encrypt(userId))) {//请求不合法return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}//产品标识, 1:可以秒杀    0:秒杀结束String state = (String) CacheHelper.get(skuId.toString());if (StringUtils.isEmpty(state)) {//请求不合法return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}if ("1".equals(state)) {//用户记录UserRecode userRecode = new UserRecode();userRecode.setUserId(userId);userRecode.setSkuId(skuId);rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);} else {//已售罄return Result.build(null, ResultCodeEnum.SECKILL_FINISH);}return Result.ok();}

2、秒杀下单监听

思路:

  1. 首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了;
  2. 判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,怎么控制呢?很简单,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false,直接返回,过期时间可以根据业务自定义,这样用户这一段就控制住了
  3. 获取队列中的商品,如果能够获取,则商品有库存,可以下单,如果获取的商品id为空,则商品售罄,商品售罄我们要第一时间通知兄弟节点,更新状态位,所以在这里发送redis广播
  4. 将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功
SeckillReceiver类添加监听方法

@Autowired
private SeckillGoodsService seckillGoodsService;/*** 秒杀用户加入队列** @param message* @param channel* @throws IOException*/@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_SECKILL_USER, durable = "true"),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_SECKILL_USER, type = ExchangeTypes.DIRECT, durable = "true"),key = {MqConst.ROUTING_SECKILL_USER}))public void seckill(UserRecode userRecode, Message message, Channel channel) throws IOException {if (null != userRecode) {//Log.info("paySuccess:"+ JSONObject.toJSONString(userRecode));//预下单seckillGoodsService.seckillOrder(userRecode.getSkuId(), userRecode.getUserId());//确认收到消息channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}}
 

预下单接口

实现类/**** 创建订单* @param skuId* @param userId*/@Overridepublic void seckillOrder(Long skuId, String userId) {//产品状态位, 1:可以秒杀 0:秒杀结束String state = (String) CacheHelper.get(skuId.toString());if("0".equals(state)) {//已售罄return;}//判断用户是否下过单boolean isExist = redisTemplate.opsForValue().setIfAbsent(RedisConst.SECKILL_USER + userId, skuId, RedisConst.SECKILL__TIMEOUT, TimeUnit.SECONDS);if (!isExist) {return;}//获取队列中的商品,如果能够获取,则商品存在,可以下单String goodsId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();if (StringUtils.isEmpty(goodsId)) {//商品售罄,更新状态位redisTemplate.convertAndSend("seckillpush", skuId+":0");//已售罄return;}//订单记录OrderRecode orderRecode = new OrderRecode();orderRecode.setUserId(userId);orderRecode.setSeckillGoods(this.getSeckillGoods(skuId));orderRecode.setNum(1);//生成下单码orderRecode.setOrderStr(MD5.encrypt(userId+skuId));//订单数据存入ReidsredisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).put(orderRecode.getUserId(), orderRecode);
//更新库存this.updateStockCount(orderRecode.getSeckillGoods().getSkuId());}
 

package com.atguigu.gmall.model.activity;@Datapublic class OrderRecode implements Serializable {private static final long serialVersionUID = 1L;private String userId;private SeckillGoods seckillGoods;private Integer num;private String orderStr;}

/*** 更新库存* @param skuId*/private void updateStockCount(Long skuId) {//更新库存,批量更新,用于页面显示,以实际扣减库存为准Long stockCount = redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();if (stockCount % 2 == 0) {//商品卖完,同步数据库SeckillGoods seckillGoods = this.getSeckillGoods(skuId);seckillGoods.setStockCount(stockCount.intValue());seckillGoodsMapper.updateById(seckillGoods);//更新缓存        redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(), seckillGoods);}}

3、页面轮询接口

该接口判断用户秒杀状态

SeckillGoodsService接口
/**** 根据用户ID查看订单信息* @param userId* @return*/
@Override
public Result checkOrder(Long skuId, String userId) {// 用户在缓存中存在,有机会秒杀到商品boolean isExist =redisTemplate.hasKey(RedisConst.SECKILL_USER + userId);if (isExist) {//判断用户是否正在排队//判断用户是否下单boolean isHasKey = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).hasKey(userId);if (isHasKey) {//抢单成功OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);// 秒杀成功!return Result.build(orderRecode, ResultCodeEnum.SECKILL_SUCCESS);}}//判断是否下单boolean isExistOrder = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).hasKey(userId);if(isExistOrder) {String orderId = (String)redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).get(userId);return Result.build(orderId, ResultCodeEnum.SECKILL_ORDER_SUCCESS);}String state = (String) CacheHelper.get(skuId.toString());if("0".equals(state)) {//已售罄 抢单失败return Result.build(null, ResultCodeEnum.SECKILL_FAIL);}//正在排队中return Result.build(null, ResultCodeEnum.SECKILL_RUN);
}

SeckillGoodsController
/*** 查询秒杀状态* @return*/@GetMapping(value = "auth/checkOrder/{skuId}")public Result checkOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) {//当前登录用户String userId = AuthContextHolder.getUserId(request);return seckillGoodsService.checkOrder(skuId, userId);}

4、轮询排队页面

该页面有四种状态:

  1. 排队中
  2. 各种提示(非法、已售罄等)
  3. 抢购成功,去下单
  4. 抢购成功,已下单,显示我的订单

抢购成功,页面显示去下单,跳转下单确认页面

<div class="seckill_dev" v-if="show == 3">抢购成功&nbsp;&nbsp;<a href="/seckill/trade.html" target="_blank">去下单</a></div>

5、下单页面

 

我们已经把下单信息记录到redis缓存中,所以接下来我们要组装下单页数据

5.1、下单页数据接口封装

Service-activity模块

SeckillGoodsController
  @Autowiredprivate RedisTemplate redisTemplate;
/*** 秒杀确认订单* @param request* @return*/@GetMapping("auth/trade")public Result trade(HttpServletRequest request) {// 获取到用户IdString userId = AuthContextHolder.getUserId(request);// 先得到用户想要购买的商品!OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);if (null == orderRecode) {return Result.fail().message("非法操作");}SeckillGoods seckillGoods = orderRecode.getSeckillGoods();//获取用户地址List<UserAddress> userAddressList = userFeignClient.findUserAddressListByUserId(userId);// 声明一个集合来存储订单明细ArrayList<OrderDetail> detailArrayList = new ArrayList<>();OrderDetail orderDetail = new OrderDetail();orderDetail.setSkuId(seckillGoods.getSkuId());orderDetail.setSkuName(seckillGoods.getSkuName());orderDetail.setImgUrl(seckillGoods.getSkuDefaultImg());orderDetail.setSkuNum(orderRecode.getNum());orderDetail.setOrderPrice(seckillGoods.getCostPrice());// 添加到集合detailArrayList.add(orderDetail);// 计算总金额OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderDetailList(detailArrayList);orderInfo.sumTotalAmount();Map<String, Object> result = new HashMap<>();result.put("userAddressList", userAddressList);result.put("detailArrayList", detailArrayList);// 保存总金额result.put("totalAmount", orderInfo.getTotalAmount());return Result.ok(result);}

5.2、web-all调用接口

SeckilController
/*** 确认订单* @param model* @return*/@GetMapping("seckill/trade.html")public String trade(Model model) {Result<Map<String, Object>> result = activityFeignClient.trade();if(result.isOk()) {model.addAllAttributes(result.getData());return "seckill/trade";} else {model.addAttribute("message",result.getMessage());return "seckill/fail";}}

页面资源: \templates\seckill\trade.html;\templates\seckill\fail.html

5.2、下单确认页面

该页面与正常下单页面类似,只是下单提交接口不一样,因为秒杀下单不需要正常下单的各种判断,因此我们要在订单服务提供一个秒杀下单接口,直接下单

Service-order模块提供秒杀下单接口

OrderApiController
/*** 秒杀提交订单,秒杀订单不需要做前置判断,直接下单* @param orderInfo* @return*/@PostMapping("inner/seckill/submitOrder")public Long submitOrder(@RequestBody OrderInfo orderInfo) {Long orderId = orderService.saveOrderInfo(orderInfo);return orderId;}

Service-activity模块秒杀下单

SeckillGoodsController

@Autowired
private OrderFeignClient orderFeignClient;/*** 秒杀提交订单** @param orderInfo* @return*/@PostMapping("auth/submitOrder")public Result submitOrder(@RequestBody OrderInfo orderInfo, HttpServletRequest request) {String userId = AuthContextHolder.getUserId(request);OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);if (null == orderRecode) {return Result.fail().message("非法操作");}orderInfo.setUserId(Long.parseLong(userId));Long orderId = orderFeignClient.submitOrder(orderInfo);if (null == orderId) {return Result.fail().message("下单失败,请重新操作");}//删除下单信息redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).delete(userId);//下单记录redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).put(userId, orderId.toString());return Result.ok(orderId);}
 

说明:下单成功后,后续流程与正常订单一致

6、秒杀结束清空redis缓存

秒杀过程中我们写入了大量redis缓存,我们可以在秒杀结束或每天固定时间清楚缓存

,释放缓存空间;

实现思路:假如根据业务,我们确定每天18点所有秒杀业务结束,那么我们编写定时任务,每天18点发送mq消息,service-activity模块监听消息清理缓存

Service-task发送消息

6.1、编写定时任务发送消息

/*** 每天下午18点执行*///@Scheduled(cron = "0/35 * * * * ?")@Scheduled(cron = "0 0 18 * * ?")public void task18() {log.info("task18");rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_18, "");}

6.3、接收消息并处理

Service-activity接收消息

SeckillReceiver
/*** 秒杀结束清空缓存** @param message* @param channel* @throws IOException*/
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_TASK_18, durable = "true"),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK, type = ExchangeTypes.DIRECT, durable = "true"),key = {MqConst.ROUTING_TASK_18}
))
public void clearRedis(Message message, Channel channel) throws IOException {//活动结束清空缓存QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>();queryWrapper.eq("status", 1);queryWrapper.le("end_time", new Date());List<SeckillGoods> list = seckillGoodsMapper.selectList(queryWrapper);//清空缓存for (SeckillGoods seckillGoods : list) {redisTemplate.delete(RedisConst.SECKILL_STOCK_PREFIX + seckillGoods.getSkuId());}redisTemplate.delete(RedisConst.SECKILL_GOODS);redisTemplate.delete(RedisConst.SECKILL_ORDERS);redisTemplate.delete(RedisConst.SECKILL_ORDERS_USERS);//将状态更新为结束SeckillGoods seckillGoodsUp = new SeckillGoods();seckillGoodsUp.setStatus("2");seckillGoodsMapper.update(seckillGoodsUp, queryWrapper);// 手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

说明:清空redis缓存,同时更改秒杀商品活动结束

开发步骤:

      0.   做个定时任务将要秒杀的商品放入消息队列

      1.   消费消息队列先将秒杀商品放入缓存

            1.1  将秒杀商品信息放入缓存中hash 数据结构中

            1.2  放入一个list 数据来存储商品的数据量

            1.3  利用缓存的订阅与发布功能,来更新状态位

                       意义:看商品是否售罄!

      2.   页面显示秒杀商品以及商品详情

            2.1  通过缓存查询所有的秒杀商品

            2.2  通过商品Id 查询秒杀的商品详情

            2.3  秒杀详情中的秒杀按钮要设定一个下单码 {防止用户直接进入秒杀业务}

      3.   进入秒杀

            3.1  获取秒杀的下单码进行校验!

            3.2  判断状态位

            3.3  将用户下单请求放入到mq,是为了防止高并发

            3.4  消费下单的mq消息,再次验证状态位,用户是否已经下单,判断库存,保存预下单的用户Id以及商品Id,

                  并将真正下单数据放入缓存,并更新数据商品的库存数

      4.   检查抢购状态

            4.1  根据用在缓存中是否有key{用户key、用户key对应的商品key} ,以及状态位,是否已经下过订单

      5.   下订单

            5.1  直接从缓存中获取下单数据,并显示下单列表页面!

            5.2  提交订单

      6.   秒杀活动结束清空缓存数据    

            6.1  商品数据

            6.2  用户数据

            6.3  订单数据

相关文章:

尚品汇总结十:秒杀模块(面试专用)

1、需求分析 所谓“秒杀”&#xff0c;就是商家发布一些超低价格的商品&#xff0c;所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉&#xff0c;往往一上架就被抢购一空&#xff0c;有时只用一秒钟。 秒…...

什么是设计模式?

目录 概述: 什么是模式&#xff01;&#xff01; 为什么学习模式&#xff01;&#xff01; 模式和框架的比较&#xff1a; 设计模式研究的历史 关于pattern的历史 Gang of Four(GoF) 关于”Design”Pattern” 重提&#xff1a;指导模式设计的三个概念 1.重用(reuse)…...

Node.js |(三)Node.js API:path模块及Node.js 模块化 | 尚硅谷2023版Node.js零基础视频教程

学习视频&#xff1a;尚硅谷2023版Node.js零基础视频教程&#xff0c;nodejs新手到高手 文章目录 &#x1f4da;path模块&#x1f4da;Node.js模块化&#x1f407;介绍&#x1f407;模块暴露数据⭐️模块初体验⭐️暴露数据 &#x1f407;导入文件模块&#x1f407;导入文件夹的…...

Netty自定义编码解码器

上次通信的时候用的是自带的编解码器&#xff0c;今天自己实现一下自定义的。 1、自定义一下协议 //协议类 Data public class Protocol<T> implements Serializable {private Long id System.currentTimeMillis();private short msgType;// 假设1为请求 2为响应privat…...

HOperatorSet.OpenFramegrabber “GigEVision“

HOperatorSet.OpenFramegrabber "GigEVision"访问失败 直接跳出 但其他算子可以访问 重装halcon x86...

图的遍历DFSBFS-有向图无向图

西江月・证明 即得易见平凡&#xff0c;仿照上例显然。留作习题答案略&#xff0c;读者自证不难。 反之亦然同理&#xff0c;推论自然成立。略去过程Q.E.D.&#xff0c;由上可知证毕。 有向图的遍历可以使用深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08…...

【NLP】深入浅出全面回顾注意力机制

深入浅出全面回顾注意力机制 1. 注意力机制概述2. 举个例子&#xff1a;使用PyTorch带注意力机制的Encoder-Decoder模型3. Transformer架构回顾3.1 Transformer的顶层设计3.2 Encoder与Decoder的输入3.3 高并发长记忆的实现self-attention的矩阵计算形式多头注意力&#xff08;…...

Linux应用编程的read函数和Linux驱动编程的read函数的区别

Linux应用编程的read函数用于从文件描述符&#xff08;文件、管道、套接字等&#xff09;中读取数据。它的原型如下&#xff1a; ssize_t read(int fd, void *buf, size_t count);其中&#xff0c;fd参数是文件描述符&#xff0c;buf是用于存储读取数据的缓冲区&#xff0c;co…...

Kubernetes(K8s)从入门到精通系列之十:使用 kubeadm 创建一个高可用 etcd 集群

Kubernetes K8s从入门到精通系列之十&#xff1a;使用 kubeadm 创建一个高可用 etcd 集群 一、etcd高可用拓扑选项1.堆叠&#xff08;Stacked&#xff09;etcd 拓扑2.外部 etcd 拓扑 二、准备工作三、建立集群1.将 kubelet 配置为 etcd 的服务管理器。2.为 kubeadm 创建配置文件…...

使用动态规划实现错排问题-2023年全国青少年信息素养大赛Python复赛真题精选

[导读]&#xff1a;超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲&#xff0c;这是超平老师解读Python编程挑战赛真题系列的第15讲。 全国青少年信息素养大赛&#xff08;原全国青少年电子信息智能创新大赛&#xff09;是“世界机器人大会青少年机器人设…...

大规模向量检索库Faiss学习总结记录

因为最近要使用到faiss来做检索和查询&#xff0c;所以这里只好抽出点时间来学习下&#xff0c;本文主要是自己最近学习的记录&#xff0c;来源于网络资料查询总结&#xff0c;仅用作个人学习总结记录。 Faiss的全称是Facebook AI Similarity Search&#xff0c;是FaceBook的A…...

SpringCloudAlibaba之Sentinel(一)流控篇

前言&#xff1a; 为什么使用Sentinel&#xff0c;这是一个高可用组件&#xff0c;为了使我们的微服务高可用而生 我们的服务会因为什么被打垮&#xff1f; 一&#xff0c;流量激增 缓存未预热&#xff0c;线程池被占满 &#xff0c;无法响应 二&#xff0c;被其他服务拖…...

哪种模式ip更适合你的爬虫项目?

作为一名爬虫程序员&#xff0c;对于数据的采集和抓取有着浓厚的兴趣。当谈到爬虫ip时&#xff0c;你可能会听说过两种常见的爬虫ip类型&#xff1a;Socks5爬虫ip和HTTP爬虫ip。但到底哪一种在你的爬虫项目中更适合呢&#xff1f;本文将帮助你进行比较和选择。 首先&#xff0c…...

优维低代码实践:对接数据

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…...

docker 离线模式-部署容器

有网络的情况下下载需要的镜像 比如(下面以tomcat为例子&#xff0c;其他镜像类似) docker pull tomcat打包镜像文件到本地 docker save tomcat -o tomcat.tar将tomcat.tar 上传到内网服务器&#xff08;无外网环境&#xff09; 导入镜像 docker load -i tomcat.tar创建容器…...

MDN-HTTP

参考资料 文章目录 HTTP简介HTTP 和 HTTPSHTTP消息典型的HTTP会话HTTP响应状态HTTP安全HTTP CookieHTTP压缩 HTTP简介 HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于在计算机网络中传输超文本和其他资源的应用层协议。他是互联网的基础协议之一&#x…...

【数据库】PostgreSQL中使用`SELECT DISTINCT`和`SUBSTRING`函数实现去重查询

在PostgreSQL中&#xff0c;我们可以使用SELECT DISTINCT和SUBSTRING函数来实现对某个字段进行去重查询。本文将介绍如何使用这两个函数来实现对resource_version字段的去重查询。 1. SELECT DISTINCT语句 SELECT DISTINCT语句用于从表中选择不重复的记录。如果没有指定列名&…...

笔记本WIFI连接无网络【实测有效,不用重启电脑】

笔记本Wifi连接无网络实测有效解决方案 问题描述&#xff1a; 笔记本买来一段时间后&#xff0c;WIFI网络连接开机一段时间还正常连接&#xff0c;但是过一段时间显示网络连接不上&#xff0c;重启电脑太麻烦&#xff0c;选择编写重启网络脚本解决。三步解决问题。 解决方案&a…...

Java课题笔记~ Spring 概述

Spring 框架 一、Spring 概述 1、Spring 框架是什么 Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架&#xff0c;它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转&#xff08;IoC&#xff09;和面向切面编程&#xff08;AOP&#xff09;。 Spring…...

2022 robocom 世界机器人开发者大赛-本科组(国赛)

RC-u1 智能红绿灯 题目描述&#xff1a; RC-u1 智能红绿灯 为了最大化通行效率同时照顾老年人穿行马路&#xff0c;在某养老社区前&#xff0c;某科技公司设置了一个智能红绿灯。 这个红绿灯是这样设计的&#xff1a; 路的两旁设置了一个按钮&#xff0c;老年人希望通行马路时会…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

【解密LSTM、GRU如何解决传统RNN梯度消失问题】

解密LSTM与GRU&#xff1a;如何让RNN变得更聪明&#xff1f; 在深度学习的世界里&#xff0c;循环神经网络&#xff08;RNN&#xff09;以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而&#xff0c;传统RNN存在的一个严重问题——梯度消失&#…...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storms…...

tauri项目,如何在rust端读取电脑环境变量

如果想在前端通过调用来获取环境变量的值&#xff0c;可以通过标准的依赖&#xff1a; std::env::var(name).ok() 想在前端通过调用来获取&#xff0c;可以写一个command函数&#xff1a; #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...

Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用

Linux 内存管理调试分析&#xff1a;ftrace、perf、crash 的系统化使用 Linux 内核内存管理是构成整个内核性能和系统稳定性的基础&#xff0c;但这一子系统结构复杂&#xff0c;常常有设置失败、性能展示不良、OOM 杀进程等问题。要分析这些问题&#xff0c;需要一套工具化、…...

轻量安全的密码管理工具Vaultwarden

一、Vaultwarden概述 Vaultwarden主要作用是提供一个自托管的密码管理器服务。它是Bitwarden密码管理器的第三方轻量版&#xff0c;由国外开发者在Bitwarden的基础上&#xff0c;采用Rust语言重写而成。 &#xff08;一&#xff09;Vaultwarden镜像的作用及特点 轻量级与高性…...

Heygem50系显卡合成的视频声音杂音模糊解决方案

如果你在使用50系显卡有杂音的情况&#xff0c;可能还是官方适配问题&#xff0c;可以使用以下方案进行解决&#xff1a; 方案一&#xff1a;剪映替换音色&#xff08;简单适合普通玩家&#xff09; 使用剪映换音色即可&#xff0c;口型还是对上的&#xff0c;没有剪映vip的&…...