整合IDEA+Maven+SSM框架的高并发的商品秒杀项目

整合 IDEA+Maven+SSM 框架的高并发的商品秒杀项目 01 Java 高并发秒杀项目之业务分析与 DAO 层 这是一个整合 IDEA+Maven+SSM 框架的高并发的商品秒杀项目

本文包含相关资料包-----> 点击直达获取<-------

整合 IDEA+Maven+SSM 框架的高并发的商品秒杀项目

01 Java 高并发秒杀项目之业务分析与 DAO 层

这是一个整合 IDEA+Maven+SSM 框架的高并发的商品秒杀项目。我们将分为以下几篇文章来进行详细的讲解:

  • 01 Java 高并发秒杀项目之业务分析与 DAO 层
  • 02 Java 高并发秒杀项目之 Service 层
  • 03 Java 高并发秒杀项目之 Web 层
  • 04 Java 高并发秒杀项目之高并发优化

项目的效果图

秒杀商品列表

秒杀未开始(此时正在倒计时)

开始秒杀提示界面

秒杀结束提示界面

新建一个 Maven 项目

以 IntelliJ IDEA 为例,点击 File > New > Project > Maven

然后点击 Next 继续;

填写相关信息,点击 Next;

最后点击 Finish,完成创建。

如果右下角弹出下面这个提示,点击 Enable Auto-Import

创建 webapp 目录

点击 File > Project Structure

步骤 1

步骤 2

步骤 3

步骤 4

步骤 5

步骤 6

步骤 7

步骤 8

构建 pom 文件

Maven 项目创建好了,接下来我们要添加一些 jar 包的依赖,也就是在 pom.xml 中添加各种开源组件的坐标。

```xml

4.0.0

<groupId>com.nnngu</groupId>
<artifactId>nguSeckill</artifactId>
<version>1.0-SNAPSHOT</version>

    <!-- 代码省略,请参照项目的源代码 -->
    ... ...

```

完整的代码,请查看项目里的 pom.xml 文件

到此,我们项目的初始化工作完成。

秒杀系统业务分析

秒杀系统业务流程如下:

由图可以发现,整个系统其实是针对库存做的系统。用户成功秒杀商品,对于我们系统的操作就是:

  1. 减库存
  2. 记录用户的购买明细。下面看看我们用户对库存的业务分析:

记录用户的秒杀成功信息,我们需要记录:1、谁购买成功了。2、购买成功的时间/有效期。3、付款/发货信息。这些数据组成了用户的秒杀成功信息,也就是用户的购买行为。

为什么我们的系统需要事务?看如下这些故障:1、若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖。2、减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在 MySQL 中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。

MySQL 实现秒杀的难点分析:当用户 A 秒杀 id 为 10 的商品时,此时 MySQL 需要进行的操作是:1、开启事务。2、更新商品的库存信息。3、添加用户的购买明细,包括用户秒杀的商品 id 以及唯一标识用户身份的信息如电话号码等。4、提交事务。若此时有另一个用户 B 也在秒杀这件 id 为 10 的商品,他就需要等待,等待到用户 A 成功秒杀到这件商品然后 MySQL 成功的提交了事务他才能拿到这个 id 为 10 的商品的锁从而进行秒杀,而同一时间是不可能只有用户 B 在等待,肯定是有很多很多的用户都在等待拿到这个行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?在后面第 4 个模块如何进行高并发的优化为大家讲解。

我们这个系统需要完成秒杀的哪些功能?先来看看天猫的一个秒杀库存系统:

大家看了是不是觉得很复杂?当然不用担心,我们只是实现秒杀的一些功能:1、秒杀接口的暴露。2、执行秒杀的操作。3、相关查询,比如说列表查询,详情页查询。我们实现这三个功能即可。接下来进行具体的编码工作,首先是建立数据库以及 Dao 层的编码。

建立数据库

```sql -- 创建一个数据库 CREATE DATABASE ngu_seckill;

-- 使用数据库 USE ngu_seckill;

-- 省略... ... ...

```

