ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

一招教你如何搭建一个秒杀系统

2021-09-04 18:04:32  阅读:217  来源: 互联网

标签:return normal spring redis 秒杀 mysql 一招 font 搭建


文章目录

1. 前言

秒杀系统在电商中越来越常见的。也成了面试中常常被问的问题。所以接下来手把手给大家搭建一个秒杀系统。面试不再慌。

2. 整体架构

我们代建的秒杀系统有如下要求:

  1. 秒杀商品xxx,数量100个。
  2. 秒杀商品不能超卖。
  3. 抢购链接隐藏
  4. 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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有