秒杀

架构图

准备数据库
| SET NAMES utf8mb4; |
| SET FOREIGN_KEY_CHECKS = 0; |
| |
| |
| |
| |
| 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; |
| |
| |
| |
| |
| 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'); |
| |
| |
| |
| |
| 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/> |
| </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 |
| 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; |
| |
| |
| |
| AtomicInteger ai = new AtomicInteger(0); |
| |
| |
| |
| |
| |
| |
| |
| |
| @GetMapping("doSeckill") |
| public String doSeckill(Integer goodsId ) { |
| int userId = ai.incrementAndGet(); |
| |
| String uk = userId + "-" + goodsId; |
| |
| Boolean flag = redisTemplate.opsForValue().setIfAbsent("seckillUk:" + uk, ""); |
| if (!flag) { |
| return "您以及参与过该商品的抢购,请参与其他商品抢购!"; |
| } |
| |
| |
| if (count < 0) { |
| return "该商品已经被抢完,请下次早点来哦O(∩_∩)O"; |
| } |
| |
| 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/> |
| </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
| |
| |
| |
| |
| @Component |
| public class DataSync { |
| |
| @Resource |
| private GoodsMapper goodsMapper; |
| |
| @Resource |
| private StringRedisTemplate redisTemplate; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| @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; |
| |
| |
| 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"); |
| |
| |
| |
| |
| |
| |
| 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
| |
| |
| |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| |
| @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()); |
| |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
秒杀总结
技术选型:SpringBoot + Redis + MySQL + RocketMQ + Security ......
设计:(抢优惠券...)
- 设计seckill-web接收处理秒杀请求
- 设计seckill-service处理秒杀真实业务的
部署细节: 2C 2B
- 用户量:50w
- QPS:2w+ 自己打日志、Nginx(access.log)
- 日活量:1w-2w 1%-5%
- 几台服务器(什么配置):8C16G 6台 seckill-web:4台 seckill-service:2台
- 带宽:100M
技术要点:
- 通过Redis的setnx对用户和商品做去重判断,防止用户刷接口的行为
- 每天晚上八点通过定时任务 把mysql中参与秒杀的库存商品,同步到Redis中去,做库存的预扣减,提升接口性能
- 通过RocketMQ消息中间件的异步消息,来将秒杀的业务异步化,进一步提升性能
- seckill-service使用并发消费模式,并且设置合理的线程数量,快速处理队列中堆积的消息
- 使用Redis的分布式锁+自旋锁,对商品的库存进行并发控制,把并发压力转移到程序中和Redis中去,减少db的压力
- 使用声明式事务注解Transactional,并且设置异常回滚类型,控制数据库的原子操作
- 使用JMeter压测工具,对秒杀接口进行压力测试,在8C16G的服务器上,QPS2k+,达到压测预期
本文作者:指尖下的代码i
本文链接:https://www.cnblogs.com/xu1feng/p/18527732
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」