• 秒杀微服务实现抢购代金券功能


    需求分析

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

    秒杀场景的解决方案

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

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

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

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    抢购活动表

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    订单表

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建秒杀服务

    pom依赖

    引入相关依赖如下:

        <dependencies>
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
            dependency>
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
            
            <dependency>
                <groupId>com.zjqgroupId>
                <artifactId>commonsartifactId>
                <version>1.0-SNAPSHOTversion>
            dependency>
            <dependency>
                <groupId>org.redissongroupId>
                <artifactId>redisson-spring-boot-starterartifactId>
                <version>3.13.6version>
            dependency>
        dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    配置文件

    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: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

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

    相关实体引入

    抢购代金券活动信息

    image.png

    代金券订单信息

    image.png

    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;
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    全局异常处理

    /**
     * 
     * 全局异常处理类
     * @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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    添加代金券秒杀活动

    代金券活动实体

    上述已引入实体。

    代金券活动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);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    代金券活动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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    验证数据库表 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    接口测试

    对抢购的代金券下单

    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, access_token, request.getServletPath());
            return resultInfo;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    SeckillService
        /**
         * 抢购代金券
         *
         * @param voucherId   代金券 ID
         * @param accessToken 登录token
         * @Para path 访问路径
         */
        public ResultInfo doSeckill(Integer voucherId, String accessToken, String path) {
            // 基本参数校验
            AssertUtil.isTrue(voucherId == null || voucherId < 0, "请选择需要抢购的代金券");
            AssertUtil.isNotEmpty(accessToken, "请登录");
            // 判断此代金券是否加入抢购
            SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId);
            AssertUtil.isTrue(seckillVouchers == null, "该代金券并未有抢购活动");
            // 判断是否有效
            AssertUtil.isTrue(seckillVouchers.getIsValid() == 0, "该活动已结束");
            // 判断是否开始、结束
            Date now = new Date();
            AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()), "该抢购还未开始");
            AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "该抢购已结束");
            // 判断是否卖完
            AssertUtil.isTrue(seckillVouchers.getAmount() < 1, "该券已经卖完了");
            // 获取登录用户信息
            String url = oauthServerName + "user/me?access_token={accessToken}";
            ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
            if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
                resultInfo.setPath(path);
                return resultInfo;
            }
            // 这里的data是一个LinkedHashMap,SignInDinerInfo
            SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                    new SignInDinerInfo(), false);
            // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
            VoucherOrders order = voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),
                    seckillVouchers.getId());
            AssertUtil.isTrue(order != null, "该用户已抢到该代金券,无需再抢");
            // 扣库存
            int count = seckillVouchersMapper.stockDecrease(seckillVouchers.getId());
            AssertUtil.isTrue(count == 0, "该券已经卖完了");
            // 下单
            VoucherOrders voucherOrders = new VoucherOrders();
            voucherOrders.setFkDinerId(dinerInfo.getId());
            voucherOrders.setFkSeckillId(seckillVouchers.getId());
            voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());
            String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
            voucherOrders.setOrderNo(orderNo);
            voucherOrders.setOrderType(1);
            voucherOrders.setStatus(0);
            count = voucherOrdersMapper.save(voucherOrders);
            AssertUtil.isTrue(count == 0, "用户抢购失败");
    
            return ResultInfoUtil.buildSuccess(path, "抢购成功");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    代金券订单 VoucherOrdersMapper
    /**
     * 代金券订单 Mapper
     * @author zjq
     */
    public interface VoucherOrdersMapper {
    
        /**
         * 根据用户 ID 和秒杀 ID 查询代金券订单
         * @param userId
         * @param voucherId
         * @return
         */
        @Select("select id, order_no, fk_voucher_id, fk_diner_id, qrcode, payment," +
                " status, fk_seckill_id, order_type, create_date, update_date, " +
                " is_valid from t_voucher_orders where fk_diner_id = #{userId} " +
                " and fk_voucher_id = #{voucherId} and is_valid = 1 and status between 0 and 1 ")
        VoucherOrders findDinerOrder(@Param("userId") Integer userId,
                                     @Param("voucherId") Integer voucherId);
    
        /**
         * 新增代金券订单
         * @param voucherOrders 代金券实体
         * @return
         */
        @Insert("insert into t_voucher_orders (order_no, fk_voucher_id, fk_diner_id, " +
                " status, fk_seckill_id, order_type, create_date, update_date,  is_valid)" +
                " values (#{orderNo}, #{fkVoucherId}, #{fkDinerId}, #{status}, #{fkSeckillId}, " +
                " #{orderType}, now(), now(), 1)")
        int save(VoucherOrders voucherOrders);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    秒杀代金券活动 SeckillVouchersMapper
        /**
         * 减库存
         * @param seckillId 秒杀id
         * @return
         */
        @Update("update t_seckill_vouchers set amount = amount - 1 " +
                " where id = #{seckillId}")
        int stockDecrease(@Param("seckillId") int seckillId);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    测试验证

    压力测试

    下载安装JMeter

    JMeter安装和使用可以参考我这篇文章:压力测试工具-JMeter安装和使用

    初始化2000个用户数据

    在这里插入图片描述
    数据库新增2000个用户数据,账号为test0到test1999,密码统一设置为123456。

    认证微服务生产2000个token

    初始化2000个token信息,存储在token.txt文件中。
    代码如下:

        @Test
        public void writeToken() throws Exception {
            String authorization = Base64Utils.encodeToString("appId:123456".getBytes());
            StringBuffer tokens = new StringBuffer();
            for (int i = 0; i < 2000; i++) {
                MvcResult mvcResult = super.mockMvc.perform(MockMvcRequestBuilders.post("/oauth/token")
                        .header("Authorization", "Basic " + authorization)
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                        .param("username", "test" + i)
                        .param("password", "123456")
                        .param("grant_type", "password")
                        .param("scope", "api")
                )
                        .andExpect(status().isOk())
                        // .andDo(print())
                        .andReturn();
                String contentAsString = mvcResult.getResponse().getContentAsString();
                ResultInfo resultInfo = (ResultInfo) JSONUtil.toBean(contentAsString, ResultInfo.class);
                JSONObject result = (JSONObject) resultInfo.getData();
                String token = result.getStr("accessToken");
                tokens.append(token).append("\r\n");
            }
    
            Files.write(Paths.get("tokens.txt"), tokens.toString().getBytes());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    测试多人抢购代金券

    添加一个代金券抢购活动信息:
    在这里插入图片描述
    通过jmeter添加用户测试计划,3000个线程同时发起两千个用户执行测试:
    在这里插入图片描述
    image.png
    测试后结果如下:
    image.png
    可以看到有些请求是失败的,因为没有做优化,抗不了这么大的并发。然后查看数据库情况发现库存已经超卖,100个库存,卖了230单,库存成了负数😰😰😰。
    image.png
    image.png

    测试同一用户抢购多次代金券

    重置数据库数据后,测试同一个用户,1000个线程发起并发请求。
    在这里插入图片描述
    image.png
    查看数据库发现这一个用户就下了10单。。。
    在这里插入图片描述

    很明显出现了超卖和同一个用户可以多次抢购同一代金券的问题,再后续博客中我会提供基于Redis来解决超卖和同一用户多次抢购的问题。

    本文内容到此结束了,
    如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
    如有错误❌疑问💬欢迎各位指出。
    主页共饮一杯无的博客汇总👨‍💻

    保持热爱,奔赴下一场山海。🏃🏃🏃

  • 相关阅读:
    HCIP---企业网的三层架构
    按规划安装配置DNS及NSX Advanced Load Balance(AVI)
    记录eclipse revert resources (0%)
    yolov7配置与训练记录(一)
    基于FreeRTOS编写ESP8266的AT命令收发解析器
    什么是快速失败(fail-fast)和安全失败(fail-safe)?
    程序员们保住自己饭碗
    全网最全谷粒商城记录_06、环境-使用vagrant快速创建linux虚拟机——2、vagrant镜像仓库、下载、安装、验证
    nuxt 如何生成sitemap.xml 动静态站点地图
    rocky(centos) 安装redis,并设置开机自启动
  • 原文地址:https://blog.csdn.net/qq_35427589/article/details/128062138