完整的数据库 SQL 代码,在项目的 sql 文件夹里的 ngu_seckill.sql

创建实体类

先创建秒杀商品类 com/nnngu/entity/Seckill.java

```java package com.nnngu.entity;

import java.io.Serializable; import java.time.LocalDateTime;

/* * 秒杀商品 / public class Seckill implements Serializable {

private static final long serialVersionUID = 2912164127598660137L;
/* 主键ID*/
private long seckillId;
/*  秒杀商品名字 */
private String name;

/* 代码省略,请参照项目的源代码 */
... ...

```

创建秒杀状态类 com/nnngu/entity/SuccessKilled.java

```java package com.nnngu.entity;

import java.io.Serializable; import java.time.LocalDateTime;

/* * 秒杀后的状态 / public class SuccessKilled implements Serializable { private static final long serialVersionUID = 1834437127882846202L;

private long seckillId;

/* 用户的手机号码*/
private long userPhone;

/* 代码省略,请参照项目的源代码 */
... ...

```

为实体类创建对应的 mapper 接口,也就是 dao 接口

com/nnngu/dao/SeckillMapper.java

```java package com.nnngu.dao;

import com.nnngu.entity.Seckill; import org.apache.ibatis.annotations.Param;

import java.time.LocalDateTime; import java.util.List; import java.util.Map;

public interface SeckillMapper { / 代码省略,请参照项目的源代码 / ... ...

} ```

com/nnngu/dao/SuccessKilledMapper.java

```java package com.nnngu.dao;

import com.nnngu.entity.SuccessKilled; import org.apache.ibatis.annotations.Param;

public interface SuccessKilledMapper { / 代码省略,请参照项目的源代码 / ... ...

}

```

创建对应的 mapper.xml

resources 目录下创建 com.nnngu.dao 包,然后创建 SeckillMapper.xml SuccessKilledMapper.xml ,如下图:

com.nnngu.dao/SeckillMapper.xml

```xml

```

com.nnngu.dao/SuccessKilledMapper.xml

```xml

```

创建 MyBatis 的配置文件 mybatis-config.xml

mybatis-config.xml 配置文件的内容参照项目的源代码

建立连接数据库的配置文件 jdbc.properties

注意: jdbc.properties 里面的属性要根据自己的情况进行修改。

建立 Spring 的 dao 的配置文件

创建 applicationContext-dao.xml 如下图:

applicationContext-dao.xml 文件的具体代码请参照项目的源代码。

测试

创建测试类 com/nnngu/dao/SeckillMapperTest.java 如下图:

该文件的具体代码请参照项目的源代码。

测试结果

测试查询所有商品的方法 queryAll()

测试结果如下:

到此,我们成功完成了 Dao 层开发及测试

02 Java 高并发秒杀项目之 Service 层

首先在编写 Service 层代码前,我们应该首先要知道这一层到底是干什么的。

Service 层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着在 Spring 的配置文件中配置其实现的关联。这样我们就可以在应用中调用 Service 接口来进行业务处理。 Service 层的业务实现,具体要调用到已定义的 dao 层的接口,封装 Service 层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。

在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以 Service 层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用 dao 层的代码,这里我们的 dao 层就是连接数据库的那一层,调用关系可以这样表达:

View(页面) > Controller(控制层) > Service(业务逻辑) > Dao(数据访问) > Database(数据库)

首先还是接口的设计,设计秒杀商品的接口,在 com.nnngu.service.interfaces 包下建立 SeckillService.java 接口文件,如下图:

SeckillService.java 文件里面的内容请参照项目的源代码。

建立好接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回 JSON 数据的话,你是返回什么样的数据好?直接返回一个数字状态码或者文字?这样设计肯定是不好的,所以我们应该向前端返回一个实体信息 JSON,里面包含了一系列的信息,无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是 PO 了,所以我们建立一个 DTO 数据传输类。关于常见的几种对象我的解释如下:

  • PO:也就是我们为每一张数据库表写一个实体类
  • VO:对某个页面或者展现层所需要的数据,封装成一个实体类
  • BO:业务对象
  • DTO:跟 VO 的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
  • POJO:简单的无规则 Java 对象

