当前位置: 首页 > 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;老年人希望通行马路时会…...

【雕爷学编程】Arduino动手做(195)---HT16k33 矩阵 8*8点阵屏模块6

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…...

Typescript]基础篇之 tsc 命令解析

[Typescript]基础[TOC]([Typescript]基础篇之 tsc 命令解析 tsc 命令概览编译参数说明--declaration--watch 这里是对 tsc 的一个详细介绍 tsc 命令概览 安装 Typescript 后可以使用 tsc 编译 ts 文件,tsc 命令是否支持其它参数 如果需要查看 tsc 支持的命令&#xff0c;或者…...

测试人员简单使用Jenkins

一、测试人员使用jenkins干什么&#xff1f; 部署测试环境 二、相关配置说明 一般由开发人员进行具体配置 1.Repository URL&#xff1a;填写git地址 2.填写开发分支&#xff0c;测试人员可通过相应分支进行测试环境的构建部署 当多个版本并行时&#xff0c;开发人员可以通过…...

使用RecyclerView构建灵活的列表界面

使用RecyclerView构建灵活的列表界面 1. 引言 在现代移动应用中&#xff0c;列表界面是最常见的用户界面之一&#xff0c;它能够展示大量的数据&#xff0c;让用户可以浏览和操作。无论是社交媒体的动态流、商品展示、新闻列表还是任务清单&#xff0c;列表界面都扮演着不可或…...

linux ubuntu安装mysql

在 Ubuntu 上安装 MySQL 的步骤如下&#xff1a; 更新系统软件包列表&#xff1a; sudo apt update 安装 MySQL 服务器&#xff1a; sudo apt install mysql-server 安装完成&#xff0c;可以使用以下命令检查 MySQL 服务器是否正在运行: sudo systemctl status mysql 如果 MyS…...

计算机网络各层的功能以及常用协议

目录 1. 物理层&#xff08;Physical Layer&#xff09;2. 数据链路层&#xff08;Data Link Layer&#xff09;3. 网络层&#xff08;Network Layer&#xff09;4. 传输层&#xff08;Transport Layer&#xff09;5. 应用层&#xff08;Application Layer&#xff09; 计算机网…...

M. Minimal and Maximal XOR Sum 2023“钉耙编程”中国大学生算法设计超级联赛(7)hdu7359

Problem - 7359 题目大意&#xff1a;给出一个n个数的排列&#xff0c;可以将任意区间内的所有数头尾翻转&#xff0c;每次操作的费用等于区间长度&#xff0c;要求将其变成一个递增排列&#xff0c;求消耗费用的异或和的最小值和最大值 1<n<1e5 思路&#xff1a;操作…...

C++基础篇(五)内存模型及详细示例

目录 一、内存分区模型二、内存分区代码示例三、new 运算符详解 一、内存分区模型 C程序在运行时&#xff0c;将内存分为四个区域&#xff0c;不同的区域赋予不同的生命周期&#xff0c;以提供强大的灵活编程。 代码区&#xff1a;存储程序的二进制代码&#xff0c;通常是只读…...

基于 JMeter API 开发性能测试平台

目录 背景&#xff1a; 常用的 JMeter 类和功能的解释&#xff1a; JMeter 编写性能测试脚本的大致流程示意图&#xff1a; 源码实现方式&#xff1a; (1) 环境初始化 (2) 环境初始化 (3) 创建测试计划 (4) 创建 ThreadGroup (5) 创建循环控制器 (6) 创建 Sampler (…...

HBase-写流程

写流程顺序正如API编写顺序&#xff0c;首先创建HBase的重量级连接 &#xff08;1&#xff09;读取本地缓存中的Meta表信息&#xff1b;&#xff08;第一次启动客户端为空&#xff09; &#xff08;2&#xff09;向ZK发起读取Meta表所在位置的请求&#xff1b; &#xff08;…...

有哪些可以做任务的网站/百度广告代理商查询

多个媒体报道指华为消费者业务CEO余承东近日在内部会议上透露&#xff0c;华为手机今年的出货量大约在2.3亿部&#xff0c;按照这样的数据推算&#xff0c;四季度华为手机的出货量同比下滑近四分之一。华为公司公布今年上半年的业绩时表示上半年的手机出货量为1.18亿部&#xf…...

网站案例/电商平台运营方案

今天笔试的东西&#xff0c;重载&#xff0c;覆盖和隐藏 重载与覆盖成员函数被重载的特征&#xff1a;&#xff08;1&#xff09;相同的范围&#xff08;在同一个类中&#xff09;&#xff1b; // 这个很重要&#xff0c;如在不同类中则是隐藏&#xff08;2&#xff09;函数名字…...

应用网站建设/seo检查工具

第1周的编程题。请注意1.x的“如何交作业” 温馨提示&#xff1a; 1.本次作业属于Online Judge题目&#xff0c;提交后由系统即时判分。 2.学生可以在作业截止时间之前不限次数提交答案&#xff0c;系统将取其中的最高分作为最终成绩。 1温度转换&#xff08;10分&#xff09;题…...

兰溪高端网站建设公司/百度官方电话号码

本实用新型为一种双快锁体&#xff0c;具体涉及锁具领域。背景技术&#xff1a;门锁广泛地被应用于生活中&#xff0c;尤其是随着人们安全意识的提高&#xff0c;对门锁的安全性和使用的便捷性的要求也越来越高。现在的锁体类型多样&#xff0c;但是材质上会选用便宜的材料&…...

wordpress boombox/搜索app下载

有些同学可能会和我有一样的困扰&#xff0c;每次想要更改字体大小、背景颜色等&#xff0c;都需要百度一下才知道怎么去做。。。不知道有没有这种情况的孩子&#xff0c;反正我经常遇到&#xff0c;老是记不住&#xff0c;今天写下来&#xff0c;顺带自己忘记的时候可以查看一…...

用jquery做的书籍网站/n127网推广

有人是为了从做报表中解放出来&#xff0c;有人是为了实现更业务化的个性化报表&#xff0c;有人只懂得用Excel且确实通用性强&#xff0c;有人是因为无BI系统支持(或BI不够业务化)……确实有n种理由&#xff0c;最终导向了这个选择--自己用Excel来制作数据分析模板用一个平凡的…...