需求: 设计一个高并发下的秒杀架构最核心的业务:
1、防止同一个用户多次秒杀一件商品
2、判断库存是否充足
3、下单业务:将订单信息写入数据库 同时要递减数据库中的库存4、redis 预热秒杀的商品,需要参与秒杀的商品提前存到 redis 中,包括库存
使用的技术栈
-
Springboot
-
redis
-
rabbitma
-
Mysql
-
MybatisPlus (mybatis)
结构图
代码结构
spike-server的pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--连接池信息-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
<!--布隆过滤器-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
spike-web的pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
</dependencies>
spike-server配置yml文件
server:
port: 8081
spring:
application:
name: spike-server
redis:
port: 6379
host: 192.168.224.129
password: 22ebiwdqnok2
database: 0
rabbitmq:
username: yl
password: admin
virtual-host: /yl
host: 192.168.224.129
port: 5672
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spike?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: admin
type: com.alibaba.druid.pool.DruidDataSource
dbcp2:
min-idle: 5
initial-size: 10
max-total: 50
max-wait-millis: 2000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: cn.yl.spike.vo
mapper-locations: classpath*:cn/yl/spike/service/mapper/xml/*.xml
spike-web配置yml文件
server:
port: 8080
spring:
rabbitmq:
port: 5672
host: 192.168.224.129
virtual-host: /yl
username: yl
password: admin
listener:
simple:
acknowledge-mode: manual
application:
name: spike-web
redis:
host: 192.168.224.129
port: 6379
password: 22ebiwdqnok2@lo
database: 0
spike-server代码
启动类
import cn.hutool.bloomfilter.BitMapBloomFilter;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
// Generated by https://start.springboot.io
// 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn
@SpringBootApplication
@MapperScan(basePackages = "cn.yl.spike.mapper")
public class SpikeServerApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeServerApplication.class, args);
}
/**
* 布隆过滤器
* @return
*/
@Bean
public BitMapBloomFilter bitMapBloomFilter() {
return new BitMapBloomFilter(100);
}
}
utils配置类
import cn.yl.spike.service.GoodsService;
import cn.yl.spike.vo.Goods;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
//CommandLineRunner初始户化数据接口实现类
@Component
public class MysqlToRedis implements CommandLineRunner {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GoodsService goodsService;
public void goodsToRedis() {
//封装查询数据
QueryWrapper<Goods> goodsQueryWrapper = new QueryWrapper<>();
goodsQueryWrapper.eq("spike", 1);
List<Goods> list = goodsService.list(goodsQueryWrapper);
if (CollectionUtils.isEmpty(list)) {
return;
}
//将查询的商品库存同步到redis中
list.forEach(goods -> {
redisTemplate.opsForValue().set(goods.getGoodsId() + "stock", goods.getTotalStocks().toString());
});
}
@Override
public void run(String... args) throws Exception {
this.goodsToRedis();
}
}
import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.yl.spike.service.OrderTableService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class SpikeConsumer {
@Autowired
private RedissonClient redissonClient;
@Autowired
private BitMapBloomFilter bloomFilter;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderTableService orderTableService;
private final String LOCK_KEY = "spike-lock";
@RabbitListener(queues = "spike-web") //监听spike-web队列
private void handlerSpikeMessage(Message message, Channel channel) {
//获取分布式公平锁
RLock fairLock = redissonClient.getFairLock(LOCK_KEY);
//看门狗机制获取到锁执行后面代码,否则等待
fairLock.lock();
//取得队列中的消息
String spikeMessage = new String(message.getBody());
//执行核心业务方法
JSONObject jsonObject = JSON.parseObject(spikeMessage);
//取得商品编号和用户编号
Integer userId = jsonObject.getInteger("userId");
Integer goodsId = jsonObject.getInteger("goodsId");
//判断库存
long stock = Long.parseLong(redisTemplate.opsForValue().get(goodsId + "stock"));
if (stock <= 0) {
return;
}
//取得消息编号
String messageId = message.getMessageProperties().getMessageId();
//取得投递编号
long tag = message.getMessageProperties().getDeliveryTag();
//判断是否重复消费
try {
if (bloomFilter.contains(messageId)) {
channel.basicAck(tag, false);
return;
}
orderTableService.addOrder(userId, goodsId);
channel.basicAck(tag, false);
//更新redis中递减的库存
redisTemplate.opsForValue().decrement(goodsId + "stock");
bloomFilter.add(messageId);//存储消费编号
} catch (Exception e) {
try {
channel.basicAck(tag, false);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
e.printStackTrace();
} finally {
//释放锁
fairLock.unlock();
}
}
}
redisson配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
//redisson配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;//主机地址
@Value("${spring.redis.port}")
private String port;//主机端口
@Value("${spring.redis.password}")
private String password;//密码
/**
* 生成redisson客户端对象
*
* @return
*/
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
return Redisson.create(config);
}
}
spike-web代码
启动类
import cn.hutool.bloomfilter.BitMapBloomFilter;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpikeWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeWebApplication.class, args);
}
@Bean
public BitMapBloomFilter bitMapBloomFilter() {
return new BitMapBloomFilter(100);
}
@Bean
Queue queue() {
return new Queue("spike-web");
}
}
控制层
import cn.hutool.bloomfilter.BitMapBloomFilter;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.UUID;
@RestController
@CrossOrigin //开启跨域支持
public class SpikeController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private BitMapBloomFilter bitMapBloomFilter;
@GetMapping("spike/{userId}/{goodsId}")
public Object doSpike(@PathVariable Integer userId
, @PathVariable Integer goodsId, HttpServletResponse response) {
//设置商品的唯一标识
String spikeId = userId + "-" + goodsId;
//判断是否已经参与过
if (bitMapBloomFilter.contains(spikeId)) {
return "你已参加过秒杀活动,请选择其他秒杀商品";
}
//判断库存
Long stoke = Long.parseLong(redisTemplate.opsForValue().get(goodsId + "stock"));
if (stoke < 1) {
return "该商品已被抢购一空,请选择其他商品";
}
//参与过秒杀,添加到过滤器中
bitMapBloomFilter.add(spikeId);
//封装发送信息
Map<String, Integer> spikeMessage = Map.of("userId", userId, "goodsId", goodsId);
//发送消息
rabbitTemplate.convertAndSend("", "spike-web", JSON.toJSONString(spikeMessage), message -> {
MessageProperties messageProperties = message.getMessageProperties();
String messageId = UUID.randomUUID().toString().replaceAll("-", "");
messageProperties.setMessageId(messageId);
return message;
});
return "正在拼命抢购中,请稍后查看订单详情...";
}
}