在 com.nnngu 下建立 dto 包,然后建立 Exposer 类,这个类是秒杀时数据库那边处理的结果的对象

Exposer.java 文件里面的内容请参照项目的源代码。

定义秒杀中可能会出现的异常

定义一个基础的异常,所有的子异常继承这个异常 SeckillException

```java package com.nnngu.exception;

/* * 秒杀基础的异常 * Created by nnngu / public class SeckillException extends RuntimeException { // 代码省略,请参照项目的源代码 ... ... }

```

可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常 SeckillCloseException ,需要继承我们前面写的基础异常

```java package com.nnngu.exception;

/* * 秒杀已经关闭异常,当秒杀结束就会出现这个异常 * Created by nnngu / public class SeckillCloseException extends SeckillException{ // 代码省略,请参照项目的源代码 ... ... }

```

定义重复秒杀异常 RepeatKillException

```java package com.nnngu.exception;

/* * 重复秒杀异常,不需要我们手动去try catch * Created by nnngu / public class RepeatKillException extends SeckillException{ // 代码省略,请参照项目的源代码 ... ... }

```

实现 Service 接口

com.nnngu.service 包下创建 SeckillServiceImpl.java 类,具体代码请参照项目的源代码。

在这里我们捕获了运行时异常,这样做的原因就是 Spring 的事务默认发生了 RuntimeException 才会回滚,可以检测出来的异常是不会导致事务的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理。如果只是为了让编译通过的话,那捕获异常也没意思,所以这里要注意事务的回滚。

然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功,秒杀失败等等,这些字符串是可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在 con.nnngu 包下建立 enums 包,专门放置枚举类,然后再建立 SeckillStatEnum 枚举类。

枚举类 SeckillStatEnum.java 的代码请参照项目的源代码。

注入 Service

resources/spring 下建立 applicationContext-service.xml 文件,用来配置 Service

applicationContext-service.xml 的代码请参照项目的源代码。

在这里开启了基于注解的事务,常见的事务操作有以下几种方法:

  • 在 Spring 早期版本中是使用 ProxyFactoryBean+XMl 方式来配置事务。
  • 在 Spring 配置文件使用 tx:advice+aop 命名空间,好处就是一次配置永久生效,你无须去关心中间出的问题,不过出错了你很难找出在哪里出了问题。
  • 注解@Transactional 的方式,注解可以在方法定义,接口定义,类定义。可以在 public 方法上,但是不能注解在 private、final、static 等方法上,因为 Spring 的事务管理默认是使用 cglib 动态代理的:

  • private 方法因为访问权限限制,无法被子类覆盖

  • final 方法无法被子类覆盖
  • static 时类级别的方法,无法被子类覆盖
  • protected 方法可以被子类覆盖,因此可以被动态字节码增强

不能被 Spring AOP 事务增强的方法

序号 动态代理策略 不能被事务增强的方法
1 基于 JDK 的动态代理 除了 public 以外的所有方法,并且 public static 的方法也不能被增强
2 基于 cglib 的动态代理 private static final 的方法

Service 层的测试

添加测试类 SeckillServiceImplTest.java ,如下图:

SeckillServiceImplTest.java 的代码请参照项目的源代码。

测试结果:

测试的方法: public void getSeckillList()

测试结果如下图:

到此,我们成功完成了 Service 层开发及测试。

03 Java 高并发秒杀项目之 Web 层

前端交互流程设计

对于一个系统,需要产品经理、前端工程师和后端工程师的参与,产品经理将用户的需求做成一个开发文档交给前端工程师和后端工程师,前端工程师为系统完成页面的开发,后端工程师为系统完成业务逻辑的开发。对于我们这个秒杀系统,它的前端交互流程设计如下图:

这个流程图就告诉了我们详情页的流程逻辑,前端工程师根据这个流程图设计页面,而我们后端工程师根据这个流程图开发我们对应的代码。前端交互流程是系统开发中很重要的一部分,接下来进行 Restful 接口设计的学习。

