好好学习,每天向上前端
本文已收录至个人Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Starjava
上面这张图是整个秒杀系统的流程。简单介绍一下:git
秒杀是一个并发量很大的系统,数据吞吐量都很大,MySQL的数据是保存在硬盘中的,数据吞吐的能力知足不了整个秒杀系统的需求。为了提升系统的访问速度,咱们定时将秒杀商品从MySQL加载进Redis,由于Redis的数据是保存在内存中的,速度很是快,能够知足很高的吞吐量。github
用户访问秒杀系统,请求到了OpenResty,OpenResty从Redis中加载秒杀商品,而后用户来到了秒杀列表页。当用户点击某个秒杀商品时,OpenResty再从Redis中加载秒杀商品详情信息,接着用户就来到了秒杀商品详情页。redis
当进入到商品详情页以后用户就能够点击下单了,点击下单的时候,OpenResty会检查商品是否还有库存,没有库存就下单失败。有库存的话还须要检查一下用户是否登陆,没有登陆的话再到OAuth2.0认证服务那边去登陆,登陆成功后再进入到秒杀微服务中,开始正式的下单流程。sql
理论上这时候还要对用户进行一些合法性检测,好比帐号是否异常等,可是这太耗时了,为了减小系统响应的时间,用户检测这一步先省略。直接让用户进行排队,排队就是将用户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队列中取出seckillStatus SeckillStatus 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的做用,其实它的做用就是供前端查询订单状态。
既然是查询订单状态,得提供一个接口吧👇
// SeckillOrderController //查询当前登陆的用户的抢单信息(状态) @GetMapping("/query") public Result<SeckillStatus> queryStatus(String username) { SeckillStatus seckillStatus = seckillOrderService.queryStatus(username); if (seckillStatus == null) { return new Result<>(false,StatusCode.NOT_FOUND_ERROR,"未查询到订单信息"); } return new Result<>(true,StatusCode.OK,"订单查询成功",seckillStatus); } ------------------------------------------------------------------------------------------- //SeckillOrderServiceImpl @Override public SeckillStatus queryStatus(String username) { return (SeckillStatus) redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).get(username); }
前端将用户名传入进来,而后查询订单状态,若是查询出来的状态是待支付的话,就能够进入支付流程了。
好了,这篇文章到这里就结束了,主要介绍了一下秒杀的流程,而后实现了定时任务,秒杀频道页,秒杀商品详情页和多线程抢单的功能。这个秒杀系统尚未结束,还存在不少问题,在下一篇文章中,将会修改现有的问题并继续完善秒杀的流程。让咱们下期再见!
码字不易,能够的话,给我来个
点赞
,收藏
,关注