当前位置:   article > 正文

电商项目day19(秒杀功能实现)

电商项目day19(秒杀功能实现)

今日目标:

秒杀实现思路
    实现秒杀下单功能
    
    完成下单并发产生的订单异常问题  超卖
    完成高并发下用户下单排队和超限问题

一.秒杀的思路分析

1.需求分析:

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

秒杀一共有,两种限制:库存限制和时间限制

需求:

(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
(2)运营商审核秒杀申请
(3)秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
(4)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为 0 或不在活动期范围内时无法秒杀。
(5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。

(6)当用户秒杀下单 5 分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。

数据库表的分析:

秒杀表的数据库表,我们不放在其他的商品表中,单独设计一个表结构

tb_seckill_goods表结构    

tb_seckill_order表主要是:秒杀成功后生成的订单

我们分析什么条件的商品的数据能够在秒杀页面展示?

     审核通过

      有库存

       当前实现大于开始时间,并小于秒杀结束时间   即正在秒杀的商品

 秒杀的实现思路分析:

         秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为 0 时或活动期结束时,同步到数据库。 产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成
功后再写入数据库。

       基于redis缓存,减少数据库的访问压力,在秒杀之前就将数据库的的数据放到缓存中

       秒杀开始后,用户抢购商品订单,下单成功后,减少库存,此时我们是操作redis中秒杀商品库存数据

那么我们在什么时候更新数据库的库存呢,      ----------------->redis库存为零,秒杀结束

使用redis缓存,单线程服务器,数据安全

定时任务spring-task实现:

定时任务主要常用的有两种:     spring - task     和quartz   

我们在这介绍spring  task   :

它可以说是轻量级的quartz    ,    主要配置文件和注解两种形式  

定时任务框架都是基于cron表达式完成定时时间指定。

往往都是6位字符
            Seconds Minutes Hours DayofMonth Month   DayofWeek 
                秒    分     时     月中某天   月     周中某天   
                
                每周一凌晨1点执行任务:0 0 1 ? * 2
                
                每天15点55分执行任务:0 55 15 * * ?
                
                每月6号凌晨执行任务:0 0 0 6 * ?
                
                每隔10秒多久执行一次:0/10 0 0 * * ?
                                    
            
        注意:月中某天和周中某天只能出现一个*,不能同时为*,如果两者中有任意一个赋值,另一个往往都赋予?

 

把数据库的商品缓存到redis中 这样就能,在下单的时候通过 通过redis总访问数据

创建一个定时任务的工程  seckill_task

配置文件:

web.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns="http://java.sun.com/xml/ns/javaee"
  4. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  5. version="2.5">
  6. <context-param>
  7. <param-name>contextConfigLocation</param-name>
  8. <param-value>classpath*:spring/applicationContext*.xml</param-value>
  9. </context-param>
  10. <listener>
  11. <listener-class>
  12. org.springframework.web.context.ContextLoaderListener
  13. </listener-class>
  14. </listener>
  15. </web-app>

applicationContext-task .xml

  1. <!--包扫描-->
  2. <context:component-scan base-package="com.pinyougou.seckill.task"/>
  3. <!--开启注解驱动-->
  4. <task:annotation-driven/>

seckillTask:主要是通过定时,从数据中获得数据缓存到redis数据库中  

  1. @Component
  2. public class SeckillTask {
  3. @Autowired
  4. private TbSeckillGoodsMapper seckillGoodsMapper;
  5. @Autowired
  6. private RedisTemplate redisTemplate;
  7. @Scheduled(cron = "0/30 * * * * ?")//30秒执行一次
  8. public void synchronizeSeckillGoodsToRedis(){
  9. //1.查询需要秒杀的商品
  10. //我们把符合的商品都查询出来放到redis中
  11. TbSeckillGoodsExample example = new TbSeckillGoodsExample();
  12. TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
  13. criteria.andStatusEqualTo("1").
  14. andStartTimeLessThanOrEqualTo(new Date()).
  15. andEndTimeGreaterThanOrEqualTo(new Date()).
  16. andStockCountEqualTo(0);
  17. List<TbSeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example);
  18. //2.将查询的商品的数据放到redis中
  19. //我们将查询符合的数据放到hash格式放到redis中
  20. for (TbSeckillGoods seckillGood : seckillGoods) {
  21. redisTemplate.boundHashOps("SECKILL_GOODS").put(seckillGood.getId(),seckillGood);
  22. //商品的详情页,我们可以通过商品的id取值,如下就是的
  23. // List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
  24. }
  25. System.out.println("synchronizeSeckillGoodsToRedis worker finished...");
  26. }
  27. }
List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();

这个数据我们在秒杀的详情页中展示,所以通过上面的格式存储后,我们就可以通过商品的id取值

二.秒杀下单功能实现

构建秒杀的功能模块  web   interface   service

从redis 中获取需要参加秒杀的商品

后台代码:

service层:

  1. @Service
  2. @Transactional
  3. public class SeckillServiceImpl implements SeckillService{
  4. @Autowired
  5. private RedisTemplate redisTemplate;
  6. /**
  7. * 从redis 中查询所有要参加秒杀的商品
  8. * @return
  9. */
  10. @Override
  11. public List<TbSeckillGoods> findAllSeckillGoodsFromRedis() {
  12. //获取redis中的数据
  13. List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
  14. return seckill_goods;
  15. }
  16. }

interface层和controller层:

  1. /**
  2. * 从redis中查询所有的秒杀商品列表
  3. */
  4. public List<TbSeckillGoods> findAllSeckillGoodsFromRedis();
  5. //controller层:
  6. @RestController
  7. @RequestMapping("/seckill")
  8. public class SeckillController {
  9. @Reference
  10. private SeckillService seckillService;
  11. /**
  12. * 从redis中查询所有的要参加秒杀的商品
  13. * @return
  14. */
  15. @RequestMapping("/findSeckillList")
  16. public List<TbSeckillGoods> findSeckillList(){
  17. return seckillService.findAllSeckillGoodsFromRedis();
  18. }
  19. }

前台页面的实现:

我们通过$http内置对象发送请求

  1. //服务层
  2. app.service('seckillService',function($http){
  3. //查询需要秒杀的商品列表
  4. this.findSeckillList=function(){
  5. return $http.get('seckill/findSeckillList.do');
  6. }
  7. });
  1. //控制层
  2. app.controller('seckillController' ,function($scope,$controller ,seckillService){
  3. $controller('baseController',{$scope:$scope});//继承
  4. //,$location 跨域
  5. //查询需要从redis中获得需要的秒杀商品数据
  6. $scope.findSeckillList=function () {
  7. seckillService.findSeckillList().success(function (response) {
  8. $scope.seckillList=response;
  9. })
  10. }
  11. });

我们通过秒杀页面跳转到秒杀的详情页,通过路由传参

详情页面的展示:

思路:我们通过商品的id查询商品的详情,前台页面进行展示,注意倒计时的处理,我们通过angularjs中$interval  一个内置对象进行设置

后台我们先从spring-sceurity中获得用户登录的信息,然后从redis中获得商品的数据,组装订单,然后在redis的商品库中减去一,保存订单,当库存为零的是时候我们,更新数据库,清除redis中该商品的数据,注意:一定要在秒杀成功后,扣减库存

  1. /**
  2. * 保存秒杀的订单
  3. */
  4. @RequestMapping("/saveSeckillOrder")
  5. public Result saveSeckillOrder(Long seckillGoodsId){
  6. try {
  7. //基于安全获取登录人信息
  8. String userId = SecurityContextHolder.getContext().getAuthentication().getName();
  9. if(userId.equals("anonymousUser")){
  10. return new Result(false,"请下登录,再下单");
  11. }
  12. seckillService.saveSeckillOrder(seckillGoodsId,userId);
  13. return new Result(true,"秒杀下单成功");
  14. } catch (RuntimeException e) {
  15. e.printStackTrace();
  16. return new Result(false,e.getMessage());
  17. }catch (Exception e) {
  18. e.printStackTrace();
  19. return new Result(false,"秒杀下单失败");
  20. }
  21. }

service层:

  1. /**
  2. * 保存秒杀订单
  3. * @param seckillGoodsId
  4. * @param userId
  5. */
  6. @Override
  7. public void saveSeckillOrder(Long seckillGoodsId, String userId) {
  8. //从缓存中获取秒杀商品
  9. TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);
  10. if(seckillGoods==null || seckillGoods.getStockCount()<=0){
  11. throw new RuntimeException("商品售完");
  12. }
  13. //组装秒杀订单数据
  14. TbSeckillOrder seckillOrder = new TbSeckillOrder();
  15. /*
  16. tb_seckill_order
  17. `id` bigint(20) NOT NULL COMMENT '主键',
  18. `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID',
  19. `money` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
  20. `user_id` varchar(50) DEFAULT NULL COMMENT '用户',
  21. `seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
  22. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  23. `status` varchar(1) DEFAULT NULL COMMENT '状态',
  24. */
  25. seckillOrder.setId(idWorker.nextId());
  26. seckillOrder.setSeckillId(seckillGoodsId);
  27. seckillOrder.setMoney(seckillGoods.getCostPrice());
  28. seckillOrder.setUserId(userId);
  29. seckillOrder.setSellerId(seckillGoods.getSellerId());
  30. seckillOrder.setCreateTime(new Date());
  31. seckillOrder.setStatus("1");//未支付
  32. //设置秒杀商品库存减一
  33. seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
  34. //保存秒杀订单
  35. seckillOrderMapper.insert(seckillOrder);
  36. if(seckillGoods.getStockCount()<=0){
  37. //商品售完,没有库存后,需要更新数据库中秒杀商品库存数据
  38. seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
  39. //清除redis中该商品
  40. redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
  41. }
  42. //秒选下单成功后,扣减库存
  43. redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);
  44. }

