一招教你如何搭建一个秒杀系统
作者:互联网
文章目录
1. 前言
秒杀系统在电商中越来越常见的。也成了面试中常常被问的问题。所以接下来手把手给大家搭建一个秒杀系统。面试不再慌。
2. 整体架构
我们代建的秒杀系统有如下要求:
- 秒杀商品xxx,数量100个。
- 秒杀商品不能超卖。
- 抢购链接隐藏
- Nginx+Redis+RocketMQ+Tomcat+MySQL
整体思路如下:
3. 设计思路
1、首先在mysql 中创建一张表,用户记录库存信息。
2、将mysql库存信息加载到redis 中。
3、用户进行抢购,先从redis 中获取库存,然后进行事务操作。
4、事务操作包含:
- redis 中减库存。
- 判断库存是否大于0
- 如果大于0,发送 生成订单消息
- 如果小于0,库存加回去。返回库存为0
5、消费者进行监听处理消息,更新mysql 库存。
6、抢购的连接采用动态链接,先获取这个动态链接,然后进行抢购,这里随机生成一个uuid加在url 当中,并且存放到redis 中缓存一分钟。也就是或动态链接1分钟有效。
4. 实现流程
4.1 mysql
mysql 的部署搭建就不说了,我这里就单机单库单表的操作。
创建表
DROP TABLE IF EXISTS `tb_spike_data_info`;
CREATE TABLE `tb_spike_data_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL comment '名称',
`number` int(11) NOT NULL comment '数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入一条数据。
这样数据库的准备工作就完成啦。
4.2 redis
redis 我这里也是单机的,启动redis 就好了,如果在生产中肯定是集群的,不然高并发redis 也不一定能抗住。redis 启动就可以了,其他操作放在代码中说吧。
4.3 RocketMQ
rocketMQ 需要启动nameServer 和 broker 。也需要部署集群。我这里模拟就用单机的。
4.4 代码
好了, 准备工作做完之后,我们就要来写代码啦。
1、引入依赖
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
<!--整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
主要是引入 redis ,mysql ,rocketmq 的依赖,因为我们会用到他们。
2、配置。我们需要配置他们的连接信息。
整体配置如下:
server.port=9096
spring.application.name=springboot-rocketmq
rocketmq.name-server=192.168.168.21:9876
rocketmq.producer.group=producer_group_spike_01
rocketmq.producer.send-message-timeout=3000
rocketmq.
#redis服务器地址
spring.redis.host=192.168.168.21
#redis服务器连接端口
spring.redis.port=6379
#redis服务器连接密码
spring.redis.password=
# Mysql数据库连接配置 : com.mysql.cj.jdbc.Driver
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.168.21:3306/spike?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
3、创建 spike 和order 的实体类,方便我们对数据库进行操作,以及进行消息的发送。
4、编写 SpikeMapper。 用来操作数据库
@Mapper
public interface SpikeMapper {
@Select("select * from tb_spike_data_info where id =#{id}")
SpikePojo findById(Integer id);
@Select("update tb_spike_data_info set `number`=`number`-1 where id =#{id}")
void updateById(Integer id);
}
5、UrlController 用来获取动态抢购连接和将数据库中的库存预热到redis 中。
@RestController
@RequestMapping("/url")
@CrossOrigin(origins = "*")
public class UrlController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SpikeMapper spikeMapper;
@RequestMapping("/get")
public String getUrl() {
String uuid = UUID.randomUUID().toString();
//保存到redis 中,设置一分钟有效。
redisTemplate.opsForValue().set(uuid, uuid, 60l, TimeUnit.SECONDS);
return uuid;
}
/**
* 库存预热
* @return
*/
@RequestMapping("/setNumber")
public String setNumber(){
String key = "product_number:001";
// 去查数据库的数据,并且把数据库的库存set进redis
SpikePojo spikePojo = spikeMapper.findById(1);
if (spikePojo.getNumber() > 0) {
redisTemplate.opsForValue().set(key, spikePojo.getNumber() + "");
}
return "success";
}
}
6、重点。进行抢购的操作。
- 请求进来先判断链接是否有效
- 有效进行抢单的操作,从redis 减库存,发现库存大于0 就往 rocketmq 中发送消息,生成订单。
@RestController
@RequestMapping("/begin")
@Slf4j
@CrossOrigin(origins = "*")
public class ProducerController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping(value = "/spike/{uuid}/{userId}", method = {RequestMethod.GET, RequestMethod.POST})
public String spike(@PathVariable String uuid, @PathVariable int userId) throws InterruptedException {
//判断链接是否正常,如果正常进行抢单操作。
if (redisTemplate.hasKey(uuid) ) {
if(spikeOrder(userId)){
return "恭喜" + userId + "用户,抢单成功";
}else {
return "抱歉"+userId+"用户,商品已经抢光,欢迎下次再来。";
}
}
return "抱歉"+userId+"用户,页面丢失了,请刷新";
}
public boolean spikeOrder(int uid) {
String key = "product_number:001";
return orderHandler(key,uid);
}
private synchronized boolean orderHandler(String key,int uid){
// 第二步:减少库存
Long value = redisTemplate.opsForValue().decrement(key);
// 库存充足
if (value >=0) {
// 通过 rocketmq 发送创建订单的消息,并且 update 数据库中商品库存。
boolean res = createOrder(uid, value);
//如果下订单成功,返回。
if (res) {
return true;
}
} else {
log.info("商品已经抢光,欢迎下次再来。");
}
//如果下单失败,则恢复库存。
redisTemplate.opsForValue().increment(key);
return false;
}
private boolean createOrder(int uid, Long value) {
//创建一个订单对想
OrderPojo orderPojo = new OrderPojo();
//设置秒杀商品编号
orderPojo.setOrderId(1);
//库存
orderPojo.setStock(value);
//购买数量,每次只能抢购一个
orderPojo.setNumber(1);
//购买用户id
orderPojo.setUserId(uid);
//需要捕获各种异常
try {
return sendMsg(orderPojo);
} catch (Exception e) {
log.info("{}", e);
}
return false;
}
public boolean sendMsg(OrderPojo orderPojo) {
//设置主题,超时时间为1s,同步发送
SendResult sendResult = rocketMQTemplate.syncSend("tp_spike_02", orderPojo.toString(), 1000);
log.info(sendResult.toString());
//发送成功,则返回成功
return SendStatus.SEND_OK.equals(sendResult.getSendStatus());
}
}
7、 创建一个消费者。进行处理消息。
@Slf4j
@Component
@RocketMQMessageListener(topic = "tp_spike_02", consumerGroup = "consumer_grp_01")
public class SpikeConsumer implements RocketMQListener<String> {
@Autowired
SpikeMapper spikeMapper;
@Override
public void onMessage(String message) {
// 处理broker推送过来的消息
log.info(message);
spikeMapper.updateById(1);
}
}
8、创建html 页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<style type="text/css">
body {
background-color: #00b38a;
text-align: center;
}
.lp-login {
position: absolute;
width: 500px;
height: 300px;
top: 50%;
left: 50%;
margin-top: -250px;
margin-left: -250px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 10px #12a591;
padding: 57px 50px 35px;
box-sizing: border-box
}
.lp-login .submitBtn {
display: block;
text-decoration: none;
height: 48px;
width: 150px;
line-height: 48px;
font-size: 16px;
color: #fff;
text-align: center;
background-image: -webkit-gradient(linear, left top, right top, from(#09cb9d), to(#02b389));
background-image: linear-gradient(90deg, #09cb9d, #02b389);
border-radius: 3px
}
input[type='text'] {
height: 30px;
width: 250px;
}
input[type='password'] {
height: 30px;
width: 250px;
}
span {
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-variant-numeric: normal;
font-variant-east-asian: normal;
font-weight: normal;
font-stretch: normal;
font-size: 14px;
line-height: 22px;
font-family: "Hiragino Sans GB", "Microsoft Yahei", SimSun, Arial, "Helvetica Neue", Helvetica;
}
</style>
<script>
function operate() {
$.ajax({
url: 'http://127.0.0.1:9096/url/get',
type: 'POST', //GET
timeout: 5000, //超时时间
success: function (data) {
if (data != "") {
const url = "index2.html?uuid=" + data;//此处拼接内容
window.location.href = url;
//window.location.href = "http://localhost/static/welcome.html";
} else {
alert("活动太火爆了,请稍候再试");
return;
}
}
})
}
</script>
</head>
<body>
<form>
<table class="lp-login">
<tr align="center">
<td colspan="2">
<button type="button" id="btn1" onclick="operate()"><span>进入抢购页面</span></button>
</td>
</tr>
</table>
</form>
</body>
</html>
index2:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>抢购页面</title>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<style type="text/css">
body {
background-color: #00b38a;
text-align: center;
}
.lp-login {
position: absolute;
width: 500px;
height: 300px;
top: 50%;
left: 50%;
margin-top: -250px;
margin-left: -250px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 10px #12a591;
padding: 57px 50px 35px;
box-sizing: border-box
}
.lp-login .submitBtn {
display: block;
text-decoration: none;
height: 48px;
width: 150px;
line-height: 48px;
font-size: 16px;
color: #fff;
text-align: center;
background-image: -webkit-gradient(linear, left top, right top, from(#09cb9d), to(#02b389));
background-image: linear-gradient(90deg, #09cb9d, #02b389);
border-radius: 3px
}
input[type='text'] {
height: 30px;
width: 250px;
}
input[type='password'] {
height: 30px;
width: 250px;
}
span {
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-variant-numeric: normal;
font-variant-east-asian: normal;
font-weight: normal;
font-stretch: normal;
font-size: 14px;
line-height: 22px;
font-family: "Hiragino Sans GB", "Microsoft Yahei", SimSun, Arial, "Helvetica Neue", Helvetica;
}
</style>
<script>
var thisURL = document.URL;
//分割成字符串
var getval = thisURL.split('?')[1];
var keyValue = getval.split('&');
var uuid = "";
for (var i = 0; i < keyValue.length; i++) {
var oneKeyValue = keyValue[i];
var oneValue = oneKeyValue.split("=")[1];
uuid = oneValue;
}
function operate() {
$.ajax({
url: 'http://127.0.0.1:9096/begin/spike/' + uuid + '/1',
type: 'POST', //GET
timeout: 5000, //超时时间
success: function (data) {
if (data != "") {
alert(data)
} else {
alert("error:");
}
}
})
}
</script>
</head>
<body>
<div id="uuid"></div>
<form>
<table class="lp-login">
<tr align="center">
<td colspan="2">
<span>欢迎来到抢购页面</span>
</td>
</tr>
<tr align="center">
<td colspan="2">
<button type="button" id="btn1" onclick="operate()"><span>立即抢购</span></button>
</td>
</tr>
</table>
</form>
</body>
</html>
5. 测试
我们首先通过页面来看下吧,页面操作不能模拟高并发的场景。不过可以验证一下流程。
1、首先我们预热库存。
http://127.0.0.1:9096/url/setNumber
2、然后访问index.html 页面。
3、点击进入抢购页面,来到了抢购页面
4、点击立即抢购
提示用户抢单成功。这里我们看下控制台。
5、检查redis 中的库存
6、检查 mysql 中的库存
这样整个流程下来,说明是没有问题的。接下来我们模拟高并发场景。我们写一个脚本,先获取动态链接,然后进行多线程抢购。
public class TestMain {
public static void main(String[] args) {
String baseUrl = getBaseUrl();
for(int i=0;i<10000;i++){
run(baseUrl,i);
}
}
public static void run(String baseUrl,int i){
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
String url=baseUrl+"/"+i;
String s = sendGet(url);
System.out.println(s);
}
}).start();
}
public static String getBaseUrl(){
String url="http://127.0.0.1:9096/url/get";
String s = sendGet(url);
return "http://127.0.0.1:9096/begin/spike/"+s;
}
public static String sendGet(String url) {
String result = "";
BufferedReader in = null;
try {
java.net.URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
}
模拟一万个用户同时抢购。 检查mysql
检查redis
6. 总结
就这样我们一个秒杀系统就搭建好了,没有接触过的小伙伴可以赶紧试试。没有想象中的那么遥不可及。说不动那天面试就被问到了呢
标签:return,normal,spring,redis,秒杀,mysql,一招,font,搭建 来源: https://blog.csdn.net/qq_27790011/article/details/120103252