因为一直对高并发的处理感兴趣,所以这段时间搜集了很多贴子,经过学习和个人的分析,我觉得这位大神的分享是比较思路清晰和能够上手的,不过大神有部分实际用到的东西没有补充,我这里就补充和记录下自己的实现过程啦。
大神的贴子:实战高并发秒杀实现(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号码):
运行后察看结果树:
有一个人抢到了,再看数据库数据是否准确:
库存确实减少了一个:
新增了一条订单数据:
大功告成!按照大神的思路确实可行,以上就是我初试并发秒杀功能的过程了,我知道正式线上的功能肯定没有这么简单,还需要考虑到许多服务器的负载方面问题,但起码踏出了第一步,不断努力吧!