前台代码:

  1. //秒杀下单
  2. this.saveSeckillOrder=function (seckillGoodsId) {
  3. return $http.get('seckill/saveSeckillOrder.do?seckillGoodsId='+seckillGoodsId)
  4. }

controller层:

  1. //秒杀下单
  2. $scope.saveSeckillOrder=function () {
  3. seckillService.saveSeckillOrder($scope.seckillGoodsId).success(function (response) {
  4. alert(response.message);
  5. })
  6. }

测试:保存订单成功,下面我们进行秒杀的优化

三.秒杀优化的解决方案

1.解决同一个人重复购买此商品的问题:
    解决方案:用户下单后,向redis中存放一个预支付信息。当进入到下单的方法时,先判断redis中是否存在该用户的预支付信息。
        如果存在,则抛出异常提醒先去支付已买商品。

service层:

 

2.解决超卖的问题
    超卖是由于redis和mysql处理能力不同造成的。
    因为用户从redis中获取商品信息时,redis处理能力是很强劲的。而用户抢到商品下订单后,订单保存到mysql数据库,数据库本来
    处理能力是没有redis强劲的。所以,可能造成同时5个用户,能够为同一件商品下订单,这是不允许的。
    
    可以使用redis队列解决。
    使用list存储形式。可以基于左右压栈操作。
    
    基于redis队列,缓存某个秒杀商品还剩多个库存。
    
    
    
    消息队列也可以实现。

