# 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
## 秒杀系统

### 秒杀系统需要考虑什么?
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
抢购页面
```
MyOrderController
```java
/**
* 抢购页面
*
* @param model model
* @return 抢购页面路径
*/
@RequestMapping(value = "/sale", method = RequestMethod.GET)
public String toSalePage(Map model) {
model.put("MyStockList", myOrderService.getStockList());
return "sale";
}
```