RESTFul 接口设计学习

什么是 RESTFul?它就是一种优雅的 URL 表述方式,用来设计我们资源的访问 URL。通过这个 URL 的设计,我们就可以很自然的感知到这个 URL 代表的是哪种业务场景或者什么样的数据或资源。基于 RESTFul 设计的 URL,对于我们接口的使用者、前端、Web 系统或者搜索引擎甚至是我们的用户,都是非常友好的。关于 RESTFul 的了解大家去网上一搜一大把,我这里就不再做介绍了。下面看看我们这个秒杀系统的 URL 设计:

接下来基于上述资源接口来开始对 Spring MVC 框架的使用。

配置 Spring MVC 框架

web.xml 文件里面引入 DispatcherServlet

web.xml 里面的代码请参照项目的源代码。

添加 applicationContext-web.xml

添加 applicationContext-web.xml ,在下图所示的位置。

applicationContext-web.xml 里面的代码请参照项目的源代码。

这样我们便完成了 Spring MVC 的相关配置(即将 Spring MVC 框架整合到了我们的项目中),接下来就要基于 RESTFul 接口进行我们项目的控制器 SeckillController 的开发工作了。

编写 SeckillController

控制器中的每一个方法都对应我们系统中的一个资源 URL,其设计应该遵循 RESTFul 接口的设计风格。

创建控制器 SeckillController.java ,如下图:

SeckillController.java 里面的代码请参照项目的源代码。

SeckillController.java 中的方法完全是对照 Service 接口方法进行开发的,第一个方法用于访问我们商品的列表页,第二个方法访问商品的详情页,第三个方法用于返回一个 JSON 数据,数据中封装了我们商品的秒杀地址,第四个方法用于封装用户是否秒杀成功的信息,第五个方法用于返回系统当前时间。代码中涉及到一个将返回秒杀商品地址封装为 JSON 数据的类,即 SeckillResult ,在 dto 包中创建它,如下:

建立一个全局 AJAX 请求返回类,返回 JSON

创建 SeckillResult.java ,如下图:

SeckillResult.java 里面的代码请参照项目的源代码。

到此,控制器的开发任务完成,接下来进行我们的页面开发。

页面的编写

项目的前端页面是由 Bootstrap 开发的,所以我们要先去下载 Bootstrap 或者是使用在线 CDN。

使用在线 CDN 的方法:

```html

```

为了方便我们本地调试,我在项目里使用的是本地的 Bootstrap

步骤:

  1. 下载 JQuery ,因为 Bootstrap 就是依赖 JQuery
  2. 下载 Bootstrap
  3. 下载一个倒计时插件 jquery.countdown.min.js ,再下载一个操作 Cookie 插件 jquery.cookie.min.js 如图放置:

  1. 编写一个公共的头部 jsp 文件,位于 WEB-INF/jsp/common 下的 head.jsp ,如下图:

head.jsp 里面的代码请参照项目的源代码。

  1. 编写一个公共的 jstl 标签库文件 tag.jsp ,在下图所示的位置。

tag.jsp 里面的代码请参照项目的源代码。

  1. 编写列表页面 list.jsp ,在下图所示的位置。

list.jsp 里面的代码请参照项目的源代码。

  1. 编写秒杀详情页面 detail.jsp ,在下图所示的位置。

detail.jsp 里面的代码请参照项目的源代码。

添加 seckill.js 文件

添加 seckill.js 文件,在下图所示的位置。

seckill.js 里面的代码请参照项目的源代码。

运行项目

运行项目,部署到 tomcat ,在浏览器地址栏输入 http://localhost:8080/seckill/list ,敲回车,即可看到如下图的界面:

点击相应商品后面的详情页链接即可查看该商品是否开启秒杀、以及秒杀该商品等活动。

到此,我们成功完成了 Web 层的开发。但一个秒杀系统,往往是会有成千上万的人进行参与,我们目前的系统是抗不起多少高并发操作的,所以后面我们会对本系统进行高并发的优化。请查看我的下一篇文章。

