关于秒杀。 如何解决这个问题。理解这个问题。 所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。 秒杀商品通常有两种限制:库存限制、时间限制。 需求: (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个并发。测试的结果图,说明此案例测试成功。
|