Java 商品抢购系统设计,实现与优化

Java 商品抢购系统设计,实现与优化

参考教程:https://www.bilibili.com/video/BV1sf4y1L7KE

0. 使用技术

Java 1.8
SpringBoot 2.7.4
RabbitMQ 3.11
Redis 6.2.7
Mybatis-plus 3.5.2
MySQL 5.7

1. 流程

  • 功能开发
  • 系统压测
  • 分布式会话
  • 优化
    • 页面优化
    • 服务优化
    • 安全优化

2. 高并发带来的一些问题

可能会遇到黄牛,大量并发写和并发读。

因此设计系统要保证高可用保证数据一致性高性能

3. 参考配置

Druid 数据源配置属性说明

MybatisX逆向工程

Mybatis 执行器

4. 分布式 Session 问题

多台 Tomcat 配合 Nginx 出现用户登录的问题。

Nginx 使用默认的负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。

比如,首先用户在 Tomcat1 上登陆后,用户信息保存在 Tomcat1 的 Session 中。过了一会请求被 Nginx 分发到了 Tomcat2 上,因为 Tomcat2 上的 Session 没有用户信息,此时又要登录。

4.1 可使用的解决方案

4.1.1 Session复制

优点:

  • 无需修改代码,只需要修改 Tomcat 配置

缺点:

  • Session 同步传输占用内网贷款
  • 多台 Tomcat 同步性能指数下降
  • Session 占用内存,无法有效水平扩展
    • 我加机器就是为了扩展内存,你这 Session 同步,每台机器还是占用那么多内存,太冗余了
4.1.2 前端存储

优点:

  • 不占服务器内存

缺点:

  • 存在安全风险
    • Cookie 是明文的,不安全
  • 数据大小受到 Cookie 限制
  • 占用外网带宽
    • 前端到后端传送对象也需要占用带宽的
4.1.3 Session 粘滞

优点:

  • 无需修改代码
  • 服务端可以水平扩展

缺点:

  • 增加新机器,会重新 Hash,导致重新登陆
  • 应用重启,需要重新登陆
4.1.4 后端集中存储

优点:

  • 安全
  • 容易水平扩展

缺点:

  • 增加复杂度
  • 需要修改代码

4.2 选择第四种方案(Redis)

docker 安装 Redis 6.2.7

客户端使用 Another Redis Desktop Manager 1.5.8

4.2.1 示例
set key value [ex 10 | px 10000] [nx | xx]
  • nx 表示当不存在该 key 设置成功
  • xx 表示存在该 key 设置成功
  • ex 10 十秒后过期
  • px 10000 一万毫秒(即十秒)后过期

4.3 SpringSession 实现方式(抛弃这种实现)

当执行 session.setAttribute() 时会自动导入 Redis
在这里插入图片描述

4.4 直接用 Redis 存储信息的实现方式(使用该实现)

首先实现序列化方法,默认的 JDK 序列方法可读性有点差。采用 Jackson 的序列化。

首次登录先存 Cookie,然后在 Redis 中存储 {“user:cookie”:userObject},下一次访问的时候直接拿着本地的 Cookie 去 Redis 取信息。

如果每个页面都需要判断是否登录太麻烦了。如何解决?

  • 先配置用户参数解析器,在解析器中通过 cookie 向 Redis 中取信息,然后直接将对象变为控制器的入参即可。

5. 优化前 Jmeter 压测记录

5.0 测试环境

  1. Windows 10,Intel i7 7700HQ,内存 16G
  2. Linux CentOS 7(虚拟机环境)4 核心,内存 4G

redis 单节点,mysql

5.1 获取商品列表信息

设置线程数 5000,每个线程重复请求 10 次。

重复此操作 3 次。

5.1.1 Windows 测试

QPS:1498.9

在这里插入图片描述

5.1.2 Linux 测试

QPS:832.8

在这里插入图片描述

5.2 秒杀

设置商品为 10。

设置线程数 1000,每个线程重复请求 10 次。

重复此操作 3 次。

5.2.1 Windows 测试

QPS:1111.9

在这里插入图片描述

可以看到数据库中超卖了 2 件:

在这里插入图片描述

订单表和秒杀订单表中的数据条数各有 338 条:

在这里插入图片描述

5.2.2 Linux 测试

QPS:812.1

在这里插入图片描述

数据中超卖了 18 件:

在这里插入图片描述

订单表和秒杀订单表中的数据条数各有 196 条数据:

在这里插入图片描述

5.3 总结

可以看到在并发环境下会出现超卖问题,以及商品库存小于 1 的时候还会加入订单。接下来将基于此进行优化,解决线程同步以及提高 QPS。

6. 初次优化

6.1 对象 JSON 缓存

每次获取商品列表或者某件商品的信息的时候,先从 Redis 中取,若 Redis 中没有,再去数据库找,并加入 Redis 缓存中。

