秒殺
架構圖
準備資料庫
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`price` decimal(10, 2) NULL DEFAULT NULL,
`stocks` int(255) NULL DEFAULT NULL,
`status` int(255) NULL DEFAULT NULL,
`pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '華為mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '錘子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');
-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`goods_id` int(11) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
建立專案seckill-web(接收使用者秒殺請求)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xyf</groupId>
<artifactId>e-seckill-web</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
修改配置檔案application.yml
[server:
port: 8081
tomcat:
threads:
max: 400
spring:
redis:
host: localhost
port: 16379
database: 0
rocketmq:
name-server: 127.0.0.1:9876](<server:
port: 8001
tomcat:
threads:
max: 400
spring:
application:
name: seckill-web
redis:
host: 127.0.0.1
port: 16379
database: 0
lettuce:
pool:
enabled: true
max-active: 100
max-idle: 20
min-idle: 5
rocketmq:
name-server: 127.0.0.1:9876 # rocketMq的nameServer地址
producer:
group: seckill-producer-group # 生產者組別
send-message-timeout: 3000 # 訊息傳送的超時時間
retry-times-when-send-async-failed: 2 # 非同步訊息傳送失敗重試次數
max-message-size: 4194304 # 訊息的最大長度>)
建立SecKillController
@RestController
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 壓測時自動是生成使用者id
*/ AtomicInteger ai = new AtomicInteger(0);
/**
* 1.一個使用者針對一種商品只能搶購一次
* 2.做庫存的預扣減 攔截掉大量無效請求
* 3.放入mq 非同步化處理訂單
*
* @return
*/
@GetMapping("doSeckill")
public String doSeckill(Integer goodsId /*, Integer userId*/) {
int userId = ai.incrementAndGet();
// unique key 唯一標記 去重
String uk = userId + "-" + goodsId;
// set nx set if not exist
Boolean flag = redisTemplate.opsForValue().setIfAbsent("seckillUk:" + uk, "");
if (!flag) {
return "您以及參與過該商品的搶購,請參與其他商品搶購!";
}
// 假設庫存已經同步了 key:goods_stock:1 val:10 Long count = redisTemplate.opsForValue().decrement("goods_stock:" + goodsId);
// getkey java setkey 先查再寫 再更新 有併發安全問題
if (count < 0) {
return "該商品已經被搶完,請下次早點來哦O(∩_∩)O";
}
// 放入mq
HashMap<String, Integer> map = new HashMap<>(4);
map.put("goodsId", goodsId);
map.put("userId", userId);
rocketMQTemplate.asyncSend("seckillTopic3", JSON.toJSONString(map), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("傳送成功" + sendResult.getSendStatus());
}
@Override
public void onException(Throwable throwable) {
System.err.println("傳送失敗" + throwable);
}
});
return "拼命搶購中,請稍後去訂單中心檢視";
}
}
建立專案seckill-service(處理秒殺)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xyf</groupId>
<artifactId>f-seckill-service</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
修改yml檔案
server:
port: 8002
spring:
application:
name: seckill-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:13306/spike?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
redis:
host: 127.0.0.1
port: 16379
database: 0
lettuce:
pool:
enabled: true
max-active: 100
max-idle: 20
min-idle: 5
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mapper/*.xml
rocketmq:
name-server: 127.0.0.1:9876
逆向生成實體類等
修改啟動類
@SpringBootApplication
@MapperScan(basePackages = {"com.xyf.mapper"})
public class SpikeServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeServiceApplication.class, args);
}
}
修改GoodsMapper
List<Goods> selectSeckillGoods();
修改GoodsMapper.xml
<select id="selectSeckillGoods" resultType="com.xyf.domain.Goods">
select id, stocks from goods where `status` = 2
</select>
同步mysql資料到redis
/**
* 1. 每天10點 晚上8點 透過定時任務 將mysql的庫存 同步到redis中去
* 2. 為了測試方便 希望專案啟動的時候 就同步資料
*/
@Component
public class DataSync {
@Resource
private GoodsMapper goodsMapper;
@Resource
private StringRedisTemplate redisTemplate;
// @Scheduled(cron = "* * 10 * * ? ")
// public void initData() {
//
// }
/**
* 我希望這個方法在專案啟動之後
* 並且在這個類的屬性注入完畢以後
*
* bean的生命週期
*
* 例項化 new
* 屬性複製
* 初始化 (前PostConstruct/中InitializingBean/後BeanPostProcessor)自定義的一個initMethod方法
* 使用
* 銷燬
* -------------
*/ @PostConstruct
public void initData() {
List<Goods> goodsList = goodsMapper.selectSeckillGoods();
if (CollectionUtils.isEmpty(goodsList)) {
return;
}
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set("goodsId:" + goods.getId(), goods.getStocks().toString());
});
}
}
建立秒殺監聽
@Component
@RocketMQMessageListener(topic = "seckillTopic3", consumerGroup = "seckill-consumer-group")
public class SeckillMsgListener implements RocketMQListener<MessageExt> {
@Autowired
private GoodsService goodsService;
@Autowired
private StringRedisTemplate redisTemplate;
// 20s
int time = 20000;
@Override
public void onMessage(MessageExt message) {
String s = new String(message.getBody());
JSONObject jsonObject = JSON.parseObject(s);
Integer goodsId = jsonObject.getInteger("goodsId");
Integer userId = jsonObject.getInteger("userId");
// 做真實的搶購業務 減庫存 寫訂單表 todo 答案2 但是不符合分散式
// synchronized (SeckillMsgListener.class) {
// goodsService.realDoSeckill(goodsId, userId);
// }
// 自旋鎖 一般 mysql 每秒1500/s寫 看數量 合理的設定自旋時間 todo 答案3
int current = 0;
while (current <= time) {
// 一般在做分散式鎖的情況下 會給鎖一個過期時間 防止出現死鎖的問題
Boolean flag = redisTemplate.opsForValue().setIfAbsent("goods_lock:" + goodsId, "", 10, TimeUnit.SECONDS);
if (flag) {
try {
goodsService.realSeckill(goodsId, userId);
return;
} finally {
redisTemplate.delete("goods_lock:" + goodsId);
}
} else {
current += 200;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
修改GoodsService
/**
* 真正處理秒殺的業務
* @param userId
* @param goodsId
*/
void realSeckill(Integer userId, Integer goodsId);
修改GoodsServiceImpl
@Service
public class GoodsServiceImpl implements GoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private OrderRecordsMapper orderRecordsMapper;
@Override
public int deleteByPrimaryKey(Integer id) {
return goodsMapper.deleteByPrimaryKey(id);
}
@Override
public int insert(Goods record) {
return goodsMapper.insert(record);
}
@Override
public int insertSelective(Goods record) {
return goodsMapper.insertSelective(record);
}
@Override
public Goods selectByPrimaryKey(Integer id) {
return goodsMapper.selectByPrimaryKey(id);
}
@Override
public int updateByPrimaryKeySelective(Goods record) {
return goodsMapper.updateByPrimaryKeySelective(record);
}
@Override
public int updateByPrimaryKey(Goods record) {
return goodsMapper.updateByPrimaryKey(record);
}
/**
* @param goodsId
* @param userId
*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void realSeckill(Integer goodsId, Integer userId) {
// 扣減庫存 插入訂單表
Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
int finalStock = goods.getStocks() - 1;
if (finalStock < 0) {
// 只是記錄日誌 讓程式碼停下來 這裡的異常使用者無法感知
throw new RuntimeException("庫存不足:" + goodsId);
}
goods.setStocks(finalStock);
goods.setUpdateTime(new Date());
// insert 要麼成功 要麼報錯 update 會出現i<=0的情況
// update goods set stocks = 1 where id = 1 沒有行鎖
int i = goodsMapper.updateByPrimaryKey(goods);
if (i > 0) {
// 寫訂單表
OrderRecords orderRecords = new OrderRecords();
orderRecords.setGoodsId(goodsId);
orderRecords.setUserId(userId);
orderRecords.setCreateTime(new Date());
// 時間戳生成訂單號
orderRecords.setOrderSn(String.valueOf(System.currentTimeMillis()));
orderRecordsMapper.insert(orderRecords);
}
}
}
/**
* mysql行鎖 innodb 行鎖
* 分散式鎖
* todo 答案1
*
* @param goodsId
* @param userId
*/
// @Override
// @Transactional(rollbackFor = RuntimeException.class)
// public void realDoSeckill(Integer goodsId, Integer userId) {
// // update goods set stocks = stocks - 1 ,update_time = now() where id = #{value}
// int i = goodsMapper.updateStocks(goodsId);
// if (i > 0) {
// // 寫訂單表
// OrderRecords orderRecords = new OrderRecords();
// orderRecords.setGoodsId(goodsId);
// orderRecords.setUserId(userId);
// orderRecords.setCreateTime(new Date());
// // 時間戳生成訂單號
// orderRecords.setOrderSn(String.valueOf(System.currentTimeMillis()));
// orderRecordsMapper.insert(orderRecords);
// }
// }
秒殺總結
技術選型:SpringBoot + Redis + MySQL + RocketMQ + Security ......
設計:(搶優惠券...)
- 設計seckill-web接收處理秒殺請求
- 設計seckill-service處理秒殺真實業務的
部署細節: 2C 2B
- 使用者量:50w
- QPS:2w+ 自己打日誌、Nginx(access.log)
- 日活量:1w-2w 1%-5%
- 幾臺伺服器(什麼配置)
- 頻寬