iis 二级网站 发布,宁波市建设网,太原流量大的网站,如何制作淘客导购网站#x1f3af; 导读#xff1a;本文档详细探讨了高并发场景下的秒杀系统设计与优化策略#xff0c;特别是如何在短时间内处理大量请求。文档分析了系统性能指标如QPS#xff08;每秒查询率#xff09;和TPS#xff08;每秒事务数#xff09;#xff0c;并通过实例讲解了… 导读本文档详细探讨了高并发场景下的秒杀系统设计与优化策略特别是如何在短时间内处理大量请求。文档分析了系统性能指标如QPS每秒查询率和TPS每秒事务数并通过实例讲解了如何使用JMeter进行性能测试。此外文档提供了技术选型指南包括SpringBoot、Redis、RocketMQ等技术的应用并给出了具体的用户量评估和服务器配置建议。最后通过分析不同的库存扣减与订单创建实现方式提出了使用Redis分布式锁等技术提高并发性能的方法。 文章目录 秒杀介绍性能指标QPSTPS怎么优化接口性能 技术选型用户量评估技术要点架构图数据库创建项目选择依赖seckill-web接受用户秒杀请求pom.xml修改配置文件创建SeckillController 创建项目选择依赖seckill-service处理秒杀修改yml文件逆向生成实体类修改启动类修改GoodsMapper修改GoodsMapper.xml同步MySQL数据到redis方法1方法2 秒杀业务监听器修改GoodsService修改GoodsServiceImpl 秒杀
介绍
秒杀很短的时间内要处理大量的请求
【高并发介绍】
并发多个任务在同一时间段内执行cpu不停切换来执行不同任务
并行多核CPU上多个任务在同一时刻执行
要想高并发硬件很重要但是成本很高企业希望在有限的硬件上最优化软件的性能
性能指标
QPS
QPS每秒钟处理请求的数量业务处理时间越低QPS越高
Tomcat 的 QPSSpringBoot的Tomcat默认是最大是200个线程如果请求处理消耗50ms理论QPS就是1000*200/504000实际大概率会更低
可以在配置文件中设置tomcat的线程数量 【使用Jmeter测试】 如果异常很大超过0.5%数据就没有太大的价值
Tomcat最大连接数改成400 如果并发量非常大一个Tomcat顶不住可以做服务集群。
一个nginx可以顶住5w的QPS再负载均衡到多个tomcat服务中 并发量达到30wnginx顶不住了使用好机器来提供虚拟IP然后再将请求分发到多个Nginx中 个人开发100wQPS就很强了。如果有很大的流量可以根据用户IP拆分到不同地区的机房。一个域名下面对应很多个服务器IP按照用户IP区域将其分发大较近的机房IP即可 TPS
每秒钟能够处理的事务或交易的数量。
怎么优化接口性能
减少IO批量查询、批量插入、批量删除尽早return例如先去Redis判断的库存够不够再去执行扣减库存能异步就异步减库存放到MQ锁粒度尽量小事务范围尽可能小前端分流如拼图滑块、计算有人快、有人慢同时可以验证是否为机器人做限制一个人针对一个商品只能抢一次优惠券Redis setnx抢过就不让进来了 seckill-web接受秒杀请求然后把业务交给seckill-service执行seckill-service处理秒杀真实业务
技术选型
Springboot 接收请求并操作 redis 和 MySQLRedis 用于缓存分布式锁RocketMQ 用于解耦、削峰、异步MySQL 用于存放真实的商品信息Mybatis 用于操作数据库的orm框架
用户量评估
总用户量50w
日活量1-2w用户不会天天用除非经常做活动
qps2w怎么统计日志统计次数
几台服务器什么配置8C16G 4-6台
seckill-web4台seckill-service2台
带宽100M
技术要点
通过 redis 的 setnx 对用户和商品做去重判断, 防止用户刷接口每天晚上 8 点通过定时任务把 MySQL 中参与秒杀的库存商品, 同步到 redis 中去, 做库存的预扣减, 提升接口性能通过 RocketMQ 消息中间件的异步消息, 来将秒杀的业务异步化, 进一步提升性能seckill-service 使用并发消费模式, 并且设置合理的线程数量, 快速处理队列中堆积的消息使用 redis 的分布式锁自旋锁, 对商品的库存进行并发控制, 把并发压力转移到程序中和 redis 中去, 减少 db 压力使用声明式事务注解 Transactional, 并且设置异常回滚类型, 控制数据库的原子性操作使用 jmeter 压测工具, 对秒杀接口进行压力测试, 在 8C16G 的服务器上, qps2k, 达到压测预期
架构图 数据库
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 version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.13/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.powernode/groupIdartifactIdseckill-web/artifactIdversion0.0.1-SNAPSHOT/versionnameseckill-web/namedescriptionDemo project for Spring Boot/descriptionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!-- RocketMQ的依赖 --dependencygroupIdorg.apache.RocketMQ/groupIdartifactIdRocketMQ-spring-boot-starter/artifactIdversion2.2.1/version/dependencydependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactIdversion2.0.14/version/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationexcludesexcludegroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/exclude/excludes/configuration/plugin/plugins/build/project修改配置文件
server:port: 7001tomcat:threads:max: 400
spring:application:name: seckill-webredis:host: 127.0.0.1port: 6379database: 0lettuce:pool:enabled: truemax-active: 100max-idle: 20min-idle: 5
RocketMQ:name-server: 192.168.188.129:9876 # RocketMQ的nameServer地址producer:access-key: dsad secret-key: dsadasfasgroup: powernode-group # 生产者组别不配置会报错send-message-timeout: 3000 # 消息发送的超时时间retry-times-when-send-async-failed: 2 # 异步消息发送失败重试次数max-message-size: 4194304 # 消息的最大长度创建SeckillController
package com.powernode.controller;import com.alibaba.fastjson.JSON;
import org.apache.RocketMQ.client.producer.SendCallback;
import org.apache.RocketMQ.client.producer.SendResult;
import org.apache.RocketMQ.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;RestController
public class SeckillController {Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate RocketMQTemplate RocketMQTemplate;/*** 压测时自动是生成用户id*/AtomicInteger ai new AtomicInteger(0);/*** 1.用户去重一个用户针对一种商品只能抢购一次* 2.做库存的预扣减 拦截掉大量无效请求* 3.放入mq 异步化处理订单* userId通过登录状态拿取* 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。如果要每天刷新key加上年月日即可key再设置过期时间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 该商品已经被抢完请下次早点来;}// 放入mqHashMapString, Integer map new HashMap(4);map.put(goodsId, goodsId);map.put(userId, userId);RocketMQTemplate.asyncSend(seckillTopic3, JSON.toJSONString(map), new SendCallback() {Overridepublic void onSuccess(SendResult sendResult) {System.out.println(发送成功 sendResult.getSendStatus());}Overridepublic void onException(Throwable throwable) {System.err.println(发送失败 throwable.getMessage());}});// 不能直接返回抢购成功因为MQ可能是有问题的return 拼命抢购中请稍后去订单中心查看;}
}创建项目选择依赖seckill-service处理秒杀
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.13/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.powernode/groupIdartifactIdseckill-service/artifactIdversion0.0.1-SNAPSHOT/versionnameseckill-service/namedescriptionDemo project for Spring Boot/descriptionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.3.0/version/dependencydependencygroupIdcom.MySQL/groupIdartifactIdMySQL-connector-j/artifactIdscoperuntime/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.2.6/version/dependency!-- RocketMQ的依赖 --dependencygroupIdorg.apache.RocketMQ/groupIdartifactIdRocketMQ-spring-boot-starter/artifactIdversion2.2.1/version/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependencydependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactIdversion2.0.14/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationexcludesexcludegroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/exclude/excludes/configuration/plugin/plugins/build/project修改yml文件
server:port: 7002
spring:application:name: seckill-servicedatasource:driver-class-name: com.MySQL.cj.jdbc.Driverurl: jdbc:MySQL://127.0.0.1:3306/seckill?useUnicodetruecharacterEncodingUTF-8serverTimezoneUTCusername: rootpassword: 123456redis:host: 127.0.0.1port: 6379database: 0lettuce:pool:enabled: truemax-active: 100max-idle: 20min-idle: 5
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath*:mapper/*.xml
RocketMQ:name-server: 192.168.188.129:9876逆向生成实体类
修改启动类
SpringBootApplication
MapperScan(basePackages {com.powernode.mapper}) // mapper上面有Mapper注解这里就不用加扫描了
EnableScheduling // 开启定时任务
public class seckillServiceApplication {public static void main(String[] args) {SpringApplication.run(seckillServiceApplication.class, args);}
}修改GoodsMapper
ListGoods selectSeckillGoods();修改GoodsMapper.xml
!-- 查询数据库中需要参于秒杀的商品数据 status 2 --
select idselectSeckillGoods resultMapBaseResultMapselect id,stocks from goods where status 2
/select同步MySQL数据到redis
方法1
package com.powernode.config;import com.powernode.domain.Goods;
import com.powernode.mapper.GoodsMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import java.util.List;/*** 将MySQL的参与抢购的商品的数据* 同步到redis里面去* 在上游服务需要使用redis来做库存的预扣减*/
Component
public class DataSyncConfig {Autowiredprivate GoodsMapper goodsMapper;Autowiredprivate StringRedisTemplate redisTemplate;// 业务场景是搞一个定时任务 每天10点开启// 为了 测试方便 项目已启动就执行一次/*** spring bean的生命周期* 在当前对象 实例化完以后* 属性注入以后* 执行 PostConstruct 注解的方法*/PostConstruct// java的注解不是Spring的注解项目启动的时候就执行这个方法Scheduled(cron 0 10 0 0 0 ?)public void initData() {ListGoods goodsList goodsMapper.selectSeckillGoods();if (CollectionUtils.isEmpty(goodsList)) {return;}goodsList.forEach(goods - redisTemplate.opsForValue().set(goods_stock: goods.getId(), goods.getStocks().toString()));}}不用上面的方法的话可以在启动类中写但是不推荐 Bean生命周期 实例化对象 new 属性赋值 初始化 spring boot 前PostConstruct或下面写法中后 Component
public class DataSync implements InitializingBean, BeanPostProcessor{Override public void afterropertiesSet() throws Exception {} Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException{}Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException{}
}使用 销毁
方法2
package com.powernode.data;import com.powernode.domain.Goods;
import com.powernode.mapper.GoodsMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;Component
public class MySQLToRedis2 implements CommandLineRunner {Resourceprivate GoodsMapper goodsMapper;Resourceprivate StringRedisTemplate stringRedisTemplate;Overridepublic void run(String... args) throws Exception {initData();}private void initData() {//1 查询数据库中需要参于秒杀的商品数据ListGoods goodsList goodsMapper.queryseckillGoods();ValueOperationsString, String operations stringRedisTemplate.opsForValue();//2 把数据同步到Redisfor (Goods goods : goodsList) {operations.set(goods: goods.getGoodsId(), goods.getTotalStocks().toString());}}}秒杀业务监听器
package com.powernode.listener;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.powernode.service.GoodsService;
import org.apache.RocketMQ.common.message.MessageExt;
import org.apache.RocketMQ.spring.annotation.RocketMQMessageListener;
import org.apache.RocketMQ.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** 默认负载均衡模式* 默认多线程消费*/
Component
RocketMQMessageListener(topic seckillTopic3, consumerGroup seckill-consumer-group)
public class SeckillMsgListener implements RocketMQListenerMessageExt {Autowiredprivate GoodsService goodsService;Autowiredprivate StringRedisTemplate redisTemplate;// 20sint time 20000;/** 扣减库存* 写订单表*/Overridepublic 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);// 减库存写订单表使用同步代码块
// synchronized (this) {
// goodsService.realDoSeckill1(goodsId, userId);
// }// 减库存写订单表使用MySQL行锁// goodsService.realDoSeckill1(goodsId, userId);// 减库存写订单表使用Redis自旋加锁 int current 0;// 如果有业务因为自旋时间限制在有限时间内没有抢得到锁可以增加限制时间上限或者把循环改成truewhile (current time) {// 一般在做分布式锁的情况下会给锁一个过期时间防止出现死锁Boolean flag redisTemplate.opsForValue().setIfAbsent(goods_lock: goodsId, , 10, TimeUnit.SECONDS);if (flag) {// 加锁成功try {goodsService.realDoSeckill(goodsId, userId);return;} finally {// 解锁redisTemplate.delete(goods_lock: goodsId);}} else {// 获取锁失败自旋加锁current 200;try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}}}}修改GoodsService
void realDoSeckill(Integer goodsId, Integer userId);修改GoodsServiceImpl
【基础方案有问题】
Resource
private GoodsMapper goodsMapper;Autowired
private OrderRecordsMapper orderRecordsMapper;/*** 扣减库存* 写订单表* param goodsId* param userId*/
Override
Transactional(rollbackFor RuntimeException.class)
public void realDoSeckill(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 会出现i0的情况// 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默认事务隔离级别是可重复读。原本有1000件两个人消费按理说是998件。但实际上A进入了方法修改完库存释放了锁但是还没有提交事务Transactional是包住整个方法的。B线程进来获得了锁查询数据库还是1000件导致两个线程业务执行完成之后还剩下999
解决要先提交事务才释放锁这样才是正确的。将代码改成锁包住事务数据正确性保证了。但是效率还是低 分布式系统要改成分布式锁
【使用MySQL行锁(innodb才有)并发性能不足】
update goods set stocks stocks - 1会触发行锁update goods set stocks 具体值不会触发行锁stocks 1加一个控制
/*** MySQL行锁 innodb 行锁* 分布式锁* todo 答案1** param goodsId* param userId*/
Override
Transactional(rollbackFor RuntimeException.class)
public void realDoSeckill1(Integer goodsId, Integer userId) {// update goods set stocks stocks - 1 ,update_time now() where id #{value} and stocks 1 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);}
}缺点通过MySQL来控制锁数据库压力大如果并发数在1000以下还好高一点还是建议其他方案
【在监听器中使用Redis自旋加锁】
详情看前面的秒杀业务监听器实现