赞
踩
秒杀系统在电商中越来越常见的。也成了面试中常常被问的问题。所以接下来手把手给大家搭建一个秒杀系统。面试不再慌。
我们代建的秒杀系统有如下要求:
整体思路如下:
1、首先在mysql 中创建一张表,用户记录库存信息。
2、将mysql库存信息加载到redis 中。
3、用户进行抢购,先从redis 中获取库存,然后进行事务操作。
4、事务操作包含:
5、消费者进行监听处理消息,更新mysql 库存。
6、抢购的连接采用动态链接,先获取这个动态链接,然后进行抢购,这里随机生成一个uuid加在url 当中,并且存放到redis 中缓存一分钟。也就是或动态链接1分钟有效。
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;
插入一条数据。
这样数据库的准备工作就完成啦。
redis 我这里也是单机的,启动redis 就好了,如果在生产中肯定是集群的,不然高并发redis 也不一定能抗住。redis 启动就可以了,其他操作放在代码中说吧。
rocketMQ 需要启动nameServer 和 broker 。也需要部署集群。我这里模拟就用单机的。
好了, 准备工作做完之后,我们就要来写代码啦。
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、重点。进行抢购的操作。
@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>
我们首先通过页面来看下吧,页面操作不能模拟高并发的场景。不过可以验证一下流程。
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
就这样我们一个秒杀系统就搭建好了,没有接触过的小伙伴可以赶紧试试。没有想象中的那么遥不可及。说不动那天面试就被问到了呢
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。