学习完RocketMQ的用法,现在用它来做一个简单的秒杀项目练练手。
关于秒杀,我之前其实有专门的学习过其中的一些业务逻辑和常见问题,我在这篇博客中有写过多并发场景下的秒杀场景,需要考虑哪些问题?也可以学习一下
除了RocketMQ,本文还需要会springBoot + Redis + Mysql的基本使用。
目录
- 前置知识
-
- QPS、jmeter压测
- 一般的秒杀架构如何设计
- 本项目结构设计
- 数据库准备
- 接收用户秒杀服务
-
- pom和配置文件
- SeckillController
- 处理秒杀服务(重点)
-
- pom和配置文件
- Mysql、Redis数据同步
- 创建秒杀监听
-
- 方案一 在事务外面加锁
-
- 先查库存再减带来的并发问题
- 方案二 分布式锁 - mysql(行锁)
- 方案三 分布式锁 - redis setnx
-
- 方案三 redis分布式锁优化
前置知识
QPS、jmeter压测
QPS:每秒处理请求的数量。tomcat一般QPS是多少呢?大家说的500?我们看一下tomcat默认是多少个线程:
boot 默认是200个线程 假设一个请求50ms 1个线程1s能处理20次 20*200 =4000qps
4000也是理论数值,线程切换需要时间,请求进来时的处理也需要时间,所以是<4000的
我们启动个boot服务,写两个接口测试一下:
@RestController
public class QpsTestController {
/**
*
* @return
*/
@GetMapping("test")
public String qpsTest() {
return "ok";
}
@GetMapping("test2")
public String qpsTest2() {
// dosth. 处理处理 操作数据库 操作redis .... 假设为50ms
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}
}
用jmeter压测一下,这个软件用过吧,没用过也没关系,很好上手,下载后在bin目录,双击jmeter.bat启动,Zoom in放大字体,还可以选中文。
在默认的Test Plan加一个线程组:
5000个线程1s内循环2次,所以1s内有10000个线程。
然后Test Plan添加Http请求:
再搞个采样的数据监测器,取结果。在Http请求里加:
然后启动就会开始打请求了。当然我们这里压测和被压测的都在同一台电脑肯定是不准的。
像我这种异常率很高的,那数据没有参考价值,qps都测出来15000了,异常率要低于0.5%才有参考价值,要点击下面的清除按钮再启动,一定要清楚,否则指标是平均值。
然后是test2接口测试,因为是一个电脑测试,异常率太高,正常测试的话就是小于4000的一个值了。
根据经验的话,如果tomcat的线程数设置为默认的200,如果一个接口的处理时间很短,在10ms内,那么QPS大概在4k左右,如果接口时间在50ms内,那么QPS大概在2k左右。直观的感受就是处理时间越短,QPS越大,可以自己尝试一下,然后也可以修改一下tomcat线程数尝试一下。
一般的秒杀架构如何设计
根据上一节的经验法则,一个tomcat也就支持2k的qps,代码写得不好可能2k都没有。
那如果是1w/s的请求来了怎么办呢?可以集群部署tomcat,然后用nginx来做负载均衡,但是nginx本身也就顶得住5~10w/s的请求,一般也就5w左右,所以一个nginx也就能配大概25个tomcat。
那么如果请求QPS大于5w,是20w/s咋办呢,底层是可以堆无数个tomcat,但是nginx也顶不住了,nginx是中心化的方案,所有的请求都要经过它去做负载均衡。而且nginx也是做不了集群的,中心化怎么做集群,就算做了也是需要一个nginx来顶住所有请求的,总得有个入口吧?
上面的方案不合理,那20w/s的请求咋办呢?入口让硬件来顶,对外提供一个虚拟ip,用Lvs或者F5这样的机器做入口,它能顶30w/s的QPS。
LVS是一个开源的负载均衡软件,基于Linux内核,可以将网络流量分发到多个后端服务器,实现高可用性和高性能。
F5是一种硬件负载均衡设备,广泛应用于企业级网络环境中。性能要比LVS更高,价格更贵。
那么如果QPS是100w/s呢?现在还没有能顶100w/s的硬件,现在大型的购物平台高峰期可能都是千万级的QPS了,怎么弄呢?做流量分发,比如优惠券,可以分成不同地区的。具体实现可以使用域名–DNS轮询策略,一个域名下面对应了很多个IP,不同的IP对应不同地区的机房,把流量分散开来。
然后我们写这个秒杀项目还接触不到这些架构设计,写这个秒杀项目的核心目的还是一开始的问题,尽量把每一个接口请求的处理时间给压缩,提高单tomcat的QPS,承受更大的并发量。那如何优化呢——关键是能异步就异步
,这就是学完了RocketMQ来写秒杀的原因。
下图是一些后端代码层面优化的一些方法,其实优化方法有很多很多,前端、网络传输、服务端、应用层各种各样的优化方法,比如如果真的做秒杀,前端也会帮我们做限流分流,比如前端可能会搞人际校验,验证码、拼图之类的。
本项目结构设计
那最后说了这么多,我们项目的结构如何设计,如下图,买东西要先判断库存,如果直接查数据库效率太低,加入redis基于内存提高IO效率,秒杀开始之前数据库的库存数量先预同步到redis,用redis做预扣减,redis理论支持11w读、8w写,真正能达到8w读、6w写。
库存减成功了之后要操作数据库,但是商品种类很多,比如10w件商品在秒杀,操作数据库太慢了,可以将这个操作异步化。用户下单是一个服务(seckill-web),数据库操作(seckill-service)是一个服务,通过rocketmq来解耦。
然后消息消费,即数据库操作也是需要时间的,所以用户也不能立马看到效果,使用异步通知,或者让用户一会再去查看结果。
然后还有一个问题,秒杀场景经常有一个逻辑是一个用户只能对一件商品抢一次,那么这个工作主要是去重,可以通过mysql去重表,但是它不适合大量并发的场景,所以还是通过redis,它的setnx命令也天然支持去重。
综上,技术选型:springBoot + Redis + Mysql + RocketMq + Security(后期登录可以加上) …
设计: (抢优惠券…)
设计seckill-web接收处理秒杀请求
设计seckill-service处理秒杀真实业务的
部署细节:
用户量: 50w
日活量: 1w-2w 1%-5%
qps: 2w+
如何知道自己的接口有多少qps。方法一:代码统计,自己log打印,统计同一分钟内有多少请求,分布式则=机器台数*单台QPS
方法二,看nginx(access.log),它会记录所有请求,可以写个脚本统计一分钟内某个接口被访问了多少次
方法三,市面上一些监控工具比如阿尔萨斯
几台服务器(什么配置):8C16G 4台 seckill-web : 4台 seckill-service 2台
带宽: 100M
技术要点:
1.通过redis的setnx对用户和商品做去重判断,防止用户刷接口的行为
2.每天晚上8点通过定时任务 把mysql中参与秒杀的库存商品,同步到redis中去,做库存的预扣减,提升接口性能
3.通过RocketMq消息中间件的异步消息,来将秒杀的业务异步化,进一步提升性能
4.seckill-service使用并发消费模式,并且设置合理的线程数量,快速处理队列中堆积的消息
5.使用redis的分布式锁+自旋锁,对商品的库存进行并发控制,把并发压力转移到程序中和redis中去,减少db压力
6.使用声明式事务注解Transactional,并且设置异常回滚类型,控制数据库的原子性操作
7.使用jmeter压测工具,对秒杀接口进行压力测试,在8C16G的服务器上,qps2k+,达到压测预期
8.使用sentinel的热点参数限流规则,针对爆款商品和普通商品的区别,区分限制
数据库准备
先准备一个数据库,设计了一个商品表和订单表,用户表没有设计问题不大后面传进来就行了。
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;
接收用户秒杀服务
pom和配置文件
pom:
<?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