一、实现目的
- 良好用户体验:能够满足高并发场景,保证系统可用
- 不能超卖:保证数据一致性
- 不能脚本抢票:数据安全性
二、基础中间件的并发
-
Nginx : 2w
-
Tomcat:5k
Tomcat的最大连接数,默认是8192,Tomcat的连接等待队列长度,默认是100,Tomcat的最大线程数,工作线程的最大数量 io密集型建议10倍的cpu数,cpu密集型建议cpu数+1,绝大部分应用都是io密集型。
-
Redis:1W
默认最大连接数10000;单机几万QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS
-
MySQL:800
最大并发连接数高达16384个,实际能够支持的并发连接数取决于服务器的CPU核心数、内存大小、硬盘I/O性能以及网络带宽等。一般来说,每个CPU核心可以支持大约100到200个并发连接。
-
Kafka:100W
分区数量决定并发能力,Kafka 中的每个分区都可以被独立消费,因此分区数量决定了 Kafka 的并发能力。
-
Jmeter: 1k
Jmeter作为Java应用,其并发用户数的上限主要受限于单台机器的硬件资源,特别是CPU和内存。通常,单台机器的并发用户数上限大约在1000左右
三、解决
1、满足QPS,保证高可用
- Nginx负载均衡
Nginx 把请求均匀的分摊给应用服务器,这样即使某一个服务器宕机也不会影响请求的处理,或者当应用服务器扛不住了,可以随时进行扩容。 Nginx 主要的负载均衡策略(内置的负载均衡)有以下四种:轮询策略(默认负载均衡策略)、最少连接数负载均衡策略、ip-hash 负载均衡策略、权重负载均衡策略
支持的QPS数量 = 应用数量 * Tomcat支持并发数 < nginx并发数
- Nacos集群部署
通过在多台服务器上部署 Nacos 实例,并将这些实例配置为集群模式,可以确保当某个节点出现故障时,其他节点仍然能够正常提供服务,从而保证业务的连续性。
- Redis高可用 主从模式、哨兵模式、集群模式
在主从模式下,主节点负责处理所有的写操作,并将写操作记录在内存中的缓冲区。从节点从主节点获取这些写操作记录,并在自己的数据库上执行这些操作,从而保持与主节点的数据一致。
在哨兵模式下,哨兵节点会定期检查主节点和从节点的运行状态。如果发现主节点发生故障,哨兵节点会在从节点中选举出一个新的主节点,并通知其他的从节点和哨兵节点。
在集群模式下,Redis使用一种叫做哈希槽的技术来实现数据的分片。整个哈希空间被分成16384个哈希槽,每个节点负责一部分哈希槽。当一个键需要被存储时,Redis会根据键的值计算出一个哈希值,然后根据哈希值决定将这个键存储在哪个节点上。这样,读写请求就可以在多个节点上并行处理,提高了系统的性能。
应用场景数据量大,需要高性能和高可用性,可选择集群模式
-
服务降级与熔断
为了保证整体系统可用,并发业务拆离单个服务,以免高并发业务影响整体系统的可用性,在服务调用时做好降级和熔断处理
基于OpenFegin的服务降级,在服务器忙碌或者网络堵塞,调用超时,服务器内部异常等场景下,给调用方一个有好的提示,采用Fallback方式
基于Sentinel的服务熔断,配置熔断规则策略(慢调用比例、异常比例、异常数),容短时间内直接返回降级结果
熔断策略:
- 慢调用比例:当每秒请求量超过设定的阈值时,开始记录慢调用,当慢调调用比例超过设定的阈值时,开启熔断。 >2. 异常比例:当每秒请求量超过设定的阈值时,开始记录异常比例,当异常比例超过设定的阈值时,开启熔断。 >3. 异常数:当每秒请求量超过设定的阈值时,异常数超过设定的阈值,开启熔断。
- 限流
- 前端限流
按钮连续点击禁用
- 后端限流
库存为0,关闭无效请求进入
- 服务限流
基于Sentinel的服务限流
- 前端限流
2、避免超卖,保证数据一致性
-
基于数据库的悲观锁(难以支持并发)
-
基于版本号的乐观锁 (并发情况在不会超卖,但是会出现卖不完情况)
-
基于Redis的库存扣减
-
单节点:
秒杀前将商品的库存信息保存到Redis中,秒杀期间扣减逻辑在Redis中执行,订单产生由消息队列异步写入数据库中,购买成功后返回给客户端状态,客户端进入下一步界面查询订单(订单ID),客户端定时轮询结果
-
主从:
主从情况下读写分离,需要保证库存原子性,业务逻辑通单节点,需要对扣减库存加锁
- 基于Redis Key值的分布式锁
每次只允许一个线程操作,库存减为0时,返回库存不足。
public int createOrderByRedisLock(Integer productId, Integer count) { String key = "stock:" + productId; int stock = (int) redisTemplate.opsForValue().get(key); if (stock <= 0) { throw new StockLackException("库存不足"); } RLock lock = redissonClient.getLock(LOCK_KEY); if (lock.tryLock()) { try { int stock1 = (int) redisTemplate.opsForValue().get(key); if (stock1 >= count) { Order order = new Order(); order.setUserId((int) Thread.currentThread().getId()); order.setProductId(productId); order.setCount(count); order.setOrderTime(new Date()); baseMapper.insert(order); redisTemplate.opsForValue().decrement(key, count); } else { throw new StockLackException("库存不足"); } } finally { lock.unlock(); } } return 1; }-
基于lua 脚本原子操作
原理与分布式锁类似,将库存判断与扣减的过程原子化,省去加锁的过程。 返回 -2 时,表示库存未初始化,需要先初始化库存到缓存中,而且只能有一个线程执行初始化的操作, 所以这里也需要加锁,初始化之前进行一次非空判断,防止重复初始化;初始化完成后,重新校验一次库存。
local key = KEYS[1] -- 获取第一个参数作为键名 local incrementBy = tonumber(ARGV[1]) -- 获取第二个参数作为增量值,并将其转换为数字类型 local stock = redis.call("GET", key) -- 通过GET命令获取键的当前值 if nil == stock or not stock then return -2 -- 库存还未初始化 elseif tonumber(stock) >= incrementBy then return redis.call('DECRBY', key, incrementBy) -- 库存充足 else return -1 -- 库存不足 end -
3、避免脚本,保证数据安全
-
限制用户下单数量
根据业务需要是否限制用户只能购买一个或者两个
-
限制用户访问频率
一秒钟内能否重复购买 ,解决同上限流方案,前端限流防止浏览器脚本,服务器限流防止请求脚本
-
动态URL
通过给URL加盐的方式,计算MD5值生成动态URL返回给客户端
- 预先生成商品的唯一标识符:每个参与秒杀的商品需要有一个唯一标识符,可以是商品的ID或其他唯一标识符。这个标识符将用于生成动态URL。
- 在秒杀开始前,生成动态URL:在秒杀开始之前,你可以使用预先生成的商品标识符结合其他信息(如时间戳、随机数等)生成动态URL。这可以通过在服务器端执行一系列操作来完成,例如使用哈希函数对商品标识符和其他信息进行混淆和加密。
- 提供生成的动态URL给用户:将生成的动态URL提供给参与秒杀的用户。这可以通过将URL返回给用户的浏览器或通过其他通信渠道进行。
- 用户访问动态URL:用户通过点击或访问生成的动态URL来参与秒杀活动。
- 服务器验证URL的有效性:当用户访问动态URL时,服务器可以根据URL中的信息进行验证。这包括解析URL、提取商品标识符和其他相关信息,并进行验证,以确保URL的有效性和合法性。
- 处理秒杀请求:如果URL验证成功,服务器可以根据商品标识符执行相应的秒杀操作,例如扣减库存、记录用户参与等。
四、测试
-
Jmeter分布式压测
- 普通电脑作为压力机的默认最大支持1000左右的并发用户数(线程数),继续增大的话,容易造成卡顿、无响应等情况,这是受限于jmeter其本身的机制和硬件配置。
- 压力测试对CPU和内存的消耗较大,在需要模拟大量并发用户数时,单机很容易出现内存溢出,导致测试瓶颈。
控制机启动时将压测脚本分发到各个执行机节点上,然后通过远程启动各个执行机节点,共同向目标服务器发送请求(产生压力)。测试结束以后,各个执行机节点主动将压测数据回传给控制机节点,由控制机节点统一汇总数据,并输出测试报告。
参考: