秒杀方案
Posted xiaowangbangzhu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀方案相关的知识,希望对你有一定的参考价值。
首先,要明确一点,高并发场景下系统的瓶颈出现在哪里,其实主要就是数据库,那么就要想办法为数据库做层层防护,减轻数据库的压力。
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 去redis 中判断该用户是否秒杀到商品了,如果有,直接给出返回信息 不能重复秒杀。否则 把商品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 接口限流放刷。
如有需要源码,请联系我。
以上是关于秒杀方案的主要内容,如果未能解决你的问题,请参考以下文章