秒杀微服务实现抢购代金券功能(代码片段)

共饮一杯无 共饮一杯无     2023-03-27     434

关键词:

文章目录

需求分析

现在日常购物或者餐饮消费,商家经常会有推出代金券功能,有些时候代金券的数量不多是需要抢购的,那么怎么设计可以保证代金券的消耗量和秒杀到的用户保持一致呢?怎么设计可以保证一个用户只能秒杀到一张代金券呢?

秒杀场景的解决方案

秒杀场景有以下几个特点:

  • 大量用户同时进行抢购操作,系统流量激增,服务器瞬时压力很大;
  • 请求数量远大于商品库存量,只有少数客户可以成功抢购;
  • 业务流程不复杂,核心功能是下订单。

秒杀场景的应对,一般要从以下几个方面进行处理,如下:

  1. 限流:从客户端层面考虑,限制单个客户抢购频率;服务端层面,加强校验,识别请求是否来源于真实的客户端,并限制请求频率,防止恶意刷单;应用层面,可以使用漏桶算法或令牌桶算法实现应用级限流。
  2. 缓存:热点数据都从缓存获得,尽可能减小数据库的访问压力;
  3. 异步:客户抢购成功后立即返回响应,之后通过消息队列,异步处理后续步骤,如发短信、更新数据库等,从而缓解服务器峰值压力。
  4. 分流:单台服务器肯定无法应对抢购期间大量请求造成的压力,需要集群部署服务器,通过负载均衡共同处理客户端请求,分散压力。

数据库表设计

本文以抢购代金券为例,来进行数据库表的设计。

代金券表

CREATE TABLE `t_voucher`  (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '代金券标题',
  `thumbnail` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '缩略图',
  `amount` int(11) NULL DEFAULT NULL COMMENT '抵扣金额',
  `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '售价',
  `status` int(10) NULL DEFAULT NULL COMMENT '-1=过期 0=下架 1=上架',
  `expire_time` datetime(0) NULL DEFAULT NULL COMMENT '过期时间',
  `redeem_restaurant_id` int(10) NULL DEFAULT NULL COMMENT '验证餐厅',
  `stock` int(11) NULL DEFAULT 0 COMMENT '库存',
  `stock_left` int(11) NULL DEFAULT 0 COMMENT '剩余数量',
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述信息',
  `clause` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '使用条款',
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  `is_valid` tinyint(1) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

抢购活动表

CREATE TABLE `t_seckill_vouchers`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `fk_voucher_id` int(11) NULL DEFAULT NULL,
  `amount` int(11) NULL DEFAULT NULL,
  `start_time` datetime(0) NULL DEFAULT NULL,
  `end_time` datetime(0) NULL DEFAULT NULL,
  `is_valid` int(11) NULL DEFAULT NULL,
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

订单表

CREATE TABLE `t_voucher_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` int(11) NULL DEFAULT NULL,
  `fk_voucher_id` int(11) NULL DEFAULT NULL,
  `fk_diner_id` int(11) NULL DEFAULT NULL,
  `qrcode` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图片地址',
  `payment` tinyint(4) NULL DEFAULT NULL COMMENT '0=微信支付 1=支付宝支付',
  `status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
  `fk_seckill_id` int(11) NULL DEFAULT NULL COMMENT '如果是抢购订单时,抢购订单的id',
  `order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  `is_valid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

创建秒杀服务

pom依赖

引入相关依赖如下:

    <dependencies>
        <!-- eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- spring data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons -->
        <dependency>
            <groupId>com.zjq</groupId>
            <artifactId>commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.6</version>
        </dependency>
    </dependencies>

配置文件

server:
  port: 7003 # 端口

spring:
  application:
    name: ms-seckill # 应用名
  # 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
  # Redis
  redis:
    port: 6379
    host: localhost
    timeout: 3000
    password: 123456
  # Swagger
  swagger:
    base-package: com.zjq.seckill
    title: 秒杀微服务API接口文档

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true
    instance-id: $spring.cloud.client.ip-address:$server.port
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰映射

service:
  name:
    ms-oauth-server: http://ms-oauth2-server/

logging:
  pattern:
    console: '%dHH:mm:ss [%thread] %-5level %logger50 - %msg%n'

关系型数据库实现代金券秒杀

相关实体引入

抢购代金券活动信息

代金券订单信息

Rest配置类

/**
 * RestTemplate 配置类
 * @author zjq
 */
@Configuration
public class RestTemplateConfiguration 

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() 
        RestTemplate restTemplate = new RestTemplate();
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
        restTemplate.getMessageConverters().add(converter);
        return restTemplate;
    
    

全局异常处理

/**
 * 
 * 全局异常处理类
 * @author zjq
 */
// 将输出的内容写入 ResponseBody 中
@RestControllerAdvice 
@Slf4j
public class GlobalExceptionHandler 

    @Resource
    private HttpServletRequest request;

    @ExceptionHandler(ParameterException.class)
    public ResultInfo<Map<String, String>> handlerParameterException(ParameterException ex) 
        String path = request.getRequestURI();
        ResultInfo<Map<String, String>> resultInfo =
                ResultInfoUtil.buildError(ex.getErrorCode(), ex.getMessage(), path);
        return resultInfo;
    

    @ExceptionHandler(Exception.class)
    public ResultInfo<Map<String, String>> handlerException(Exception ex) 
        log.info("未知异常:", ex);
        String path = request.getRequestURI();
        ResultInfo<Map<String, String>> resultInfo =
                ResultInfoUtil.buildError(path);
        return resultInfo;
    


添加代金券秒杀活动

代金券活动实体

上述已引入实体。

代金券活动Mapper->SeckillVouchersMapper

/**
 * 秒杀代金券 Mapper
 * @author zjq
 */
public interface SeckillVouchersMapper 

    /**
     * 新增秒杀活动
     * @param seckillVouchers 代金券实体
     * @return
     */
    @Insert("insert into t_seckill_vouchers (fk_voucher_id, amount, start_time, end_time, is_valid, create_date, update_date) " +
            " values (#fkVoucherId, #amount, #startTime, #endTime, 1, now(), now())")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int save(SeckillVouchers seckillVouchers);

    /**
     * 根据代金券 ID 查询该代金券是否参与抢购活动
     * @param voucherId 代金券id
     * @return
     */
    @Select("select id, fk_voucher_id, amount, start_time, end_time, is_valid " +
            " from t_seckill_vouchers where fk_voucher_id = #voucherId")
    SeckillVouchers selectVoucher(Integer voucherId);


代金券活动Service->SeckillService

/**
 * 秒杀业务逻辑层
 * @author zjq
 */
@Service
public class SeckillService 

    @Resource
    private SeckillVouchersMapper seckillVouchersMapper;

    /**
     * 添加需要抢购的代金券
     *
     * @param seckillVouchers
     */
    @Transactional(rollbackFor = Exception.class)
    public void addSeckillVouchers(SeckillVouchers seckillVouchers) 
        // 非空校验
        AssertUtil.isTrue(seckillVouchers.getFkVoucherId() == null, "请选择需要抢购的代金券");
        AssertUtil.isTrue(seckillVouchers.getAmount() == 0, "请输入抢购总数量");
        Date now = new Date();
        AssertUtil.isNotNull(seckillVouchers.getStartTime(), "请输入开始时间");
        // 生产环境下面一行代码需放行,这里注释方便测试
        // AssertUtil.isTrue(now.after(seckillVouchers.getStartTime()), "开始时间不能早于当前时间");
        AssertUtil.isNotNull(seckillVouchers.getEndTime(), "请输入结束时间");
        AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "结束时间不能早于当前时间");
        AssertUtil.isTrue(seckillVouchers.getStartTime().after(seckillVouchers.getEndTime()), "开始时间不能晚于结束时间");

        // 验证数据库中是否已经存在该券的秒杀活动
         SeckillVouchers seckillVouchersFromDb = seckillVouchersMapper.selectVoucher(seckillVouchers.getFkVoucherId());
         AssertUtil.isTrue(seckillVouchersFromDb != null, "该券已经拥有了抢购活动");
