SpringBoot+Redis+RabbitMQ 实现订单秒杀

前言

电商平台有时候会搞一些秒杀活动,秒杀活动的并发量特别高,会导致访问变慢、商品超卖等问题。所以,写一篇博客记录一下主要实现思路,解决一些小问题。

一、建表

就简单的建一张表,秒杀活动表。然后就可以写代码,通过页面添加秒杀活动了,这里就不写这些了。

CREATE TABLE `seckill_promotion_table` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(20) DEFAULT '0' COMMENT '商品id',
  `ps_count` int(11) DEFAULT NULL COMMENT '秒杀数量',
  `current_price` decimal(10,2) DEFAULT NULL COMMENT '秒杀价格',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `status` int(11) NOT NULL DEFAULT '0' COMMENT '0-未开始 1-进行中  2-已结束',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='秒杀活动表';

二、数据静态化

把商品信息等等一些不经常改变的数据都写死在html页面里,不用从后台获取,这样秒杀的时候就能加快用户的访问速度,秒杀倒计时和评论信息可以从后台获取进行渲染。可以使用thymeleaf或者freemarker来渲染,然后生成html文件,直接放在nginx里就可以访问了。这个也很简单,就不在赘述了。

三、初始化进redis,防止超卖

添加秒杀活动的时候会设置开始时间和结束时间。设置的开始时间并不是马上就开始,所以我们就需要定时任务去查询当前是否有开始的秒杀任务。

然后把要开始的秒杀任务扔进redis里,为什么要扔进redis里呢?因为redis操作都是串行的,同时又好多请求的时候,会排队一个一个的执行,防止商品的超卖。所以, 有几个要秒杀的商品,我们就初始化几个list对象放进redis里。

@Mapper
public interface SeckillPromotionLWQDAO{

    @InsertProvider(method = "insertSeckillPromotion", type = SeckillPromotionLWQProvider.class)
    @Options(useGeneratedKeys = true,keyProperty = "seckillpromotion.id")
	Integer insert(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;

    @UpdateProvider(method = "updateSeckillPromotion", type = SeckillPromotionLWQProvider.class)
	Integer updateById(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;

    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table "
    		+ "WHERE id = #{id}")
	SeckillPromotionDO getByPrimaryKey(Integer id)throws Exception;

    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table ")
	List<SeckillPromotionDO> listAllSeckillPromotionDO()throws Exception;
    
    /**
     * 查询未开始的秒杀动
     * @return
     * @throws Exception
     */
    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
    		+ "FROM seckill_promotion_table "
    		+ "WHERE now() BETWEEN start_time AND end_time AND status = 0")
	List<SeckillPromotionDO> listUnstartSeckill()throws Exception;
    
    /**
     * 查询已经过期的秒杀活动
     * @return
     * @throws Exception
     */
    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
    		+ "FROM seckill_promotion_table "
    		+ "WHERE now() > end_time AND status = 1")
	List<SeckillPromotionDO> listExpireSeckill()throws Exception;
    
}
@Component
public class SeckillTask {

	private static final String PREFIX = "seckill:count:";
	
	@Autowired
	private SeckillPromotionLWQDAO seckillDAO;
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;
	
	@Scheduled(cron = "0/5 * * * * ?")
	public void startSecKill() throws Exception{
		
		List<SeckillPromotionDO> list = seckillDAO.listUnstartSeckill();
		if (!list.isEmpty()) {
			
			for (SeckillPromotionDO seckill : list) {
				
				 //删掉以前重复的活动任务缓存
	            redisTemplate.delete(PREFIX + seckill.getId());
	            
	            //有几个库存商品,则初始化几个list对象
	            for(int i = 0 ; i < seckill.getPsCount() ; i++){
	                redisTemplate.opsForList().rightPush(PREFIX + seckill.getId(), seckill.getGoodsId());
	            }
	            seckill.setStatus(1);
	            seckillDAO.updateById(seckill);
				
			}
			
		}
		
	}
	
