admin管理员组文章数量:1794759
电商项目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
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="www.w3/2001/XMLSchema-instance" xmlns="java.sun/xml/ns/javaee" xsi:schemaLocation="java.sun/xml/ns/javaee java.sun/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring/applicationContext*.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> </web-app>applicationContext-task .xml
<!--包扫描--> <context:component-scan base-package="com.pinyougou.seckill.task"/> <!--开启注解驱动--> <task:annotation-driven/>seckillTask:主要是通过定时,从数据中获得数据缓存到redis数据库中
@Component public class SeckillTask { @Autowired private TbSeckillGoodsMapper seckillGoodsMapper; @Autowired private RedisTemplate redisTemplate; @Scheduled(cron = "0/30 * * * * ?")//每30秒执行一次 public void synchronizeSeckillGoodsToRedis(){ //1.查询需要秒杀的商品 //我们把符合的商品都查询出来放到redis中 TbSeckillGoodsExample example = new TbSeckillGoodsExample(); TbSeckillGoodsExample.Criteria criteria = example.createCriteria(); criteria.andStatusEqualTo("1"). andStartTimeLessThanOrEqualTo(new Date()). andEndTimeGreaterThanOrEqualTo(new Date()). andStockCountEqualTo(0); List<TbSeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example); //2.将查询的商品的数据放到redis中 //我们将查询符合的数据放到hash格式放到redis中 for (TbSeckillGoods seckillGood : seckillGoods) { redisTemplate.boundHashOps("SECKILL_GOODS").put(seckillGood.getId(),seckillGood); //商品的详情页,我们可以通过商品的id取值,如下就是的 // List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values(); } System.out.println("synchronizeSeckillGoodsToRedis worker finished..."); } } List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();这个数据我们在秒杀的详情页中展示,所以通过上面的格式存储后,我们就可以通过商品的id取值
二.秒杀下单功能实现构建秒杀的功能模块 web interface service
从redis 中获取需要参加秒杀的商品
后台代码:
service层:
@Service @Transactional public class SeckillServiceImpl implements SeckillService{ @Autowired private RedisTemplate redisTemplate; /** * 从redis 中查询所有要参加秒杀的商品 * @return */ @Override public List<TbSeckillGoods> findAllSeckillGoodsFromRedis() { //获取redis中的数据 List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values(); return seckill_goods; } }interface层和controller层:
/** * 从redis中查询所有的秒杀商品列表 */ public List<TbSeckillGoods> findAllSeckillGoodsFromRedis(); //controller层: @RestController @RequestMapping("/seckill") public class SeckillController { @Reference private SeckillService seckillService; /** * 从redis中查询所有的要参加秒杀的商品 * @return */ @RequestMapping("/findSeckillList") public List<TbSeckillGoods> findSeckillList(){ return seckillService.findAllSeckillGoodsFromRedis(); } }前台页面的实现:
我们通过$http内置对象发送请求
//服务层 app.service('seckillService',function($http){ //查询需要秒杀的商品列表 this.findSeckillList=function(){ return $http.get('seckill/findSeckillList.do'); } }); //控制层 app.controller('seckillController' ,function($scope,$controller ,seckillService){ $controller('baseController',{$scope:$scope});//继承 //,$location 跨域 //查询需要从redis中获得需要的秒杀商品数据 $scope.findSeckillList=function () { seckillService.findSeckillList().success(function (response) { $scope.seckillList=response; }) } });我们通过秒杀页面跳转到秒杀的详情页,通过路由传参
详情页面的展示:
思路:我们通过商品的id查询商品的详情,前台页面进行展示,注意倒计时的处理,我们通过angularjs中$interval 一个内置对象进行设置
后台我们先从spring-sceurity中获得用户登录的信,然后从redis中获得商品的数据,组装订单,然后在redis的商品库中减去一,保存订单,当库存为零的是时候我们,更新数据库,清除redis中该商品的数据,注意:一定要在秒杀成功后,扣减库存
/** * 保存秒杀的订单 */ @RequestMapping("/saveSeckillOrder") public Result saveSeckillOrder(Long seckillGoodsId){ try { //基于安全获取登录人信 String userId = SecurityContextHolder.getContext().getAuthentication().getName(); if(userId.equals("anonymousUser")){ return new Result(false,"请下登录,再下单"); } seckillService.saveSeckillOrder(seckillGoodsId,userId); return new Result(true,"秒杀下单成功"); } catch (RuntimeException e) { e.printStackTrace(); return new Result(false,e.getMessage()); }catch (Exception e) { e.printStackTrace(); return new Result(false,"秒杀下单失败"); } }service层:
/** * 保存秒杀订单 * @param seckillGoodsId * @param userId */ @Override public void saveSeckillOrder(Long seckillGoodsId, String userId) { //从缓存中获取秒杀商品 TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId); if(seckillGoods==null || seckillGoods.getStockCount()<=0){ throw new RuntimeException("商品售完"); } //组装秒杀订单数据 TbSeckillOrder seckillOrder = new TbSeckillOrder(); /* tb_seckill_order `id` bigint(20) NOT NULL COMMENT '主键', `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID', `money` decimal(10,2) DEFAULT NULL COMMENT '支付金额', `user_id` varchar(50) DEFAULT NULL COMMENT '用户', `seller_id` varchar(50) DEFAULT NULL COMMENT '商家', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `status` varchar(1) DEFAULT NULL COMMENT '状态', */ seckillOrder.setId(idWorker.nextId()); seckillOrder.setSeckillId(seckillGoodsId); seckillOrder.setMoney(seckillGoods.getCostPrice()); seckillOrder.setUserId(userId); seckillOrder.setSellerId(seckillGoods.getSellerId()); seckillOrder.setCreateTime(new Date()); seckillOrder.setStatus("1");//未支付 //设置秒杀商品库存减一 seckillGoods.setStockCount(seckillGoods.getStockCount()-1); //保存秒杀订单 seckillOrderMapper.insert(seckillOrder); if(seckillGoods.getStockCount()<=0){ //商品售完,没有库存后,需要更新数据库中秒杀商品库存数据 seckillGoodsMapper.updateByPrimaryKey(seckillGoods); //清除redis中该商品 redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId); } //秒选下单成功后,扣减库存 redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods); }前台代码:
//秒杀下单 this.saveSeckillOrder=function (seckillGoodsId) { return $http.get('seckill/saveSeckillOrder.do?seckillGoodsId='+seckillGoodsId) }controller层:
//秒杀下单 $scope.saveSeckillOrder=function () { seckillService.saveSeckillOrder($scope.seckillGoodsId).success(function (response) { alert(response.message); }) }测试:保存订单成功,下面我们进行秒杀的优化
三.秒杀优化的解决方案1.解决同一个人重复购买此商品的问题: 解决方案:用户下单后,向redis中存放一个预支付信。当进入到下单的方法时,先判断redis中是否存在该用户的预支付信。 如果存在,则抛出异常提醒先去支付已买商品。
service层:
2.解决超卖的问题 超卖是由于redis和mysql处理能力不同造成的。 因为用户从redis中获取商品信时,redis处理能力是很强劲的。而用户抢到商品下订单后,订单保存到mysql数据库,数据库本来 处理能力是没有redis强劲的。所以,可能造成同时5个用户,能够为同一件商品下订单,这是不允许的。 可以使用redis队列解决。 使用list存储形式。可以基于左右压栈操作。 基于redis队列,缓存某个秒杀商品还剩多个库存。 消队列也可以实现。
在这我们通过redis的左压栈实现
3.使用多线程解决操作mysql的问题 因为mysql执行效率比redis要低,所以,需要充分利用CPU的资源,提升mysql处理操作 可以基于spring整合多线程完成该操作。在spring配置文件中配置线程池。
配置线程池:
我们将执行数据库操作出去到线程中去
package com.pinyougou.seckill.service.impl; import com.pinyougou.mapper.TbSeckillGoodsMapper; import com.pinyougou.mapper.TbSeckillOrderMapper; import com.pinyougou.pojo.TbSeckillGoods; import com.pinyougou.pojo.TbSeckillOrder; import com.pinyougou.util.IdWorker; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import java.util.Date; import java.util.Map; public class CreateSeckillOrder implements Runnable { @Autowired private RedisTemplate redisTemplate; @Autowired private IdWorker idWorker; @Autowired private TbSeckillOrderMapper seckillOrderMapper; @Autowired private TbSeckillGoodsMapper seckillGoodsMapper; @Override public void run() { //从redis中取出我们需要的订单任务 Map<String,Object> param = (Map<String, Object>) redisTemplate.boundListOps("seckill_order_queue").rightPop(); Long seckillGoodsId = (Long) param.get("seckillGoodsId"); String userId = (String) param.get("userId"); //从缓存中获取秒杀商品 TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId); //组装秒杀订单数据 TbSeckillOrder seckillOrder = new TbSeckillOrder(); /* tb_seckill_order `id` bigint(20) NOT NULL COMMENT '主键', `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID', `money` decimal(10,2) DEFAULT NULL COMMENT '支付金额', `user_id` varchar(50) DEFAULT NULL COMMENT '用户', `seller_id` varchar(50) DEFAULT NULL COMMENT '商家', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `status` varchar(1) DEFAULT NULL COMMENT '状态', */ seckillOrder.setId(idWorker.nextId()); seckillOrder.setSeckillId(seckillGoodsId); seckillOrder.setMoney(seckillGoods.getCostPrice()); seckillOrder.setUserId(userId); seckillOrder.setSellerId(seckillGoods.getSellerId()); seckillOrder.setCreateTime(new Date()); seckillOrder.setStatus("1");//未支付 //设置秒杀商品库存减一 seckillGoods.setStockCount(seckillGoods.getStockCount()-1); //保存秒杀订单 seckillOrderMapper.insert(seckillOrder); //秒杀下单成功后,我们要保存一份预支付的订单到redis,中,下次我们再在redis中判断,是否是第二次购买该商品 //解决同一个人,不能重复购买的问题 redisTemplate.boundSetOps("seckill_goods_"+seckillGoodsId).add(userId); if(seckillGoods.getStockCount()<=0){ //商品售完,没有库存后,需要更新数据库中秒杀商品库存数据 seckillGoodsMapper.updateByPrimaryKey(seckillGoods); //清除redis中该商品 redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId); } //秒选下单成功后,扣减库存 redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods); } }在service中调用
注意注入我们配置的线程池:
4.排队人数提醒 当一个请求进入下单的方法时,需要设置排队人数加1,当排队人多大于库存一定值时(例如10,根据业务规则确定),抛出异常,提醒排队人数过多。 当一个人下单买完商品后,排队人数减一(在多线程下单模块完成)
版权声明:本文标题:电商项目day19(秒杀功能实现) 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1686518615a76749.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论