//         插入数据库
         seckillVouchersMapper.save(seckillVouchers);
    


验证数据库表 t_seckill_vouchers 中是否已经存在该券的秒杀活动:

  • 如果存在则抛出异常;
  • 如果不存在则将添加一个代金券抢购活动到 t_seckill_vouchers 表中;

代金券活动Controller->SeckillController

在网关微服务中配置秒杀服务路由和白名单方向

spring:
  application:
    name: ms-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启配置注册中心进行路由功能
          lower-case-service-id: true # 将服务名称转小写
      routes:
        - id: ms-seckill
          uri: lb://ms-seckill
          predicates:
            - Path=/seckill/**
          filters:
            - StripPrefix=1
            
secure:
  ignore:
    urls: # 配置白名单路径
      # 内部配置所以放行
      - /seckill/add

接口测试

对抢购的代金券下单

SeckillController

    /**
     * 秒杀下单
     *
     * @param voucherId 代金券id
     * @param access_token 请求token
     * @return
     */
    @PostMapping("voucherId")
    public ResultInfo<String> doSeckill(@PathVariable Integer voucherId, String access_token) 
        ResultInfo resultInfo = seckillService.doSeckill(voucherId,redis实现高并发下的抢购/秒杀功能

...次抽空整理下实际场景中的具体代码逻辑实现吧:抢购/秒杀 查看详情

php和redis实现在高并发下的抢购及秒杀功能示例详解(代码片段)

抢购、秒杀是平常很常见的场景,面试的时候面试官也经常会问到,比如问你淘宝中的抢购秒杀是怎么实现的等等。抢购、秒杀实现很简单,但是有些问题需要解决,主要针对两个问题:一、高并发对数据库产生的压力二、竞争... 查看详情

netcore微服务实现事务一致性masstransit之saga使用(代码片段)

demo如下,一个订单处理的小例子:首先看看结果很简单:核心代码如下:usingMassTransit;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingOrderProcessor.Event;usingServiceModel;usingServiceModel.Comman 查看详情

javaredis实现抢购秒杀

2018.10.24今天研究了下抢购秒杀的功能实现网上查了一大堆用redis的最多。主要是通过redis的watchmulti事务来控制秒杀数量不超卖。这里说下自己的感受:不超卖的话那就要一个个的来减库存这样的话效率上会有点问题这里上下代码... 查看详情

秒杀系统的设计与实现(限时抢购抢救接口单用户限制实现)(代码片段)

...设计的系统还有一些问题:我们应该在一定的时间内执行秒杀处理,不能再任意时间都接受秒杀请求。如何加入时间验证?对于稍微懂点电脑的,又会动歪脑筋的人来说开始通过抓包方式获取我们的接口地址。然后通过脚本进行... 查看详情

go语言实战(15)gin+grpc微服务实现备忘录(下)|备忘录模块(代码片段)

文章目录写在前面源码地址:1.备忘录部分1.1proto模块定义1.2数据库结构定义1.3接入ETCD2.网关接入2.1服务接入2.2controller3.测试返回写在前面和上篇文章类似,这次我们完成具体功能部分。源码地址:https://github.com/Cocaine... 查看详情

redis实现高并发下的抢购秒杀功能

博主最近在项目中遇到了抢购问题!现在分享下。抢购、秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个:1高并发对数据库产生的压力2竞争状态下如何解决库存的正确减少("超卖"问题)对于第一个问题,已经... 查看详情

go语言实战(14)gin+grpc微服务实现备忘录(上)|用户模块(代码片段)

写在前面介于很多同学让我出一下关于gRPC的内容,我就用gRPC把备忘录重新做一遍。源码地址:https://github.com/CocaineCong/gRPC-todoList1.安装部分1.1安装gRPCgogetgoogle.golang.org/grpcgogetgoogle.golang.org/protobuf1.2安装protoc可用于通讯协议... 查看详情

使用springcloudconfig统一管理微服务配置(代码片段)

使用SpringCloudConfig统一管理微服务配置为什么要统一管理微服务配置对于传统的单体应用,常使用配置文件管理所有配置。例如一个SpringBoot开发的单体应用,可将配置内容放在application.yml文件中。如果需要切换环境,... 查看详情

apollo微服务配置中心详解(代码片段)

Apollo微服务配置中心详解前言一、Apollo架构(一)简介(二)角色介绍(三)服务端实现(四)客服端实现二、Apollo部署(一)准备数据库(二)配置服务1.手动部署(1)Confi... 查看详情

微服务实践之网关(springcloudgateway)详解-springcloud(2021.0.x)-3(代码片段)

...业目的注明出处可自由转载出自:shusheng007系列文章微服务实践之服务注册与发现(Nacos)-SpringCloud(2020.0.x)-1微服务实践之负载均衡(SpringCloudLoadBalancer)-SpringCloud(2020.0.x)-2概述本文将介绍微服务架构中的SpringCloudGateway这... 查看详情

使用gitee管理微服务配置文件(代码片段)

使用gitee管理微服务配置文件简介在gitee上创建服务提供者的配置文件搭建配置中心微服务获取配置中心配置简介在分布式系统中,由于服务数量非常多,配置文件分散在不同的微服务项目中,管理不方便。为了方便... 查看详情

秒杀/抢购架构设计(代码片段)

1秒杀业务分析1.1正常电子商务流程(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货1.2秒杀业务的特性( 查看详情

python实现秒杀抢购某宝商品,不再害怕双十一抢不到了(代码片段)

前言马上就要双十一咯,给你们展示一下我在618干的大事,直接用Python抢购商品今天就来分享给你们吧这又快要到付尾款的日子咯,有些哥们需要送礼物给对象的,赶紧买这些预售的商品吧,听说今年预售的... 查看详情

淘宝自动抢购(代码片段)

#!/usr/bin/envpython‘‘‘作者:张铭达功能:淘宝秒杀购物版本:0.1日期2019-06-16‘‘‘fromseleniumimportwebdriverimporttime,datetimedriver=webdriver.Chrome()driver.maximize_window()username=‘张铭达33333‘classTaoBao(object):def__in 查看详情

redis轻松实现秒杀系统(代码片段)

点击关注公众号,实用技术文章及时了解什么是秒杀秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售... 查看详情

redis轻松实现秒杀系统(代码片段)

点击关注公众号,实用技术文章及时了解什么是秒杀秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售... 查看详情

websocketpp的流媒体微服务使用安全(代码片段)

...们在使用媒体服务的过程中,可以使用websocketpp制作微服务,提供tcpstream流服务长链接系统,最大的两个问题1是记录客户端数据,2是安全问题。这里使用websocket的原因是提供流媒体服务。鉴权安全性:  f 查看详情