商品秒杀设计思路

注意点

  1. 很短时间内出现大量请求
  2. 对软件和硬件做出相应优化
  3. 保证系统的高可用性

整体架构图

实现方案

  1. redis:实现缓存+分布式锁
  2. RocketMQ:用于解耦和异步调用
  3. Mysql:存放真实的商品信息
  4. Mybatis:orm框架
  5. springboot:接收请求,进行整合 

 

简易代码实现

数据库

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `price` decimal(10, 2) NULL DEFAULT NULL,
  `stocks` int(255) NULL DEFAULT NULL,
  `status` int(255) NULL DEFAULT NULL,
  `pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '华为mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '锤子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');

-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NULL DEFAULT NULL,
  `order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `goods_id` int(11) NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

seckill-web服务

配置yml文件

server:
  port: 8082
  tomcat:
    threads:
      max: 400
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 10
    password: 1234
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: seckill-group

编写控制类

思路:

1.利用setnx对用户进行去重,每种商品只能抢购一次

2.库存预扣减

3.消息放入mq中,异步处理订单

@GetMapping("/seckill")
public String doSecKill(Integer goodsId/*, Integer userId*/) {
    int userId = userIdAtomic.incrementAndGet();
    // uk
    String key = userId + "-" + goodsId;
    Boolean flag = redisTemplate.opsForValue().setIfAbsent("uk" + key, "");
    if (!flag) {
        return "你已经参与过该商品的抢购了";
    }
    //假设库存已经同步了  key:goods_stock:1  val:10
    Long count = redisTemplate.opsForValue().decrement("goodsId", goodsId);
    if (count <= 0) {
        return "商品已经清空";
    }
    //mq 异步处理
    HashMap<String, Integer> map = new HashMap<>(4);
    map.put("goodsId", goodsId);
    map.put("userId", userId);
    rocketMQTemplate.asyncSend("secKillTopic", JSON.toJSONString(map), new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("发送成功");
        }

        @Override
        public void onException(Throwable throwable) {
            System.out.println("发送失败:" + throwable.getMessage());
        }
    });
    return "正在抢购中......";
}

seckill-service服务

配置yml文件

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/seckill?serverTimezone=GMT%2B8&SSL=false
    password: root
    username: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: 127.0.0.1
    port: 6379
    database: 10
    password: 1234
  main:
    allow-bean-definition-overriding: true
rocketmq:
  name-server: 127.0.0.1:9876
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

 通过easy-code插件生成对应的service,dao,mapper文件(省略)

配置数据同步(定时任务同步库存到redis中 为了测试方便,项目启动时同步数据)

@Component
public class DataSync {
    @Resource
    private GoodsDao goodsDao;

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 在项目启动后触发
     * 并且属性注入完后执行
     * @PostConstruct
     * 实例化 (new)
     * 属性注入
     * 初始化(前PostConstruct、中InitializingBean、后BeanPostProcessor)自定义的initMethod方法
     * 使用
     */
    @PostConstruct
    public void initData() {
        List<Goods> goodsList =  goodsDao.selectSecKillGoods();
        if (CollectionUtils.isEmpty(goodsList)){
            return;
        }
        goodsList.forEach(good->{
            redisTemplate.opsForValue().set("goodsId"+good.getId(),good.getStocks().toString());
        });
    }

}

配置rocketmq的监听器

@Component
@RocketMQMessageListener(topic = "secKillTopic",
        consumerGroup = "secKill-consumer-group",
        consumeMode = ConsumeMode.CONCURRENTLY,
        consumeThreadNumber = 40)
public class SecKillListener implements RocketMQListener<MessageExt> {


    @Resource
    private GoodsService goodsService;

    @Resource
    private RedisTemplate redisTemplate;


    int ZX_TIME = 10000;

