秒杀
架构图
准备数据库
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: 8001tomcat:threads:max: 400
spring:application:name: seckill-webredis:host: 127.0.0.1port: 16379database: 0lettuce:pool:enabled: truemax-active: 100max-idle: 20min-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%
- 几台服务器(什么配置)
- 带宽