淘东电商项目(76) -秒杀系统(完整代码实现)

简介: 淘东电商项目(76) -秒杀系统(完整代码实现)

引言

本文代码已提交至Github(版本号:2c985822b282756e3fd70490cb0ba6f4f2140e47),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

秒杀系统在前面已经讲解了“前端优化”以及“防止库存超卖”的功能,但是在效率这一块还是很慢的,那么后台的秒杀完整代码流程是如何的呢?本文来讲解下,阅读前,童鞋们可以先阅读之前写的博客:

本文目录结构:

l____引言

l____ 1.秒杀原理图

l____ 2. 后台核心代码

l________ 2.1 令牌桶生成接口

l________ 2.2 秒杀接口(核心)

l________________ 2.2.1 MQ配置

l________________ 2.2.2 生产者

l________________ 2.2.3 消费者

l________ 2.3 用户查询接口

l____ 3. 测试

1.秒杀原理图

下面贴上我自己整理的原理图,如下:

从原理图,可以看到秒杀的流程大致如下:

  1. 商户添加秒杀商品的时候,后台会自动从Redis里生成令牌桶,如商品A的库存有100个,那么当用户修改商品时会去Redis里添加一条数据,格式:商品id+List令牌桶(数量是库存数量)
  2. 用户抢购时,会从令牌桶里获取令牌,如果能获取成功,则通过MQ去异步修改数据库里面的订单表以及秒杀表。
  3. 抢购完成后,会提示用户“正在排队中…”,用户需要自己主动的去查询抢购结果。

2. 后台核心代码

2.1 令牌桶生成接口

令牌桶生成接口核心代码:

@Override
public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity) {
  // 1.验证参数
  if (seckillId == null) {
    return setResultError("商品库存id不能为空!");
  }
  if (tokenQuantity == null) {
    return setResultError("token数量不能为空!");
  }
  SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
  if (seckillEntity == null) {
    return setResultError("商品信息不存在!");
  }
  // 2.使用多线程异步生产令牌
  createSeckillToken(seckillId, tokenQuantity);
  return setResultSuccess("令牌正在生成中.....");
}
@Async
public void createSeckillToken(Long seckillId, Long tokenQuantity) {
  generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
}

Redis令牌桶生成工具类:

①GenerateToken

public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
    List<String> listToken = getListToken(keyPrefix, tokenQuantity);
    redisUtil.setList(redisKey, listToken);
}
public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
    List<String> listToken = new ArrayList<>();
    for (int i = 0; i < tokenQuantity; i++) {
        String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
        listToken.add(token);
    }
    return listToken;
}

②RedisUtil:

public void setList(String key, List<String> listToken) {
  stringRedisTemplate.opsForList().leftPushAll(key, listToken);
}

2.2 秒杀接口(核心)

2.2.1 MQ配置

①application.yml配置:

rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号   
    port: 5672
    ####账号 
    username: guest
    ####密码  
    password: guest
    ### 地址
    virtual-host: spike_host
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔时间
          initial-interval: 1000
        ####开启手动ack  
        acknowledge-mode: manual
        default-requeue-rejected: false

②RabbitMQ配置:

/**
 * description: RabbitmqConfig 配置
 * create by: YangLinWei
 * create time: 2020/5/26 10:54 上午
 */
@Component
public class RabbitmqConfig {
  // 添加修改库存队列
  public static final String MODIFY_INVENTORY_QUEUE = "modify_inventory_queue";
  // 交换机名称
  private static final String MODIFY_EXCHANGE_NAME = "modify_exchange_name";
  // 1.添加交换机队列
  @Bean
  public Queue directModifyInventoryQueue() {
    return new Queue(MODIFY_INVENTORY_QUEUE);
  }
  // 2.定义交换机
  @Bean
  DirectExchange directModifyExchange() {
    return new DirectExchange(MODIFY_EXCHANGE_NAME);
  }
  // 3.修改库存队列绑定交换机
  @Bean
  Binding bindingExchangeintegralDicQueue() {
    return BindingBuilder.bind(directModifyInventoryQueue()).to(directModifyExchange()).with("modifyRoutingKey");
  }
}
2.2.2 生产者
/**
 * description: 秒杀生产者
 * create by: YangLinWei
 * create time: 2020/5/26 10:58 上午
 */