    /**
     * 扣减库存
     * 减订单表
     * 方案一:加Synchronized锁
     * @param messageExt
     */
//    @Override
//    public void onMessage(MessageExt messageExt) {
//        String msg = new String(messageExt.getBody());
//        // userId-goodsId
//        int userId = Integer.parseInt(msg.split("-")[0]);
//        int goodsId = Integer.parseInt(msg.split("-")[1]);
//        //在事务外面加锁 实现安全
//        synchronized (this) {
//            goodsService.realSecKill(userId, goodsId);
//        }
//    }

    /**
     * 方案二:redis setnx
     *
     * @param messageExt
     */
    @Override
    public void onMessage(MessageExt messageExt) {
        String msg = new String(messageExt.getBody());
        // userId-goodsId
        int userId = Integer.parseInt(msg.split("-")[0]);
        int goodsId = Integer.parseInt(msg.split("-")[1]);

        int curThreadTime = 0;
        while (curThreadTime < ZX_TIME) {
            Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "");
            if (flag) {
                try {
                    goodsService.realSecKill(userId, goodsId);
                    return;
                } finally {
                    redisTemplate.delete("lock:" + goodsId);
                }
            } else {
                curThreadTime += 200;
                try {
                    Thread.sleep(200L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

 商品测试类

@Service("goodsService")
public class GoodsServiceImpl implements GoodsService {
    @Resource
    private GoodsDao goodsDao;

    @Resource
    private OrderRecordsDao orderRecordsDao;

    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    @Override
    public Goods queryById(Integer id) {
        return this.goodsDao.queryById(id);
    }


    /**
     * 新增数据
     *
     * @param goods 实例对象
     * @return 实例对象
     */
    @Override
    public Goods insert(Goods goods) {
        this.goodsDao.insert(goods);
        return goods;
    }

    /**
     * 修改数据
     *
     * @param goods 实例对象
     * @return 实例对象
     */
    @Override
    public Goods update(Goods goods) {
        this.goodsDao.update(goods);
        return this.queryById(goods.getId());
    }

    /**
     * 通过主键删除数据
     *
     * @param id 主键
     * @return 是否成功
     */
    @Override
    public boolean deleteById(Integer id) {
        return this.goodsDao.deleteById(id) > 0;
    }

    /**
     * 方案一常规做法
     * 扣减库存
     * 写订单表
     *
     * @param userId
     * @param goodsId
     */
//    @Override
//    @Transactional(rollbackFor = Exception.class)
//    public void realSecKill(int userId, int goodsId) {
//        Goods goods = goodsDao.queryById(goodsId);
//        Integer finalStock = goods.getStocks() - 1;
//        if (finalStock < 0) {
//            throw new RuntimeException("商品" + goodsId + "不足,用户Id:" + userId);
//        }
//        goods.setStocks(finalStock);
//        goods.setUpdateTime(new Date());
//        int update = goodsDao.update(goods);
//        if (update > 0) {
//            OrderRecords order = new OrderRecords();
//            order.setUserId(userId);
//            order.setGoodsId(goodsId);
//            order.setCreateTime(new Date());
//            orderRecordsDao.insert(order);
//        }
//    }

    /**
     * 方案二行锁方案:但不适用于并发量高的场景,最终压力都是数据库承担
     * update goods set stocks = stocks - 1,update_time = now() where id = #{value}
     * @param userId
     * @param goodsId
     */
//    @Override
//    @Transactional(rollbackFor = Exception.class)
//    public void realSecKill(int userId, int goodsId) {
//        int i = goodsDao.updateStock(goodsId);
//        if (i > 0) {
//            OrderRecords order = new OrderRecords();
//            order.setUserId(userId);
//            order.setGoodsId(goodsId);
//            order.setCreateTime(new Date());
//            orderRecordsDao.insert(order);
//        }
//    }


    /**
     * 方案三
     * @param userId
     * @param goodsId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void realSecKill(int userId, int goodsId) {
        int i = goodsDao.updateStock(goodsId);
        if (i > 0) {
            OrderRecords order = new OrderRecords();
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            order.setCreateTime(new Date());
            orderRecordsDao.insert(order);
        }
    }


}
  • 10
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值