秒杀方案

前言

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

1. 业务场景

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

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

1
2
3
4
5
6
7
8
9
10
-- 秒杀商品表
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;<br><br>--秒杀订单表

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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>();
     
    /**
     * <strong>系统初始化  实现 implements InitializingBean  就可以完成 系统初始化加载数据</strong>
     * */<br>     @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中。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//内存标记,减少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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 减库存 ,下订单 写入秒杀订单,同时向数据库中写入该用户生成的订单信息(应为前端会轮询的调用我们查询该用户下单结果接口,到时候查询该缓存信息就行)。

//减库存

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

  //下单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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);<br>          //订单信息保存到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 @   好记性不如烂笔头=>  阅读(462)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示