好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

  • 畅购商城(一):环境搭建
  • 畅购商城(二):分布式文件系统FastDFS
  • 畅购商城(三):商品管理
  • 畅购商城(四):Lua、OpenResty、Canal实现广告缓存与同步
  • 畅购商城(五):Elasticsearch实现商品搜索
  • 畅购商城(六):商品搜索
  • 畅购商城(七):Thymeleaf实现静态页
  • 畅购商城(八):微服务网关和JWT令牌
  • 畅购商城(九):Spring Security Oauth2
  • 畅购商城(十):购物车
  • 畅购商城(十一):订单
  • 畅购商城(十二):接入微信支付
  • 畅购商城(十三):秒杀系统「上」
  • 畅购商城(十四):秒杀系统「下」

流程分析

上面这张图是整个秒杀系统的流程。简单介绍一下:

秒杀是一个并发量很大的系统,数据吞吐量都很大,MySQL的数据是保存在硬盘中的,数据吞吐的能力满足不了整个秒杀系统的需求。为了提高系统的访问速度,我们定时将秒杀商品从MySQL加载进Redis,因为Redis的数据是保存在内存中的,速度非常快,可以满足很高的吞吐量。

用户访问秒杀系统,请求到了OpenResty,OpenResty从Redis中加载秒杀商品,然后用户来到了秒杀列表页。当用户点击某个秒杀商品时,OpenResty再从Redis中加载秒杀商品详情信息,接着用户就来到了秒杀商品详情页。

当进入到商品详情页之后用户就可以点击下单了,点击下单的时候,OpenResty会检查商品是否还有库存,没有库存就下单失败。有库存的话还需要检查一下用户是否登录,没有登录的话再到OAuth2.0认证服务那边去登录,登录成功后再进入到秒杀微服务中,开始正式的下单流程。

理论上这时候还要对用户进行一些合法性检测,比如账号是否异常等,但是这太耗时了,为了减少系统响应的时间,用户检测这一步先省略。直接让用户进行排队,排队就是将用户id和商品id存入Redis队列,成功排队后给用户返回一个 “正在排队”的信息。

当排队成功后就开启多线程抢单,为每个排队的用户分配一个线程。在排队用户自己的线程中开始检测账号的状态是否正常,然后从Redis中检测库存时候足够,当所有条件都满足的时候,下单成功,将订单信息存入Redis。并将Redis中的排队信息从“排队中”改为“待支付”,这样前端在查询状态的时候就知道可以开始支付了,然后跳转到支付页面进行支付。当用户支付成功后,将抢单信息从Redis中删除,并同步到MySQL中。

最后一个问题,有的用户成功抢单后并不去付款,所以我们需要定时去处理未支付的订单。方案和上一篇文章中提到的一样,使用RabbitMQ死信队列。在抢单成功后将订单id、用户id和商品id存到RabbitMQ的队列1,设置半个小时后过期,过期后将信息发送给队列2,我们去监听队列2。当监听到队列2中的消息的时候,说明半个小时已经到了,这时候我们再去Redis中查询订单的状态,如果已经支付了就不去管它;如果没有支付就向微信服务器发送请求关闭支付,然后回滚库存,并将Redis中的抢单信息删除。

这样整个秒杀流程就结束了。

定时任务

怎么搭建秒杀微服务就不记录了,没什么好说的,秒杀微服务名为changgou-service-seckill。定时任务我也是第一次接触,所以在这里记录一下。

首先在启动类上添加一个注解**@EnableScheduling去开始对定时任务的支持。然后创建一个类SeckillGoodsPushTask**,在这个类上添加@Component注解,将其注入Spring容器。然后再添加一个方法,加上**@Scheduled**注解,声明这个方法是一个定时任务。

