当前位置:   article > 正文

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

rocketmq 抢单

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

插入一条数据。
在这里插入图片描述
这样数据库的准备工作就完成啦。

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

主要是引入 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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";
        }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

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());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124

5. 测试

我们首先通过页面来看下吧,页面操作不能模拟高并发的场景。不过可以验证一下流程。

1、首先我们预热库存。

http://127.0.0.1:9096/url/setNumber
  • 1

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

模拟一万个用户同时抢购。 检查mysql
在这里插入图片描述

检查redis

在这里插入图片描述

6. 总结

就这样我们一个秒杀系统就搭建好了,没有接触过的小伙伴可以赶紧试试。没有想象中的那么遥不可及。说不动那天面试就被问到了呢

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/807275
推荐阅读
相关标签
  

闽ICP备14008679号