	@Scheduled(cron = "0/5 * * * * ?")
	public void endSecKill() throws Exception{
		
		List<SeckillPromotionDO> list = seckillDAO.listExpireSeckill();
		if (! list.isEmpty()) {
			
			for (SeckillPromotionDO seckill : list) {
				
				seckill.setStatus(2);
				seckillDAO.updateById(seckill);
				
				redisTemplate.delete(PREFIX + seckill.getId());
			}
		}
		
	}
	
}

四、开始秒杀

接下来就是秒杀商品了,我们会去redis里取上一步放进去的list,如果能取到就是秒杀成功了,然后把用户id放进redis的set里,这样秒杀的用户就不回重复了。

抢到商品后,生成订单信息,然后把订单信息扔进MQ里,用于削峰处理。

public ReturnDataDTO<Object> processSeckill(String userId, Integer seckillId) throws Exception {
		
		SeckillPromotionDO seckill = seckillDAO.getByPrimaryKey(seckillId);
		if (Objects.equals(seckill, null)) {
			throw new MyException(LwqExceptionEnum.NOT_EXIT);
		}
		Integer status = seckill.getStatus();
		if (status == 0) {
			throw new MyException(LwqExceptionEnum.NOT_START);
		}
		if (status == 2) {
			throw new MyException(LwqExceptionEnum.EVER_END);
		}
		String goodsIdInRedis = (String) redisTemplate.opsForList().leftPop(GOODS_PREFIX + seckill.getId());
		if (goodsIdInRedis != null) {
            //判断是否已经抢购过
            boolean isExisted = redisTemplate.opsForSet().isMember(USER_PREFIX + seckill.getId(), userId);
            if (!isExisted) {
            	//抢到商品了
                redisTemplate.opsForSet().add("seckill:users:" + seckill.getId(), userId);
            }else{
            	//已经抢购过的用户,再把商品id放回去
                redisTemplate.opsForList().rightPush("seckill:count:" + seckill.getId(), seckill.getGoodsId());
                throw new MyException(LwqExceptionEnum.EVER_SECKILL);
            }
        } else {
            throw new MyException(1018,"抱歉,该商品已被抢光,下次再来吧!!");
        }
	    
		String goodsId = seckill.getGoodsId();
		GoodsDO goodsDO = goodsDAO.getByPrimaryKey(goodsId);
		if (Objects.equals(goodsDO, null)) {
			throw new MyException(1019, "没有该商品");
		}
		BigDecimal currentPrice = goodsDO.getCurrentPrice();
		
		String orderNumber = UUID.randomUUID().toString();
		Snowflake snowflake = IdUtil.createSnowflake(1, 1);
		String id = snowflake.nextIdStr();
		
		GoodsOrderDO order = new GoodsOrderDO();
		order.setId(id);
		order.setOrderNumber(orderNumber);
		order.setUserId(userId);
		order.setTotalAmount(currentPrice);
		order.setStatus(1);
		
		//把订单信息发送到MQ,用于削峰处理
		JSONObject json = (JSONObject) JSONObject.toJSON(order);
	    seckillProducer.sendSeckillOrderMessage(json);
		
	    //返给前端订单号,用于主动查询订单是否创建成功
	    JSONObject data = new JSONObject(true);
	    data.put("userId", userId);
	    data.put("orderNumber", orderNumber);
	    
		return ReturnDataDTO.ok(data);
	}

五、生成订单

MQ的消费者就可以把订单信息入库,这样就生成了订单。

用户端就可以根据订单号去查询是否生成订单了。MQ消费者订单没有入库的时候,就给用户展示订单正在生成中等提示信息。

有了订单信息就可以去支付了。。

六、写在最后的话

主要是贴了一些主要的代码,思路就时有多少秒杀商品就初始化几个list到redis里,秒杀的时候去redis里拿list,能拿到就说明秒杀成功(注意判断用户的重复),就把用户放进set里,生成订单信息,扔进MQ。这时候就轮训订单是否生成。

  • 1
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值