初试并发秒杀功能

因为一直对高并发的处理感兴趣,所以这段时间搜集了很多贴子,经过学习和个人的分析,我觉得这位大神的分享是比较思路清晰和能够上手的,不过大神有部分实际用到的东西没有补充,我这里就补充和记录下自己的实现过程啦。

大神的贴子:实战高并发秒杀实现(2):防止库存超卖问题(超详细)_RuiKe的博客-CSDN博客_库存超卖

pom引入:

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <mybatis-plus-boot-starter.version>3.0.6</mybatis-plus-boot-starter.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- 导入mybatis-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-annotation</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <!--在引用时请在maven中央仓库搜索最新版本号-->
            <version>2.0.8</version>
        </dependency>
        <!--rabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- hystrix断路器 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
        <!--工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>
    </dependencies>

Application:

@SpringBootApplication
@ServletComponentScan
@EnableHystrix
@EnableRabbit
public class StoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(StoreApplication.class, args);
    }

}

application.yml(部分需要自行修改的就不多说啦):

server:
  port: 8082

spring:
  profiles:
    active: local
  application:
    name: store

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  typeAliasesPackage: com.hyz.store.bean.entity
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
    refresh: true
  configuration:
    cache-enabled: false
    map-underscore-to-camel-case: false

application-local.yml:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_feign?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&useSSL=false&zeroDateTimeBehavior=convertToNull
    username: root
    password: 1234
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    #确认消息已发送到交换机(Exchange)
    publisher-confirm-type: correlated
    #确认消息已发送到队列(Queue)
    publisher-returns: true
    #设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
    template:
      mandatory: true
      #是否支持重试
      retry:
        enabled: false
        #最大重试次数
        max-attempts: 5
        #最大重试时间
        max-interval: 1200
    #采用手动应答
    listener:
      simple:
        acknowledge-mode: manual
        #是否支持重试
        retry:
          enabled: false
          #最大重试次数
          max-attempts: 5
          #最大重试时间
          max-interval: 1200
  redis:
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 200
        max-wait: -1
        max-idle: 10
        min-idle: 0
    timeout: 1000

接下来正式开始,按照大神的思路,是需要先在redis中生成对应商品对应数量的秒杀token的,因此先实现token生成:

redis工具类-RedisUtil(这里为了方便只展示用到的方法,更全面的可以网上找):

import java.util.*;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public StringRedisTemplate getRedisTemplate() {
        return this.redisTemplate;
    }

    /**
     *
     * @param key
     * @param value
     * @return
     */
    public Long lLeftPushAll(String key, Collection<String> value) {
        return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /**
     * 移出并获取列表的第一个元素
     *
     * @param key
     * @return 删除的元素
     */
    public String lLeftPop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }
}

token生成类-GenerateToken:

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;


@Component
public class GenerateToken {

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 批量生成token
     * @param keyPrefix
     *            令牌key前缀
     */
    public Long createListToken(String keyPrefix, String key, Long quantity){
        List<String> values=new ArrayList<>();
        System.out.println("生成token:");
        for(int i=0;i<quantity;i++){
            String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
            System.out.println(token);
            values.add(token);
        }
        return redisUtil.lLeftPushAll(keyPrefix + key,values);
    }

    /**
     * 获取并移除list中的token
     */
    public String getListKeyToken(String keyPrefix, String key){
        return redisUtil.lLeftPop(keyPrefix + key);
    }

}

以上token生成准备好,接下来可以弄主体秒杀功能:

数据表按照分享贴子的生成(修改了部分):