/*** SeckillGoodsPushTask* 定时将秒杀商品加载到redis中*/
@Scheduled(cron = "0/5 * * * * ?")
public void loadGoodsPushRedis() {List<Date> dateMenu = DateUtil.getDateMenus();for (Date date : dateMenu) {date.setYear(2019-1900);    //2019-6-1 为了方便测试date.setMonth(6-1);date.setDate(1);String dateString = SystemConstants.SEC_KILL_GOODS_PREFIX +DateUtil.data2str(date,"yyyyMMddHH");BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(dateString);Set<Long> keys = boundHashOperations.keys();    //获取Redis中已有的商品的id集合List<SeckillGoods> seckillGoods;//将秒杀商品的信息从数据库中加载出来if (keys!=null && keys.size()>0) {seckillGoods = mapper.findSeckillGoodsNotIn(date,keys); } else {seckillGoods = mapper.findSeckillGoods(date);}//遍历秒杀商品集合,将商品依次放入Redis中for (SeckillGoods seckillGood : seckillGoods) {boundHashOperations.put(seckillGood.getId(),seckillGood);}}
}
----------------------------------------------------------------------------------------------------------------
@Repository("seckillGoodsMapper")
public interface SeckillGoodsMapper extends Mapper<SeckillGoods> {//查找符合条件的秒杀商品@Select("SELECT" +" * " +" FROM " +" tb_seckill_goods " +" WHERE " +" status = 1 " +" AND stock_count > 0 " +" AND start_time >= #{date} " +" AND end_time < DATE_ADD(#{date},INTERVAL 2 HOUR)")List<SeckillGoods> findSeckillGoods(@Param("date") Date date);//查询出符合条件的秒杀商品,排除之前已存入的@SelectProvider(type = SeckillGoodsMapper.SeckillProvider.class, method = "findSeckillGoodsNotIn")List<SeckillGoods> findSeckillGoodsNotIn(@Param("date") Date date, @Param("keys") Set<Long> keys);class SeckillProvider {public String findSeckillGoodsNotIn(@Param("date") Date date, @Param("keys") Set<Long> keys) {StringBuilder sql = new StringBuilder("SELECT" +" * " +" FROM " +" tb_seckill_goods " +" WHERE " +" status = 1 " +" AND stock_count > 0 " +" AND start_time >=  ");sql.append("'").append(date.toLocaleString()).append("'").append(" AND end_time < DATE_ADD(").append("'").append(date.toLocaleString()).append("'").append(" ,INTERVAL 2 HOUR) ").append(" AND id NOT IN (");for (Long key : keys) {sql.append(key).append(",");}sql.deleteCharAt(sql.length() - 1).append(")");System.out.println(sql.toString());return sql.toString();}}}

(cron = "0/5 * * * * ?")中几个参数分别代表秒-分-时-日-月-周-年。年可以省略,所以是6个。*表示所有值,比如 “分” 是*就代表每分钟都执行。?表示不需要关心这个值是多少。/表示递增触发,0/5表示从0秒开始每5秒触发一次。所以这段代码配置的就是每5秒执行一次定时任务。

上面这段代码的意思是:将MySQL中的秒杀商品放入Redis,为了避免添加重复的商品,先获取Redis中已有商品的id集合,然后在查询数据库的时候将已有的排除掉。redis中存入商品的键为秒杀开始的时间,例如 "2020100110"表示2020年10月1日10点,获取时间菜单用的是资料提供的一个工具类DateUtil。DateUtil的代码不难,我就不介绍了,开调试模式跟着走一遍就能看懂。为了方便测试,我将日期定在了2019年6月1日,实际开发中应该用当前日期。

秒杀频道页

将商品加载到Redis中后就可以开始下单流程了,首先需要有个秒杀频道页,就是将对应时间段的秒杀商品加载到页面上展示出来。前端将当前时间的字符串(yyyyMMddHH)传到后端,后端从Redis中查询出对应的商品返回到前端,前端进行展示。

//   SeckillGoodsController
//根据时间段(2019090516) 查询该时间段的所有的秒杀的商品
@GetMapping("/list")
public Result<List<SeckillGoods>> list(@RequestParam("time") String time){List<SeckillGoods> list = seckillGoodsService.list(time);return new Result<>(true,StatusCode.OK,"查询成功",list);
}
-----------------------------------------------------------------------------------
//  SeckillGoodsServiceImpl
@Override
public List<SeckillGoods> list(String time) {return redisTemplate.boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX+time).values();
}

代码很简单,就是根据键将商品从Redis中查询出来。

秒杀商品详情页

当用户点击秒杀频道页的商品后,就会进入到秒杀商品详情页。前端将当前时间段和商品的id传到后端,后端从Redis中将商品信息查询出来,然后返回给前端进行展示。

//   SeckillGoodsController
//根据时间段  和秒杀商品的ID 获取商品的数据
@GetMapping("/one")
public Result<SeckillGoods> one(String time,Long id){SeckillGoods seckillGoods = seckillGoodsService.one(time, id);return new Result<>(true,StatusCode.OK,"查询商品数据成功",seckillGoods);
}
------------------------------------------------------------------------------------------
//  SeckillGoodsServiceImpl
@GetMapping("/one")
public Result<SeckillGoods> one(String time,Long id){SeckillGoods seckillGoods = seckillGoodsService.one(time, id);return new Result<>(true,StatusCode.OK,"查询商品数据成功",seckillGoods);
}

多线程抢单

上面两个小节内容都不多,现在正式进入下单的流程。因为在秒杀环境中,并发量都很大,如果只开一个线程的话,用户不知道要等到猴年马月,所以为每个下单的用户分配一个线程去进行处理是比较妥当的。

要在SpringBoot中开启多线程,首先在启动类上添加一个注解**@EnableAsync**去开启对异步任务的支持。

//SeckillOrderController
//下单
@RequestMapping("/add")
public Result<Boolean> add(String time,Long id){//1.获取当前登录的用户的名称String username ="robod";//测试用写死boolean flag = seckillOrderService.add(id, time, username);return new Result(true,StatusCode.OK,"排队中。。。",flag);
}

前端将时间段和商品的id传进来,用户名暂时写死,方便测试。

//  SeckillOrderServiceImpl
@Override
public boolean add(Long id, String time, String username) {SeckillStatus seckillStatus = new SeckillStatus(username,LocalDateTime.now(),1,id,time);//将seckillStatus存入redis队列redisTemplate.boundListOps(SystemConstants.SEC_KILL_USER_QUEUE_KEY).leftPush(seckillStatus);redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).put(username,seckillStatus);multiThreadingCreateOrder.createOrder();return true;
}

在这段代码中,先根据已有的信息创建了一个SeckillStatus对象,这个类中存放了秒杀的一些状态信息。然后将seckillStatus放入redis队列中,如果及时地处理订单系统响应速度就会变慢,所以先创建一个SeckillStatus放入redis,然后调用**multiThreadingCreateOrder.createOrder()**去开启一个线程处理订单。

@Component
public class MultiThreadingCreateOrder {…………//异步抢单@Async  //声明该方法是个异步任务,另开一个线程去运行public void createOrder() {//从redis队列中取出seckillStatusSeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps(SystemConstants.SEC_KILL_USER_QUEUE_KEY).rightPop();BoundHashOperations seckillGoodsBoundHashOps = redisTemplate.boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX + seckillStatus.getTime());//从redis中查询出秒杀商品SeckillGoods seckillGoods = (SeckillGoods)seckillGoodsBoundHashOps.get(seckillStatus.getGoodsId());   if (seckillGoods == null || seckillGoods.getStockCount() <=0 ) {throw new RuntimeException("已售罄");}//创建秒杀订单SeckillOrder seckillOrder = new SeckillOrder();seckillOrder.setSeckillId(seckillGoods.getId());seckillOrder.setMoney(seckillGoods.getCostPrice());seckillOrder.setUserId(seckillStatus.getUsername());seckillOrder.setCreateTime(LocalDateTime.now());seckillOrder.setStatus("0");//将秒杀订单存入redis,键为用户名,确保一个用户只有一个秒杀订单redisTemplate.boundHashOps(SystemConstants.SEC_KILL_ORDER_KEY).put(seckillStatus.getUsername(),seckillOrder);//减库存,如果库存没了就从redis中删除,并将库存数据写到MySQL中seckillGoods.setStockCount(seckillGoods.getStockCount()-1);if (seckillGoods.getStockCount() <= 0) {seckillGoodsBoundHashOps.delete(seckillStatus.getGoodsId());seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);} else {seckillGoodsBoundHashOps.put(seckillStatus.getGoodsId(),seckillGoods);}//下单成功,更改seckillstatus的状态,再存入redis中seckillStatus.setOrderId(seckillOrder.getId());seckillStatus.setMoney(Float.valueOf(seckillGoods.getCostPrice()));seckillStatus.setStatus(2);      //等待支付redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).put(seckillStatus.getUsername(),seckillStatus);}}

在这个方法上添加了一个**@Async注解,说明该方法是个异步任务,每次执行该方法的时候都会另开一个线程去运行。之前不是将订单存入redis队列中了吗,现在从redis队列中取出。然后根据商品id查询出商品信息。接着进行库存判断,如果没有商品或者库存没了说明已经卖完了,抛出已售罄的异常。如果有库存的话,就创建一个秒杀订单,将status置为0表示未支付**。 然后将订单存入redis中,这样订单就算创建完成了。成功创建订单后就应该减去相应的库存。如果减完库存后发现库存没了,说明最后一件商品已经卖完了,这时候就可以将redis中的该商品删除,并更新到MySQL中。

最后修改seckillstatus的内容,并更新到redis中。之前没说把seckillstatus存入redis的作用,其实它的作用就是供前端查询订单状态。

既然是查询订单状态,得提供一个接口吧

畅购商城(十三):秒杀系统「上」相关推荐

  1. 畅购商城(三):商品管理

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 畅购商城(一):环境搭建 畅购商 ...

  2. 商品品牌信息的增删改查操作步骤_畅购商城(三):商品管理

    好好学习,天天向上 本文已收录至我的Github仓库 DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star 畅购商城(一):环境搭建 畅购商城(二):分布式文件系统 ...

  3. 畅购商城(五):Elasticsearch实现商品搜索

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 畅购商城(一):环境搭建 畅购商 ...

  4. Java毕业设计项目【畅购商城】

    为了帮助更多的铁汁们,快速进步,完成毕业设计,挺近大厂,我前面已经分享了很多项目 但是有铁汁们觉得实战项目不够,为了给支持我的朋友吧 此次分享的是商城项目,里面包含视频和代码,涉及到SSM.Sprin ...

  5. 畅购商城六:微服务网关与jwt令牌

    微服务网关 基本概念 对于微服务的各个服务一般会有不同的地址,外部客户端的一个服务可能要调用诸多的接口,这会带来以下的问题 客户端会多次请求不同的微服务,地址复杂 存在跨域请求,处理复杂 认证复杂 难 ...

  6. 畅购商城_第11章_ 订单

    畅购商城_第11章_ 订单 文章目录 畅购商城_第11章_ 订单 第11章 订单 课程内容 1 订单结算页 1.1 收件地址分析 1.2 实现用户收件地址查询 1.2.1 代码实现 1.2.2 测试 ...

  7. 畅购商城4.0 微信支付

    畅购商城4.0 1.微信支付 1.1流程分析 1.2微信支付概述 1.2.1账号申请 步骤一:注册公众号,根据自身主体类型注册对应的公众号 只能申请服务号,订阅号没有办法申请支付 https:// ...

  8. 畅购商城-添加订单实现(一)

    观前提示: 详细资料观看黑马程序员的畅购商城. 该博客尝试用解题思路说明代码实现. 笔者当前水平有限,因此该博客质量不高. 已知: Idworke:一个分布式的ID生成工具. 可以理解为帮助生成数据库 ...

  9. 【畅购商城】校验用户名、手机号以及前置技术Redis和阿里大鱼短信验证码

    搭建环境 后端web服务:changgou4-service-web 修改pom.xml文档 <?xml version="1.0" encoding="UTF-8 ...

最新文章

  1. 解决postgresql数据库localhost可以连接,ip连接不了的问题
  2. 计算机中什么是适配器及作用,适配器是什么?适配器的作用主要有哪些
  3. IPSec的NAT穿越
  4. 是什么在吞食我们的科研时间2019-11-24
  5. 浅谈手机游戏测试的要点
  6. c语言中栈堆,C语言中堆和栈的区别
  7. 遍历某路径下的所有文件
  8. pyqt 界面关闭信号_PyQt从类(子窗口)发送信号返回到MainWindow(类)
  9. flash相册制作软件模板_儿童照片相册模板 怎么制作炫酷视频相册
  10. 【特征提取】基于matlab自相关函数最大值端点检测【含Matlab源码 1769期】
  11. 各省简称 拼音 缩写_中国各省市的简称读音
  12. 微软更新服务器ip地址,微软承认Windows 10更新导致路由等本地IP地址打不开
  13. 计算机英语新词的认知语义阐释论文,英语新词的认知语义分析
  14. 高德地图API的一些使用心得
  15. 【董天一】IPFS的竞争对手们(一)
  16. 大数据行业六大核心发展趋势
  17. LM09丨费雪逆变换反转网格策略
  18. C++算法:最大回文子串---动态规划-----夹逼法----中心扩展法
  19. 群联PS3111主控+3D TLC实现全盘不掉速!群联的另类玩法
  20. 一站式社区智慧路灯系统集成解决方案解析

热门文章

  1. 编译程序,翻译程序,解释程序,目标程序解释
  2. JavaScript基础实战知识点记录及个人理解2
  3. 运动员减压各有各招儿
  4. Intel Aero飞行日记
  5. python两个文件内容异或,python 异或两个文件
  6. 仿射密码 -- 逆元
  7. 在微型计算机里1mb等于,在计算机中,1MB等于多少字节?
  8. oracle 文章趣读
  9. Bankless:Maker DAO的生存危机
  10. 微信小程序如何循环控制一行显示几个wx:for