04 Java 高并发秒杀项目之高并发优化

关于并发

并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS(Query Per Second 每秒查询率)和事务执行的时间有密切关系,事务执行时间越短,并发性越高,这也是要将费时的 IO 操作移出事务的原因。

项目中的高并发发生在哪?

下图中,红色的部分就表示会发生高并发的地方,绿色部分表示对于高并发没有影响。

为什么要单独获取系统时间?

这是为了我们的秒杀系统的优化做铺垫。比如在秒杀还未开始的时候,用户大量刷新秒杀商品详情页面是很正常的情况,这时候秒杀还未开始,大量的请求发送到服务器会造成不必要的负担。

我们将这个详情页放置到 CDN 中,这样用户在访问该页面时就不需要访问我们的服务器了,起到了降低服务器压力的作用。而 CDN 中存储的是静态化的详情页和一些静态资源(CSS,js 等),这样我们就拿不到系统的时间来进行秒杀时段的控制,所以我们需要单独设计一个请求来获取我们服务器的系统时间。

CDN(Content Delivery Network)的理解

获取系统时间不需要优化

因为 Java 访问一次内存(Cacheline)大约 10ns,1s=10 亿 ns,也就是如果不考虑 GC,这个操作 1s 可以做 1 亿次。

秒杀地址接口分析

  • 无法使用 CDN 缓存,因为 CDN 适合请求对应的资源不变化的,比如静态资源、JavaScript;秒杀地址返回的数据是变化的,不适合放在 CDN 缓存;
  • 适合服务端缓存:Redis 等,1 秒钟可以承受 10 万 QPS。多个 Redis 组成集群,可以到 100 万个 QPS。所以后端缓存可以用业务系统控制。

秒杀地址接口优化

秒杀操作优化分析

  • 无法使用 cdn 缓存
  • 后端缓存困难: 库存问题
  • 一行数据竞争:热门商品

大部分写的操作和核心操作无法使用 CDN,也不可能在缓存中减库存。你在 Redis 中减库存,那么用户也可能通过缓存来减库存,这样库存会不一致,所以要通过 MySQL 的事务来保证一致性。

比如一个热门商品所有人都在抢,那么会在同一时间对数据表中的一行数据进行大量的 update set 操作。

行级锁在 commit 之后才释放,所以优化方向是减少行级锁的持有时间。

延迟问题很关键

  • 同城机房网络(0.5ms~2ms),最高并发性是 1000qps。
  • update 后 JVM -GC(垃圾回收机制)大约 50ms,最高并发性是 20qps。并发性越高,GC 就越可能发生,虽然不一定每次都会发生,但一定会发生。
  • 异地机房,比如北京到上海之间的网络延迟,经过计算大概 13~20ms。

如何判断 update 更新库存成功?

有两个条件:

  • update 自身没报错;
  • 客户端确认 update 影响记录数

优化思路:把客户端逻辑放到 MySQL 服务端,避免网络延迟和 GC 影响

如何把客户端逻辑放到 MySQL 服务端

有两种方案:

  • 定制 SQL 方案,在每次 update 后都会自动提交,但需要修改 MySQL 源码,成本很高,不是大公司(BAT 等)一般不会使用这种方法。
  • 使用存储过程:整个事务在 MySQL 端完成,用存储过程写业务逻辑,服务端负责调用。
接下来先分析第一种方案

根据上图的成本分析,我们的秒杀系统采用第二种方案,即使用存储过程。
优化总结
  • 前端控制。暴露接口,按钮防重复(点击一次按钮后就变成灰色,禁止重复点击按钮)
  • 动静态数据分离。CDN 缓存,后端缓存
  • 事务竞争优化。减少事务行级锁的持有时间

下载安装 Redis

Redis 是一个开源的、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库

下载安装 Redis 的步骤,搜索引擎能找到相关的资料,本文不做展开。

下载安装完 Redis 之后就可以继续进行操作。

