# rocketmq_seckill **Repository Path**: wangzhe_spring/rocketmq_seckill ## Basic Information - **Project Name**: rocketmq_seckill - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-12-27 - **Last Updated**: 2020-12-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 秒杀系统 ![img](https://lc-qgLLpAND.cn-n1.lcfile.com/3017ad2bc3927807ade4.png/2020-12-27_135101.png) ### 秒杀系统需要考虑什么? 1. 不能超卖 2. 链接隐藏,防止用户在抢购前拿到链接,使用脚本进行抢购 3. 接口限流,防止系统崩溃 4. 防止单个用户频繁调用接口,抢购多件商品 5. 用户抢购结果应当尽量快速返回,提高用户体验 ### 主要技术实现 springboot、MySQL、Redis、RocketMQ ### 数据表 1. my_stock ```mysql CREATE TABLE `my_stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键,自增长', `name` varchar(255) DEFAULT NULL COMMENT '产品名称', `start_time` datetime DEFAULT NULL COMMENT '抢购开始时间', `quantity` int(11) DEFAULT NULL COMMENT '库存数量', `sale` int(11) DEFAULT NULL COMMENT '售出数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; ``` 2. my_order ```mysql CREATE TABLE `my_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键,自增长', `stock_id` bigint(20) DEFAULT NULL COMMENT '库存id', `name` varchar(255) DEFAULT NULL COMMENT '产品名称', `state` int(11) DEFAULT NULL COMMENT '状态,0表示已下单,1表示已支付,2表示超时关闭', `create_time` datetime DEFAULT NULL COMMENT '下单时间', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; ``` 3. my_user ```mysql CREATE TABLE `my_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键,自增长', `username` varchar(255) DEFAULT NULL COMMENT '用户名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; ``` ### 代码实现 1. 创建一个springboot项目,pom文件如下: ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.0.RELEASE 1.8 1.8 com.wangzhe rocketmq_seckill 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf mysql mysql-connector-java runtime org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.0 com.alibaba fastjson 1.2.51 org.springframework.boot spring-boot-starter-data-redis org.apache.rocketmq rocketmq-spring-boot-starter 2.0.3 com.google.guava guava 19.0 org.springframework.boot spring-boot-maven-plugin org.mybatis.generator mybatis-generator-maven-plugin 1.3.5 true false mysql mysql-connector-java 5.1.46 ``` 配置文件application.properties ```properties server.port=9999 #数据库 spring.datasource.url=jdbc:mysql://localhost:3306/rocketmq_seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #Redis spring.redis.host=127.0.0.1 spring.redis.port=6379 #RocketMQ rocketmq.name-server=192.168.42.171:9876 rocketmq.producer.group=my_order ``` 2. 项目启动类 ```java package com.wangzhe.rocketmq.seckill; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author wangzhe * @date 2020-12-23 15:22 */ @SpringBootApplication @MapperScan("com.wangzhe.rocketmq.seckill.mapper") @EnableTransactionManagement @EnableScheduling @EnableAsync public class SeckillApplication { public static void main(String[] args) { SpringApplication.run(SeckillApplication.class, args); } } ``` 3. 实现接口:获取验证值 该接口是为了防止用户在抢购前获取到完整的抢购链接。 MyOrderController ```java /** * 获取验证值 * * @param userId 用户id * @param stockId 库存id * @return 验证值 */ @RequestMapping(value = "/order/verifyHash", method = RequestMethod.GET) @ResponseBody public String getVerifyHash(Long userId, Long stockId) { return myOrderService.getVerifyHash(userId, stockId); } ``` MyOrderServiceImpl ```java @Override public String getVerifyHash(Long userId, Long stockId) { // 检查抢购是否已经开始 ValueOperations ops = redisTemplate.opsForValue(); String redisCache = ops.get("myStock:" + stockId); MyStock myStock; if (redisCache == null || redisCache.trim().equals("")) { myStock = myStockMapper.selectByPrimaryKey(stockId); if (myStock == null) { return "stockId error"; } ops.set("myStock:" + stockId, JSON.toJSONString(myStock), 15, TimeUnit.MINUTES); } else { myStock = JSONObject.parseObject(redisCache, MyStock.class); } if (System.currentTimeMillis() - myStock.getStartTime().getTime() < 0) { return "not started"; } // 检查用户是否合法 MyUser user = myUserMapper.selectByPrimaryKey(userId); if (user == null) { return "user error"; } // 生成验证值 String verify = "my_salt" + stockId + userId; String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes()); ops.set("verifyHash:" + stockId + ":" + userId, verifyHash, 15, TimeUnit.MINUTES); return verifyHash; } ``` 4. 实现接口:抢购下单 MyOrderController ```java /** * 令牌桶限流,每秒放行100个请求 */ private RateLimiter rateLimiter = RateLimiter.create(100); /** * 抢购下单 * * @param userId 用户id * @param stockId 库存id * @param verifyHash 验证值 * @return 抢购结果 * success 表示抢购成功 * fail 表示抢购失败 * sale out 表示售完 * not started 表示未开始 * param error 表示参数错误 * Illegal request 表示非法请求 */ @RequestMapping(value = "/order/commitOrder", method = RequestMethod.POST) @ResponseBody public String commitOrder(Long userId, Long stockId, String verifyHash) { String result; // 非阻塞式获取令牌 if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) { result = "limit"; } else { result = myOrderService.commitOrder(userId, stockId, verifyHash); } return result; } ``` MyOrderServiceImpl ```java @Override public String commitOrder(Long userId, Long stockId, String verifyHash) { ValueOperations ops = redisTemplate.opsForValue(); // 检查验证值是否合法 String temp = ops.get("verifyHash:" + stockId + ":" + userId); if (temp == null || temp.trim().equals("")) { return "Illegal request"; } else if (!Objects.equals(temp, verifyHash)) { return "Illegal request"; } // 检查当前用户访问次数,超过20次直接返回fail String requestCount = ops.get("requestCount:" + stockId + ":" + userId); if (requestCount == null || requestCount.trim().equals("")) { ops.set("requestCount:" + stockId + ":" + userId, "0"); } else if (Integer.parseInt(requestCount) > 20) { return "fail"; } ops.increment("requestCount:" + stockId + ":" + userId); // 从redis检查库存 String redisCache = ops.get("myStock:" + stockId); MyStock myStock; if (redisCache == null || redisCache.trim().equals("")) { myStock = myStockMapper.selectByPrimaryKey(stockId); if (myStock == null) { return "param error"; } ops.set("myStock:" + stockId, JSON.toJSONString(myStock), 15, TimeUnit.MINUTES); } else { myStock = JSONObject.parseObject(redisCache, MyStock.class); } // 检查抢购是否已经开始 if (System.currentTimeMillis() - myStock.getStartTime().getTime() < 0) { return "not started"; } // 若库存充足,扣减库存,使用乐观锁防止超卖 if (myStock.getSale() < myStock.getQuantity()) { redisTemplate.delete("myStock:" + stockId); int count = myStockMapper.updateSale(stockId, myStock.getSale()); if (count < 1) { return "fail"; } // redis缓存延时双删 myAsyncMethod.deleteCache("myStock:" + stockId); // 发送创建订单的mq消息 MyOrder myOrder = new MyOrder(); myOrder.setStockId(stockId); myOrder.setName(myStock.getName()); myOrder.setState(0); myOrder.setCreateTime(new Date()); rocketMQTemplate.syncSend("myOrder", JSON.toJSONString(myOrder)); return "success"; } // 若库存不足,直接返回sale out return "sale out"; } ``` MyAsyncMethod ```java package com.wangzhe.rocketmq.seckill.async; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; /** * @author wangzhe * @date 2020-12-25 16:15 */ @Component public class MyAsyncMethod { @Autowired private RedisTemplate redisTemplate; /** * 延时删除缓存 * * @param key 缓存key */ @Async public void deleteCache(String key) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } redisTemplate.delete(key); } } ``` 5. 订单消费者,将订单写入数据库 MyOrderConsumer ```java package com.wangzhe.rocketmq.seckill.mq; import com.alibaba.fastjson.JSONObject; import com.wangzhe.rocketmq.seckill.entity.MyOrder; import com.wangzhe.rocketmq.seckill.mapper.MyOrderMapper; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author wangzhe * @date 2020-12-25 12:05 */ @Component @RocketMQMessageListener(topic = "myOrder", consumerGroup = "myOrderConsumer") public class MyOrderConsumer implements RocketMQListener { @Autowired private MyOrderMapper myOrderMapper; @Override public void onMessage(String message) { if (message == null || message.trim().equals("")) { return; } MyOrder myOrder = JSONObject.parseObject(message, MyOrder.class); myOrderMapper.insert(myOrder); } } ``` 6. 设置定时任务处理超时订单 MyOrderTimer ```java package com.wangzhe.rocketmq.seckill.time; import com.wangzhe.rocketmq.seckill.entity.MyOrder; import com.wangzhe.rocketmq.seckill.mapper.MyOrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.List; /** * @author wangzhe * @date 2020-12-26 10:32 */ @Configuration public class MyOrderTimer { @Autowired private MyOrderMapper myOrderMapper; @Autowired private StringRedisTemplate redisTemplate; /** * 每分钟处理一次超时订单 */ @Scheduled(cron = "0 * * * * ?") private void closeTimeoutOrder() { myOrderService.closeTimeoutOrder(); } } ``` MyOrderServiceImpl ```java @Override public void closeTimeoutOrder() { // 查询超过30分钟未支付的订单 Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); calendar.add(Calendar.MINUTE, -30); List list = myOrderMapper.selectTimeoutOrder(calendar.getTime()); if (list != null && list.size() > 0) { // 修改订单状态为已关闭 myOrderMapper.updateState(list, 2); // 恢复库存 HashMap map = new HashMap<>(); HashSet set = new HashSet<>(); for (MyOrder myOrder : list) { Long key = myOrder.getStockId(); set.add("myStock:" + key); if (map.get(key) == null) { map.put(key, 1); } else { map.put(key, map.get(key) + 1); } } redisTemplate.delete(set); List myStockList = myStockMapper.selectByIdForUpdate(map.keySet()); for (MyStock myStock : myStockList) { myStock.setSale(myStock.getSale() - map.get(myStock.getId())); myStockMapper.updateByPrimaryKey(myStock); } // 延时双删redis缓存 myAsyncMethod.deleteCache(set); } } ``` 7. 抢购页面 sale.html ```html 抢购页面
id 商品名称
``` MyOrderController ```java /** * 抢购页面 * * @param model model * @return 抢购页面路径 */ @RequestMapping(value = "/sale", method = RequestMethod.GET) public String toSalePage(Map model) { model.put("MyStockList", myOrderService.getStockList()); return "sale"; } ```