CREATE TABLE `store_order` (
  `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',
  `user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
  `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',
  `createAt` datetime NOT NULL COMMENT '创建时间',
  KEY `idx_createAt` (`createAt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

CREATE TABLE `store_seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',
  `inventory` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime NOT NULL COMMENT '秒杀开启时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `createAt` datetime NOT NULL COMMENT '创建时间',
  `version` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';

entity:

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author wongChil
 * @date 2022/1/13 17:58
 */
@Data
public class StoreOrder {
    @TableField("seckill_id")
    private Long seckillId;

    @TableField("user_phone")
    private String userPhone;

    private Integer state;

    @TableField(value = "createAt", fill = FieldFill.INSERT)
    private LocalDateTime createAt;
}
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.annotation.Id;

import java.time.LocalDateTime;

/**
 * @author wongChil
 * @date 2022/1/13 15:27
 */
@Data
public class StoreSeckill {
    @Id
    @TableId(value = "seckill_id",type = IdType.AUTO)
    private Long seckillId;

    private String name;

    private int inventory;

    private LocalDateTime startTime;

    private LocalDateTime endTime;

    @TableField(value = "createAt", fill = FieldFill.INSERT)
    private LocalDateTime createAt;

    private Long version;
}

mapper:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hyz.store.bean.entity.StoreSeckill;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * @author wongChil
 * @date 2022/1/13 15:34
 */
@Mapper
public interface StoreSeckillMapper extends BaseMapper<StoreSeckill> {

    /**
     * 基于版本号形式实现乐观锁
     * @param seckillId
     * @return
     */
    @Update("update store_seckill set inventory=inventory-1 ,version=version+1 where  seckill_id=#{seckillId} and version=#{version} and inventory>0;")
    int optimisticVersionSeckill(@Param("seckillId") Long seckillId, @Param("version") Long version);


    @Select("SELECT seckill_id AS seckillId,name as name,inventory as inventory,start_time as startTime,end_time as endTime,createAt,version as version from store_seckill where seckill_id=#{seckillId}")
    StoreSeckill findBySeckillId(Long seckillId);

}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hyz.store.bean.entity.StoreOrder;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author wongChil
 * @date 2022/1/13 18:00
 */
@Mapper
public interface StoreOrderMapper extends BaseMapper<StoreOrder> {
}

service:

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hyz.store.bean.entity.StoreOrder;
import com.hyz.store.bean.entity.StoreSeckill;
import com.hyz.store.mapper.StoreSeckillMapper;
import com.hyz.store.util.GenerateToken;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

/**
 * @author wongChil
 * @date 2022/1/13 15:41
 */
@Service
public class StoreSeckillService extends ServiceImpl<StoreSeckillMapper, StoreSeckill> {
    @Autowired
    private StoreSeckillMapper storeSeckillMapper;
    @Autowired
    private GenerateToken generateToken;
    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 秒杀服务
     */
    @Transactional
    @HystrixCommand(fallbackMethod = "spikeFallback")
    public String spike(Long seckillId){
        return generateToken.getListKeyToken("seckill_",seckillId.toString());
    }


    /**
     * 获取到秒杀token之后,异步放入mq中实现修改商品的库存
     */
    @Async
    public void sendSeckillMsg(Long seckillId, String phone) {
        StoreOrder storeOrder=new StoreOrder();
        storeOrder.setSeckillId(seckillId);
        storeOrder.setUserPhone(phone);
        String orderId = UUID.randomUUID().toString();
        String jsonString = JSONUtil.toJsonStr(storeOrder);
        Message messages = MessageBuilder.withBody(jsonString.getBytes())
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
                .setMessageId(orderId)
                .build();
        rabbitTemplate.convertAndSend("storeExchange","store.seckill",messages);
    }

    /**
     * 批量生成秒杀token
     */
    @Async
    public Long createSeckillToken(Long seckillId, Long tokenQuantity) {
        return generateToken.createListToken("seckill_", seckillId.toString(), tokenQuantity);
    }

    public StoreSeckill findBySeckillId(Long seckillId){
        return storeSeckillMapper.findBySeckillId(seckillId);
    }

    /**
     * 调用服务异常执行方法
     * */
    private String spikeFallback(Long seckillId){
        return "";
    }


}

rabbitmq:

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hyz.store.bean.entity.StoreOrder;
import com.hyz.store.bean.entity.StoreSeckill;
import com.hyz.store.mapper.StoreSeckillMapper;
import com.hyz.store.service.StoreOrderService;
import com.hyz.store.service.StoreSeckillService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author wongChil
 * @date 2022/1/13 17:16
 */
@Component
public class StoreSeckillReceiver {

    @Autowired
    StoreOrderService storeOrderService;
    @Autowired
    StoreSeckillService storeSeckillService;
    @Autowired
    StoreSeckillMapper storeSeckillMapper;

    @RabbitListener(bindings = @QueueBinding(value = @Queue(value = "store.seckill"),
            exchange = @Exchange(value = "storeExchange",type = ExchangeTypes.TOPIC),key = "store.seckill"))
    public void process(Message message, Channel channel) throws IOException {
        try{
            String msg = new String(message.getBody(), "UTF-8");
            JSONObject jsonObject = JSONUtil.parseObj(msg);
            StoreOrder storeOrder=JSONUtil.toBean(jsonObject,StoreOrder.class);
            System.out.println(storeOrder.getUserPhone());
            storeOrder.setState(0);
            storeOrderService.save(storeOrder);
            StoreSeckill storeSeckill=storeSeckillService.findBySeckillId(storeOrder.getSeckillId());
            storeSeckillMapper.optimisticVersionSeckill(storeOrder.getSeckillId(),storeSeckill.getVersion());

            //消息确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
        catch (Exception e){
            //失败后消息被确认,
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        }

    }
}

controller(swagger没有使用的话可以删掉部分代码):

import com.hyz.store.bean.entity.StoreSeckill;
import com.hyz.store.service.StoreSeckillService;
import com.hyz.store.util.Result;
import com.hyz.store.util.ResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author wongChil
 * @date 2022/1/13 17:34
 */
@Api(tags = "秒杀接口")
@RestController
@RequestMapping("seckill")
public class StoreSeckillController {
    @Autowired
    private StoreSeckillService storeSeckillService;

    @ApiOperation("秒杀")
    @GetMapping("spike")
    public Result spike(@RequestParam("seckillId") Long seckillId ,@RequestParam("phone") String phone){
        String seckillToken=storeSeckillService.spike(seckillId);
        if (StringUtils.isEmpty(seckillToken)) {
            return ResultUtil.exception("亲,该秒杀已经售空,请下次再来!");
        }
        storeSeckillService.sendSeckillMsg(seckillId,phone);
        return ResultUtil.success(seckillToken);
    }

    @ApiOperation("生成秒杀令牌")
    @GetMapping("createToken")
    public Result createSeckillToken(@RequestParam("seckillId") Long seckillId, @RequestParam("tokenQuantity") Long tokenQuantity){
        StoreSeckill storeSeckill=storeSeckillService.findBySeckillId(seckillId);
        if(storeSeckill==null){
            return ResultUtil.exception("商品信息不存在!");
        }
        Long num=storeSeckillService.createSeckillToken(seckillId,tokenQuantity);
        return ResultUtil.success("令牌生成成功,生成数量为:"+num+"个");
    }
}

以上都处理好后,可以正式开始测试了。我这里采用的是Jmeter进行并发测试。

加入商品数据:

 生成1个秒杀商品的token:

 查看redis是否成功生成:

 Jmeter测试并发:

 csv文件数据(模拟不同用户传不同的phone号码):

 运行后察看结果树:

 有一个人抢到了,再看数据库数据是否准确:

库存确实减少了一个:

新增了一条订单数据:

 

大功告成!按照大神的思路确实可行,以上就是我初试并发秒杀功能的过程了,我知道正式线上的功能肯定没有这么简单,还需要考虑到许多服务器的负载方面问题,但起码踏出了第一步,不断努力吧!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值