@Component
@Slf4j
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {
  @Autowired
  private RabbitTemplate rabbitTemplate;
  @Transactional
  public void send(JSONObject jsonObject) {
    String jsonString = jsonObject.toJSONString();
    System.out.println("jsonString:" + jsonString);
    String messAgeId = UUID.randomUUID().toString().replace("-", "");
    // 封装消息
    Message message = MessageBuilder.withBody(jsonString.getBytes())
        .setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(messAgeId)
        .build();
    // 构建回调返回的数据(消息id)
    this.rabbitTemplate.setMandatory(true);
    this.rabbitTemplate.setConfirmCallback(this);
    CorrelationData correlationData = new CorrelationData(jsonString);
    rabbitTemplate.convertAndSend("modify_exchange_name", "modifyRoutingKey", message, correlationData);
  }
  // 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
  @Override
  public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    String jsonString = correlationData.getId();
    System.out.println("消息id:" + correlationData.getId());
    if (ack) {
      log.info(">>>使用MQ消息确认机制确保消息一定要投递到MQ中成功");
      return;
    }
    JSONObject jsonObject = JSONObject.parseObject(jsonString);
    // 生产者消息投递失败的话,采用递归重试机制
    send(jsonObject);
    log.info(">>>使用MQ消息确认机制投递到MQ中失败");
  }
}
2.2.3 消费者
/**
 * description: 库存消费者
 * create by: YangLinWei
 * create time: 2020/5/26 10:59 上午
 */
@Component
@Slf4j
public class StockConsumer {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private OrderMapper orderMapper;
    @RabbitListener(queues = "modify_inventory_queue")
    @Transactional
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        log.info(">>>messageId:{},msg:{}", messageId, msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        // 1.获取秒杀id
        Long seckillId = jsonObject.getLong("seckillId");
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            log.warn("seckillId:{},商品信息不存在!", seckillId);
            basicNack(message, channel);
            return;
        }
        Long version = seckillEntity.getVersion();
        int inventoryDeduction = seckillMapper.optimisticDeduction(seckillId, version);
        if (!toDaoResult(inventoryDeduction)) {
            log.info(">>>seckillId:{}修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", seckillId, inventoryDeduction);
            basicNack(message, channel);
            return;
        }
        // 2.添加秒杀订单
        OrderEntity orderEntity = new OrderEntity();
        String phone = jsonObject.getString("phone");
        orderEntity.setUserPhone(phone);
        orderEntity.setSeckillId(seckillId);
        orderEntity.setState(1l);
        int insertOrder = orderMapper.insertOrder(orderEntity);
        if (!toDaoResult(insertOrder)) {
            basicNack(message, channel);
            return;
        }
        log.info(">>>修改库存成功seckillId:{}>>>>inventoryDeduction返回为{} 秒杀成功", seckillId, inventoryDeduction);
        basicNack(message, channel);
    }
    // 调用数据库层判断
    public Boolean toDaoResult(int result) {
        return result > 0 ? true : false;
    }
    // 消费者获取到消息之后 手动签收 通知MQ删除该消息
    private void basicNack(Message message, Channel channel) throws IOException {
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
    }
}

2.3 用户查询接口

@RestController
public class OrderSeckillServiceImpl extends BaseApiService<JSONObject> implements OrderSeckillService {
  @Autowired
  private OrderMapper orderMapper;
  @Override
  public BaseResponse<JSONObject> getOrder(String phone, Long seckillId) {
    if (StringUtils.isEmpty(phone)) {
      return setResultError("手机号码不能为空!");
    }
    if (seckillId == null) {
      return setResultError("商品库存id不能为空!");
    }
    OrderEntity orderEntity = orderMapper.findByOrder(phone, seckillId);
    if (orderEntity == null) {
      return setResultError("正在排队中.....");
    }
    return setResultSuccess("恭喜你秒杀成功!");
  }
}

3. 测试

①模拟用户修改商品库存,更新令牌桶,浏览器访问:http://localhost:9800/addSpikeToken?seckillId=100001&tokenQuantity=100