使用 Java 操作 Redis

Java 操作 Redis 使用的是 jedis 包。

pom.xml 添加 jedis 的依赖,如下图:

添加 protostuff-core 以及 protostuff-runtime 序列化 jar 包,如下图:

序列化是处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象在网络间传输。反序列化就是将流化后的对象重新转化成原来的对象。

在 Java 中内置了序列化机制,通过 implements Serializable 来标识一个对象实现了序列化接口,不过其性能并不高。

建立操作 Redis 的 dao 类

原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在 Redis,在查询秒杀商品时先去 Redis 缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入 Redis 缓存中,这样下次就可以直接去缓存中直接查询到。

添加 RedisDao.java 文件,位于下图所示的位置:

RedisDao.java 文件里面的代码请参照项目的源代码。

在 applicationContext-dao.xml 中注入 redisDao

applicationContext-dao.xml 中添加下图所示的内容:

改造 exportSeckillUrl 方法:

修改 SeckillServiceImpl.java 文件中的 exportSeckillUrl 方法:

```java /* * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址 * * @param seckillId 秒杀商品Id * @return 根据对应的状态返回对应的状态实体 / @Override public Exposer exportSeckillUrl(long seckillId) {

    Seckill seckill = redisDao.getSeckill(seckillId);
    if (seckill == null) {
        // 访问数据库读取数据
        seckill = seckillMapper.queryById(seckillId);
        if (seckill == null) {
            return new Exposer(false, seckillId);
        } else {
            // 放入redis
            redisDao.putSeckill(seckill);
        }
    }

    // 判断是否还没到秒杀时间或者是过了秒杀时间
    Date startTime = seckill.getStartTime();
    Date endTime = seckill.getEndTime();
    Date nowTime = new Date();
    // 开始时间大于现在的时候说明没有开始秒杀活动;秒杀活动结束时间小于现在的时间说明秒杀已经结束了
    if (nowTime.getTime() > startTime.getTime() && nowTime.getTime() < endTime.getTime()) {
        //秒杀开启,返回秒杀商品的id,用给接口加密的md5
        String md5 = getMd5(seckillId);
        return new Exposer(true, md5, seckillId);
    }
    return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());

}

```

简单的优化

以前的实现流程:

用户的秒杀操作分为两步:减库存、插入购买明细,我们在这里进行简单的优化,就是将原本先 update(减库存)再进行 insert(插入购买明细)的步骤改成:先 insert 再 update。

为什么要先 insert 再 update

  • 首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。
  • 在这里,插入是可以并行的,而更新由于会加行级锁是串行的。
  • 也就是说是如果更新在前,加锁和释放锁之间两次的网络延迟和 GC,如果更新在后,则加锁和释放锁之间只有一次的网络延迟和 GC,也就是减少的持有锁的时间。
  • 这里先 insert 并不是忽略了库存不足的情况,而是因为 insert 和 update 是在同一个事务里,光是 insert 并不一定会提交,只有在 update 成功才会提交,所以并不会造成过量插入秒杀成功记录。

去代码里改造执行秒杀的 executeSeckill() 方法,优化性能。

深度优化(使用存储过程)

前边通过调整 insert 和 update 的执行顺序来实现简单的优化,但依然存在着 Java 客户端和服务器通信时的网络延迟和 GC 影响,我们可以将执行秒杀操作时的 insert 和 update 放到 MySQL 服务端的存储过程里,而 Java 客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的 GC 影响。另外,由于我们使用了存储过程,也就用不到 Spring 的事务管理了,因为在存储过程里我们会直接启用一个事务。

去 MySQL 的控制台执行储存过程 procedure.sql 里面的代码

去 MySQL 的控制台执行储存过程 procedure.sql 里面的代码。

procedure.sql 文件位于项目的 SQL 目录下。

注意点:在存储过程中,row_count() 函数用来返回上一条 SQL(delete, insert, update)影响的行数。

根据 row_count() 的返回值,可以进行接下来的流程判断:

0 :未修改数据;