在这我们通过redis的左压栈实现

3.使用多线程解决操作mysql的问题
    因为mysql执行效率比redis要低,所以,需要充分利用CPU的资源,提升mysql处理操作
    可以基于spring整合多线程完成该操作。在spring配置文件中配置线程池。

配置线程池:

我们将执行数据库操作出去到线程中去

  1. package com.pinyougou.seckill.service.impl;
  2. import com.pinyougou.mapper.TbSeckillGoodsMapper;
  3. import com.pinyougou.mapper.TbSeckillOrderMapper;
  4. import com.pinyougou.pojo.TbSeckillGoods;
  5. import com.pinyougou.pojo.TbSeckillOrder;
  6. import com.pinyougou.util.IdWorker;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.data.redis.core.RedisTemplate;
  9. import java.util.Date;
  10. import java.util.Map;
  11. public class CreateSeckillOrder implements Runnable {
  12. @Autowired
  13. private RedisTemplate redisTemplate;
  14. @Autowired
  15. private IdWorker idWorker;
  16. @Autowired
  17. private TbSeckillOrderMapper seckillOrderMapper;
  18. @Autowired
  19. private TbSeckillGoodsMapper seckillGoodsMapper;
  20. @Override
  21. public void run() {
  22. //从redis中取出我们需要的订单任务
  23. Map<String,Object> param = (Map<String, Object>) redisTemplate.boundListOps("seckill_order_queue").rightPop();
  24. Long seckillGoodsId = (Long) param.get("seckillGoodsId");
  25. String userId = (String) param.get("userId");
  26. //从缓存中获取秒杀商品
  27. TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);
  28. //组装秒杀订单数据
  29. TbSeckillOrder seckillOrder = new TbSeckillOrder();
  30. /*
  31. tb_seckill_order
  32. `id` bigint(20) NOT NULL COMMENT '主键',
  33. `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID',
  34. `money` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
  35. `user_id` varchar(50) DEFAULT NULL COMMENT '用户',
  36. `seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
  37. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  38. `status` varchar(1) DEFAULT NULL COMMENT '状态',
  39. */
  40. seckillOrder.setId(idWorker.nextId());
  41. seckillOrder.setSeckillId(seckillGoodsId);
  42. seckillOrder.setMoney(seckillGoods.getCostPrice());
  43. seckillOrder.setUserId(userId);
  44. seckillOrder.setSellerId(seckillGoods.getSellerId());
  45. seckillOrder.setCreateTime(new Date());
  46. seckillOrder.setStatus("1");//未支付
  47. //设置秒杀商品库存减一
  48. seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
  49. //保存秒杀订单
  50. seckillOrderMapper.insert(seckillOrder);
  51. //秒杀下单成功后,我们要保存一份预支付的订单到redis,中,下次我们再在redis中判断,是否是第二次购买该商品
  52. //解决同一个人,不能重复购买的问题
  53. redisTemplate.boundSetOps("seckill_goods_"+seckillGoodsId).add(userId);
  54. if(seckillGoods.getStockCount()<=0){
  55. //商品售完,没有库存后,需要更新数据库中秒杀商品库存数据
  56. seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
  57. //清除redis中该商品
  58. redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
  59. }
  60. //秒选下单成功后,扣减库存
  61. redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);
  62. }
  63. }

在service中调用

注意注入我们配置的线程池:

4.排队人数提醒
    当一个请求进入下单的方法时,需要设置排队人数加1,当排队人多大于库存一定值时(例如10,根据业务规则确定),抛出异常,提醒排队人数过多。
    当一个人下单买完商品后,排队人数减一(在多线程下单模块完成)

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/224829
推荐阅读
相关标签
  

闽ICP备14008679号