黑马程序员技术交流社区

标题: [学习交流] 【广州校区】+ 秒杀的实现过程 [打印本页]

作者: ley    时间: 2019-1-2 17:13
标题: [学习交流] 【广州校区】+ 秒杀的实现过程
关于秒杀。
如何解决这个问题。理解这个问题。
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制:库存限制、时间限制。
需求:
(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
(2)运营商审核秒杀申请
(3)秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
(4)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
(5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
(6)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
上面保存订单的方式是先查看Redis中对应商品是否存在,如果存在且数量是否>0如果>0则下单,如果在并发情况下,如果20个人同时在执行如上查询代码这里,而此时对应商品只有一个,则会下20个单,而这20个单一定是有问题的,因为1件商品不可能同时给20个人发货。那么如何解决这种并发问题呢?我们可以用Redis队列实现。
秒杀下单优化
修改pyg-seckill-service的SeckillOrderServiceImpl.java,加入从队列中取数据校验商品是否存在的实现过程,队列中商品存在,则继续下单,否则抛出异常提示已售罄。
/***
*
创建订单
* @param
seckillid
* @param userid
*/
  
@Override
  
public void saveOrder(Long seckillid, String userid) {
   
//获取队列中的商品,如果能够获取,则商品存在,可以下单
    //这样可以避免多个用户同时抢购意见商品重复下单
   
Long goodsId = (Long) redisTemplate.boundListOps(SysContant.SECKILL_PREFIX + seckillid).rightPop();
    if(goodsId==null){
        throw new RuntimeException("
已售罄!");
    }

  
   
//获取商品详情
   
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SeckillGoods.class.getSimpleName()).get(seckillid);
   
//略....
}
上面的方案解决了并发情况下下单操作异常问题,但其实际秒杀中大量并发情况下,这个下单过程是需要很长等待时间的,所以这里我们建议用异步和多线程实现,最好不要让程序处于阻塞状态,而是在用户一下单的时候确认用户是否符合下单条件,如果符合,则开启线程执行,执行完毕之后,用户等待查询结果即可。
当我们在实现秒杀功能时候需要,解决队列排队造成的阻塞相应。
创建CreateOrder.java实现订单下单操作,在面试中常常会被问及多线程应用在项目哪里,这正好是一个很好的案例。
@Component
  
//@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE) //采用原型模式
  
public class CreateOrder implements Runnable {
  
   
@Autowired
   
private SeckillGoodsMapper seckillGoodsMapper;
  
   
@Autowired
   
private RedisTemplate redisTemplate;
  
   
@Autowired
   
private IdWorker idWorker;
  
   
@Override
   
public void run() {
        
//从Redis中获取排队信息,然后依次下单
        
OrderRecode orderRecode = (OrderRecode) redisTemplate.boundListOps(OrderRecode.class.getSimpleName()).rightPop();
  
        
if(orderRecode!=null){
            
//获取队列中的商品,如果能够获取,则商品存在,可以下单
            //这样可以避免多个用户同时抢购意见商品重复下单
            
Long goodsId = (Long) redisTemplate.boundListOps(SysContant.SECKILL_PREFIX + orderRecode.getGoodsId()).rightPop();
            
if(goodsId==null){
               
//售罄
                //将用户从排队中移除,否则用户不能继续排队
               
redisTemplate.boundSetOps(SysContant.SECKILL_USER).remove(orderRecode.getUserid());
               
return;
            }
  
            
//获取商品详情
            
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SeckillGoods.class.getSimpleName()).get(orderRecode.getGoodsId());
           
if(seckillGoods!=null){
               
//创建订单
               
SeckillOrder order = new SeckillOrder();
                order.setId(
idWorker.nextId());
                order.setSeckillId(orderRecode.getGoodsId());
                order.setMoney(seckillGoods.getCostPrice());   
//秒杀价格
               
order.setUserId(orderRecode.getUserid());
                order.setSellerId(seckillGoods.getSellerId());
                order.setCreateTime(
new Date());
                order.setStatus(
"0");   //0未支付,1已支付
                //订单数据存入Reids
               
redisTemplate.boundHashOps(SeckillOrder.class.getSimpleName()).put(orderRecode.getUserid(),order);
  
               
//Reids数据递减    ----    当库存为1的时候。有并发   加个同步锁。
               
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
               
if(seckillGoods.getStockCount()<=0){
                    
//商品卖完,同步数据库
                    
seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
  
                    
//清除缓存
                    
redisTemplate.boundHashOps(SeckillGoods.class.getSimpleName()).delete(seckillGoods.getId());
                }
else{
                    
//Redis库存递减同步
                    
redisTemplate.boundHashOps(SeckillGoods.class.getSimpleName()).put(seckillGoods.getId(),seckillGoods);
                }
  
               
//抢购人数削减
               
redisTemplate.boundValueOps(SysContant.SECKILL_COUNT_GOODSID_PREFIX+orderRecode.getGoodsId()).increment(-1);
            }
        }
    }
}
1.1.  配置线程池
<!--
   
线程池配置
-->
  
<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" id="executor">
   
<!-- 核心线程数,默认为1-->
   
<property name="corePoolSize" value="10" />
   
<!--最大线程数,默认为Integer.MAX_VALUE-->
   
<property name="maxPoolSize" value="50" />
   
<!--队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE-->
   
<property name="queueCapacity" value="10000" />
   
<!--线程池维护线程所允许的空闲时间,默认为60s-->
   
<property name="keepAliveSeconds" value="300" />
   
<!--线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者-->
   
<property name="rejectedExecutionHandler">
        
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
   
</property>
  
</bean>
下单保存修改
修改SeckillOrderServiceImpl.java,把之前实现的保存订单修改成启动线程调用
@Autowired
  
private RedisTemplate redisTemplate;
  
  
@Autowired
  
private CreateOrder createOrder;
  
  
@Autowired
  
private ThreadPoolTaskExecutor executor;
  
/***
*
创建订单
* @param
seckillid
* @param userid
*/
  
@Override
  
public void saveOrder(Long seckillid, String userid) {
   
//获取队列中的商品,如果能够获取,则商品存在,可以下单
    //这样可以避免多个用户同时抢购意见商品重复下单
  

   
//获取商品详情
   
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SeckillGoods.class.getSimpleName()).get(seckillid);
   
if(seckillGoods==null || seckillGoods.getStockCount()<=0){
        
throw new RuntimeException("已售罄!");
    }
  
   
//判断用户是否正在排队
   
Boolean member = redisTemplate.boundSetOps(SysContant.SECKILL_USER).isMember(userid);
   
if(member){
        
//判断用户是否下单
        
Object order = redisTemplate.boundHashOps(SeckillOrder.class.getSimpleName()).get(userid);
        
if(order!=null){
            
throw new RuntimeException("您有未支付的订单!");
        }
        
throw new RuntimeException("正在排队中!");
    }
  
   
//当前商品秒杀的人数
   
Long seckillusercount = redisTemplate.boundValueOps(SysContant.SECKILL_COUNT_GOODSID_PREFIX + seckillid).increment(0);
  
   
//当前商品的剩余个数
   
if((seckillGoods.getStockCount().intValue()+200)<=seckillusercount){
        
//超过限制,这里的200可以自定义
        
throw new RuntimeException("当前商品抢购人数过多");
    }
  
   
//下单数据加入缓存
   
OrderRecode orderRecode = new OrderRecode(seckillid, userid);
   
redisTemplate.boundListOps(OrderRecode.class.getSimpleName()).leftPush(orderRecode);
  
   
//排队记录加入缓存
   
redisTemplate.boundSetOps(SysContant.SECKILL_USER).add(userid);
  
   
//抢购人数记录
   
redisTemplate.boundValueOps(SysContant.SECKILL_COUNT_GOODSID_PREFIX+seckillid).increment(1);
  
   
//调用线程执行
   
executor.execute(createOrder);
}
以下是模拟1000个并发。测试的结果图,说明此案例测试成功。


1.png (36.73 KB, 下载次数: 11)

1.png

2.png (40.08 KB, 下载次数: 12)

2.png

作者: 一个人一座城0.0    时间: 2019-1-7 08:36
看一看。




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2