可以看到Redis里生成商品key id为100001,值为list,大小为100的集合:

②模拟抢购,浏览器访问:http://localhost:9800/spike?phone=13800000001&seckillId=100001

可以看到数据库库存减一:

订单并生成了一条记录:

Redis减少了一个令牌:

③模拟用户查询抢购结果,浏览器访问:

目录
相关文章
|
API
在vite.config.js 配置代理
在vite.config.js 配置代理
2137 2
|
缓存
npm修改全局包安装路径
npm修改全局包安装路径
539 0
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
3747 7
|
9月前
|
人工智能 智能硬件
吃碗面的功夫,我随口用“灵光”搓出一个有8大功能的app,还不止(含三大应用解锁)
法思诺创新学院关注AI赋能实体产业。本文通过体验蚂蚁集团“灵光”AI,展示其快速生成英语学习APP、智能音箱界面及3D模型的能力,尤其“闪应用”与“灵光开眼”在企业研发、培训中的潜力,探讨AI从虚拟走向物理世界的关键突破。
1425 2
|
弹性计算 固态存储 大数据
2024阿里云服务器租用价格表(一年/按月/按小时报价明细)
阿里云服务器2024年最新租用价格表显示,轻量应用服务器2核2G3M带宽一年82元(约6.8元/月),2核4G4M带宽轻量服务器一年298元。新老用户共享99元一年的2核2G3M带宽ECS经济型e实例服务器与199元一年的企业专享2核4G5M带宽ECS u1实例服务器优惠。4核16G10M带宽游戏服务器70元/月,8核32G10M带宽160元/月。GPU服务器如gn6v和gn6i等提供新用户专享折扣。续费折扣方面,续费一年享有7.5折,续费五年则有3折优惠。按小时计费的云服务器ECS实例中,如ecs.u1-c1m4.large(2核8G)每小时0.45元。
37364 17
|
消息中间件 Java Kafka
Kafka【环境搭建 01】kafka_2.12-2.6.0 单机版安装+参数配置及说明+添加到service服务+开机启动配置+验证+chkconfig配置说明(一篇入门kafka)
【2月更文挑战第19天】Kafka【环境搭建 01】kafka_2.12-2.6.0 单机版安装+参数配置及说明+添加到service服务+开机启动配置+验证+chkconfig配置说明(一篇入门kafka)
1481 1
|
人工智能 关系型数据库 分布式数据库
100%兼容MySQL!手把手教你基于PolarDB搭建RAG系统
100%兼容MySQL!手把手教你基于PolarDB搭建RAG系统
957 0
|
存储 Java easyexcel
招行面试:100万级别数据的Excel,如何秒级导入到数据库?
本文由40岁老架构师尼恩撰写,分享了应对招商银行Java后端面试绝命12题的经验。文章详细介绍了如何通过系统化准备,在面试中展示强大的技术实力。针对百万级数据的Excel导入难题,尼恩推荐使用阿里巴巴开源的EasyExcel框架,并结合高性能分片读取、Disruptor队列缓冲和高并发批量写入的架构方案,实现高效的数据处理。此外,文章还提供了完整的代码示例和配置说明,帮助读者快速掌握相关技能。建议读者参考《尼恩Java面试宝典PDF》进行系统化刷题,提升面试竞争力。关注公众号【技术自由圈】可获取更多技术资源和指导。
|
SQL 关系型数据库 MySQL
详解MySQL覆盖索引、索引下推
1.覆盖索引 1.1.概述 覆盖索引,是为了避免“回表查询”,从而降低查询耗时的一种使用索引的方法,所以要聊覆盖索引首先我们要知道什么是"回表查询,“回表查询”是因为MySQL的索引结构决定的,是因为非聚集索引要找聚集索引拿数据而出现的现象,所以我们又要先了解MySQL中的聚集索引和非聚集索引。 文章的脉络就是先聊聚集索引、非聚集索引是怎么带来了“回表查询”的问题,然后怎么用用覆盖索引解决这个问题。
2717 0
|
Kubernetes Java 开发工具
Kubernetes部署项目流程(新手上线新版本服务整个流程)
【8月更文挑战第1天】Kubernetes(k8s)新手上线新版本服务整个流程
867 5