秒杀方案

前言

首先,要明确一点,高并发场景下系统的瓶颈出现在哪里,其实主要就是数据库,那么就要想办法为数据库做层层防护,减轻数据库的压力。

1. 业务场景

  1. 秒杀频道首页列出秒杀商品,点击秒杀商品图片可以跳转到秒杀商品详细页面
  2. 商品详细页面显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存,当库存为0或者不存在活动时间范围内时无法秒杀
  3. 秒杀下单成功,直接跳转到支付页面(扫码),支付成功,跳转到成功页面,填写收货、电话、收件人等信息,完成订单。
2.数据库的设计

 应为秒杀活动是经常举行的,而且防止商品表,订单表上面冗余太多的字段,对于秒杀我们公司有一套专门的表。

-- 秒杀商品表
CREATE TABLE `miaosha_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀的商品表',
  `goods_id` bigint(20) DEFAULT NULL COMMENT '商品Id',
  `miaosha_price` decimal(10,2) DEFAULT '0.00' COMMENT '秒杀价',
  `stock_count` int(11) DEFAULT NULL COMMENT '库存数量',
  `start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
  `end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

--秒杀订单表

CREATE TABLE `miaosha_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`order_id` bigint(20) DEFAULT NULL COMMENT '订单ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品ID',
PRIMARY KEY (`id`),
UNIQUE KEY `u_uid_gid` (`user_id`,`goods_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1551 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

--订单详情表

CREATE TABLE `order_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品ID',
`delivery_addr_id` bigint(20) DEFAULT NULL COMMENT '收获地址ID',
`goods_name` varchar(16) DEFAULT NULL COMMENT '冗余过来的商品名称',
`goods_count` int(11) DEFAULT '0' COMMENT '商品数量',
`goods_price` decimal(10,2) DEFAULT '0.00' COMMENT '商品单价',
`order_channel` tinyint(4) DEFAULT '0' COMMENT '1pc,2android,3ios',
`status` tinyint(4) DEFAULT '0' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退款,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '订单的创建时间',
`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1565 DEFAULT CHARSET=utf8mb4;

 

--秒杀用户表

CREATE TABLE `miaosha_user` (
`id` bigint(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` varchar(255) NOT NULL,
`password` varchar(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
`salt` varchar(10) DEFAULT NULL,
`head` varchar(128) DEFAULT NULL COMMENT '头像,云存储的ID',
`register_date` datetime DEFAULT NULL COMMENT '注册时间',
`last_login_date` datetime DEFAULT NULL COMMENT '上蔟登录时间',
`login_count` int(11) DEFAULT '0' COMMENT '登录次数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  

三.秒杀实现思路

实现秒杀,注意3点

1. 防止卖超。(在数据库层解决,其他的什么缓存reids 花里胡哨的判断,只能说是优化,最基本的解决就是在数据库)

  解决方式 乐观锁,在商品减库存的时候,增加count>0的条件。

2. 防止重复下单。(在数据库层解决,其他的什么缓存reids 花里胡哨的判断,只能说是优化,最基本的解决就是在数据库)

  解决方式 唯一索引   在秒杀订单表中 使用 商品id 和用户id 当中唯一索引。

3. 解决并发,提升qps 。 

  解决方式: 减少数据库访问次数,使用内存,缓存redis ,消息队列 rabbitMq。

四.实现关键步骤说明

1. redis预减库存,减少对数据库的访问。
2.内存标记减少对redis 的访问。
3. 请求先入队缓存,直接返回排队中。
4. 然后通过mq 请求出队 ,异步操作。做后续的工作,比如数据库的减库存,生成订单。
5. 客户端轮询调用查询是否秒杀成功接口接口

代码逻辑。

1. 系统初始化的时候,查询商品信息,缓存到redis中,同时也缓存到本地标识中,其实就是一个hashMap

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {
	
	private static volatile boolean isGlobalActivityOver = false;
	private static HashMap<Long, Integer> stockMap =  new HashMap<Long, Integer>();
	//内存标记减少对redis 的访问
	private HashMap<Long, Boolean> localOverMap =  new HashMap<Long, Boolean>();
	
	/**
	 * 系统初始化  实现 implements InitializingBean  就可以完成 系统初始化加载数据
	 * */
     @Override public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); //内存标记,把商品的信息放到map中,进行判断减少对redis 的访问 localOverMap.put(goods.getId(), false); }

  2. 用户点击秒杀接口

    2.1 首先 判断该商品在内存(hashMap)是否存在,不存在,直接给出返回 商品已经秒杀完毕 信息,后续的redis 判断都没必要访问了。

    2.2 如果判断判断该商品在内存中有,然后读入reids 中商品的数据,预减库存(redis中的数据),并返回该商品在redis 中的数量。

      如果redis 中的商品数量大于商品实际的数量了。说明该商品已经卖完,同时,把hashMap 中对应商品的值,设置为true。

    2.3  通过商品id 和用户id 去数据库中判断该用户是否秒杀到商品了,如果有,直接给出返回信息 不能重复秒杀。否则 把商品id和用户id 入队 放到rabbitMq中。

    -- -- 以上操作是没有访问过数据库的。 代码如下

//内存标记,减少redis访问
    	boolean over = localOverMap.get(goodsId);
    	if(over) {
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//预减库存
    	long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
    	if(stock < 0) {
    		 localOverMap.put(goodsId, true);
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//判断是否已经秒杀到了
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
    		return Result.error(CodeMsg.REPEATE_MIAOSHA);
    	}
    	//入队
    	MiaoshaMessage mm = new MiaoshaMessage();
    	mm.setUser(user);
    	mm.setGoodsId(goodsId);
    	sender.sendMiaoshaMessage(mm);
    	return Result.success(0);//排队中

    2.4  消息出队,根据商品id 去数据库查询商品的数量,如果小于零,直接return,否则在去数据库查询该商品是否已经秒杀到了,如果秒杀到了,直接return。

log.info("receive message:"+message);
			MiaoshaMessage mm  = RedisService.stringToBean(message, MiaoshaMessage.class);
			MiaoshaUser user = mm.getUser();
			long goodsId = mm.getGoodsId();
			//判断商品数量是否大于零
			GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
	    	int stock = goods.getStockCount();
	    	if(stock <= 0) {
	    		return;
	    	}
	    	//判断是否已经秒杀到了
	    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
	    	if(order != null) {
	    		return;
	    	}

  

    2.5 减库存 ,下订单 写入秒杀订单,同时向数据库中写入该用户生成的订单信息(应为前端会轮询的调用我们查询该用户下单结果接口,到时候查询该缓存信息就行)。

//减库存

      //减库存 下订单 写入秒杀订单
		boolean success = goodsService.reduceStock(goods);
		if(success) {
			return orderService.createOrder(user, goods);
		}else {
			setGoodsOver(goods.getId());
			return null;
		}

  //下单

	@Transactional
	public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
		OrderInfo orderInfo = new OrderInfo();
		orderInfo.setCreateDate(new Date());
		orderInfo.setDeliveryAddrId(0L);
		orderInfo.setGoodsCount(1);
		orderInfo.setGoodsId(goods.getId());
		orderInfo.setGoodsName(goods.getGoodsName());
		orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
		orderInfo.setOrderChannel(1);
		orderInfo.setStatus(0);
		orderInfo.setUserId(user.getId());
		orderDao.insert(orderInfo);
		MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
		miaoshaOrder.setGoodsId(goods.getId());
		miaoshaOrder.setOrderId(orderInfo.getId());
		miaoshaOrder.setUserId(user.getId());
		orderDao.insertMiaoshaOrder(miaoshaOrder);
          //订单信息保存到redis redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder); return orderInfo; }

  

  3. 对于秒杀一些其他的小优化。

   3.1 图形验证码功能,这样对于缓解并发也是一个不错的手段。

   3.2 接口限流放刷。 请看我的另一篇博客 https://www.cnblogs.com/xiaowangbangzhu/p/13387243.html

  如有需要源码,请联系我。

 

posted @ 2020-07-25 18:00  好记性不如烂笔头=>  阅读(451)  评论(0编辑  收藏  举报