// 先从 Redis 中取缓存, 若返回的对象不为空则直接返回对象
ValueOperations valueOperations = redisTemplate.opsForValue();
List<GoodsVo> goodList = (List<GoodsVo>) valueOperations.get("goodsList");
if (!ObjectUtils.isEmpty(goodList)) {
    return  goodList;
}
// 若 Redis 中没有则从数据库加载
List<GoodsVo> list = goodsService.findGoodVo();
// 设置失效时间 5 min
valueOperations.set("goodsList", list, 5, TimeUnit.MINUTES);

return list;

6.2 解决数据库超卖问题以及将下单信息加入缓存

  • 在数据库中将 seckill_order 表中 user_id 字段和 goods_id 字段组成联合唯一索引,解决同一个用户多次秒杀同一件商品。
  • 判定库存为 < 1 时,抛出售空的异常。
  • 将下单信息加入 Redis 缓存,多次购买同一件商品时直接从 Redis 中获取。

7. 加入对象 JSON 缓存,解决数据库超卖问题以及将下单信息加入缓存 Jmeter 压测记录

7.1 获取商品列表测试

设置线程数 5000,每个线程重复请求 10 次。

重复此操作 3 次。

7.1.1 Windows 测试

QPS:3171.3

在这里插入图片描述

7.1.2 Linux 测试

QPS:2333.6
在这里插入图片描述

7.2 秒杀

设置线程数 1000,每个线程重复请求 10 次。

重复此操作 3 次。

7.2.1 Windows 测试

QPS:1421.1

在这里插入图片描述

7.2.2 Linux 测试

QPS:1064.8

在这里插入图片描述

7.3 总结

在这里插入图片描述
在这里插入图片描述

相对于之前没有缓存的情况有明显的提升,并且解决了超卖问题,以及解决了同一个用户只能购买一件同样的商品。

8. 中间件

应用程序(可以是不同编程语言开发)使用某种协议或者说规范与 OS 底层进行通信。这种规范就是中间件,中间件具有跨平台,支持通信,高可用,持久性等特点。

8.1 消息中间件

利用可靠的消息传递机制进行系统和系统直接的通讯,通过提供消息传递和西澳西的排队机制,它可以在分布式系统环境下,扩展进程间的通信。

本质
  • 接收数据
  • 接受请求
  • 存储数据
  • 发送请求等功能的技术服务

同时性能要比普通的服务和技术要高。

组成部分
  1. 消息的协议
  2. 消息的持久化机制
  3. 消息的分发策略
  4. 消息的高可用,高可靠
  5. 消息的容错机制
应用场景

加入有 10W 的并发请求下订单,我们可以在这些订单入库之前把订单请求堆积到消息队列中,让它稳健可靠的入库和执行。

协议

AMQP,高级消息队列协议

8.2 RabbitMQ

RabbitMQ 中消息传递模型的核心思想是:生产者永远不会直接向队列发送任何消息。生产者常常根本不知道消息是否会被传递到任何队列。

生产者只能将消息发送到交换器(exchange)。

8.2.1 交换器(Exchange)

交换器职责:

  • 接收来自生产者的消息
  • 将这些消息推送到队列
  • 交换器必须确切地知道如何处理它接收到的消息
    • 它应该被追加到特定的队列中吗?
    • 它应该被追加到许多队列中吗?
    • 还是应该被丢弃?

以上规则由交换机类型定义,类型有:directtopicheadersfanout

RabbitMQ 默认交换机:
在这里插入图片描述

8.2.1.1 Fanout 模式

类似发布订阅,也有点像广播,多个消费者会受到同一个发布者发送的同一个消息。
在这里插入图片描述

8.2.1.2 Direct 模式(常用)

有选择地接收信息。

绑定的时候可以采取一个额外的 routingKey 参数。为了避免与 basic_publish 参数相混淆,我们要把它称为绑定键。

Fanout 模式中是将消息传给所有的队列,Direct 模式可以指定不同的路由键的消息发送给指定消费者。

在这里插入图片描述

8.2.1.3 Topic 模式(常用)

根据一个模式(主题)接收信息。

可以指定 RoutingKey 包含某个关键词的队列进行发送。带来更多的灵活性。

RoutingKey 中可以有任意多的单词,最多不能超过 255 字节的限制。

在这里插入图片描述

  • * 表示匹配一个词
  • # 表示匹配 0 个或者多个词

当一个队列用 # 绑定键绑定时–它将收到所有的消息,不管 RoutingKey 是什么,就好像在 Fanout 交换机中一样。

8.2.1.4 Header 模式

可以指定哪些指定 key 的 value 值进行发送。(用的不多)

9. 接口优化

  1. 系统初始化的时候加载全部商品 id 和库存数量到 Redis,并初始化全部商品库存为空的标记全为 false
  2. 通过 Redis 预减库存,减少数据库的访问,若库存为 0,则将库存为空的标记置为 true
  3. 请求进入队列缓存,异步下单(告诉客户端正在排队中)

10. 使用 Redis 预减库存,使用消息队列通知数据库处理订单,Jmeter 秒杀压测

设置线程数 1000,每个线程重复请求 10 次。

重复此操作 3 次。

10.1 Windows 测试

QPS:2196.5

在这里插入图片描述

10.2 Linux 测试

QPS:1784.7

在这里插入图片描述

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇咔咔负负得正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值