>0 :表示修改的行数;

<0 :表示 SQL 错误或未执行修改 SQL

修改源码以调用存储过程

SeckillMapper.java 接口中声明 killByProcedure() 方法

java /** * 使用储存过程执行秒杀 * * @param paramMap */ void killByProcedure(Map<String, Object> paramMap);

然后在 SeckillMapper.xml 中写 sql 语句,具体代码请参照项目的源代码。

接着在 SeckillService.java 接口中声明 executeSeckillProcedure() 方法

在 pom.xml 中添加 commons-collections 的依赖,如下图:

然后在 SeckillServiceImpl.java 中实现 executeSeckillProcedure() 方法。

SeckillServiceImplTest.java 中编写测试方法 executeSeckillProcedureTest()

测试结果:

修改 SeckillController.java 中的 execute() 方法,把一开始调用普通方法的改成调用储存过程的方法。

存储过程优化总结

  • 存储过程优化:事务行级锁持有的时间
  • 不要过度依赖存储过程
  • 简单的逻辑依赖存储过程
  • QPS:一个秒杀单 6000/qps

经过简单优化和深度优化之后,本项目大概能达到一个秒杀单 6000qps,这个数据对于一个秒杀商品来说其实已经挺 ok 了,注意这里是指同一个秒杀商品 6000qps,如果是不同商品不存在行级锁竞争的问题。

系统部署架构

CDN:放置一些静态化资源,或者可以将动态数据分离。一些 js 依赖直接用公网的 CDN,自己开发的一些页面也做静态化处理推送到 CDN。用户在 CDN 获取到的数据不需要再访问我们的服务器,动静态分离可以降低服务器请求量。比如秒杀详情页,做成 HTML 放在 CDN 上,动态数据可以通过 AJAX 请求后台获取。

Nginx:作为 http 服务器,响应客户请求,为后端的 servlet 容器做反向代理,以达到负载均衡的效果。

Redis:用来做服务器端的缓存,通过 Jedis 提供的 API 来达到热点数据的一个快速存取的过程,减少数据库的请求量。

MySQL:保证秒杀过程的数据一致性与完整性。

智能 DNS 解析 + 智能 CDN 加速 +Nginx 并发 +Redis 缓存 +MySQL 分库分表,如下图:

大型系统部署架构,逻辑集群就是开发的部分。

  • Nginx 做负载均衡
  • 分库分表:在秒杀系统中,一般通过关键的秒杀商品 id 取模进行分库分表,以 512 为一张表,1024 为一张表。分库分表一般采用开源架构,如阿里巴巴的 tddl 分库分表框架。
  • 统计分析:一般使用 hadoop 等架构进行分析

在这样一个架构中,可能参与的角色如下:

到此,该项目已经全部完成。

参考文献

  • 一个基于微服务的个性化电商系统的设计与实现(华中科技大学·胡梦婷)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 基于SSM框架的网上商城系统的设计与实现(西安电子科技大学·薄志强)
  • 基于SSM框架的B2C网上商城系统的设计与实现(湖南大学·陈峰)
  • 基于SSM框架的B2C网上商城系统的设计与实现(湖南大学·陈峰)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 一个基于微服务的个性化电商系统的设计与实现(华中科技大学·胡梦婷)
  • 海量并发下高可用库存中心的设计与实现(北京交通大学·罗玲玲)
  • 基于SSM框架的B2C网上商城系统的设计与实现(湖南大学·陈峰)
  • 基于SSM框架的网购商城的设计与实现(北京交通大学·朱重佳)
  • 基于SSM框架的网购商城的设计与实现(北京交通大学·朱重佳)
  • 基于SSM框架的网购商城的设计与实现(北京交通大学·朱重佳)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 基于SSM框架的网购商城的设计与实现(北京交通大学·朱重佳)
  • 基于SSM框架的网购商城的设计与实现(北京交通大学·朱重佳)

本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:毕设工坊 ,原文地址:https://m.bishedaima.com/yuanma/35838.html

相关推荐

发表回复

登录后才能评论