基于SSM实现高并发秒杀API课程笔记

课程概述

为什么使用SSM框架

  • 易用且轻便
  • 互联网公司常用
  • 低业务代码侵入性
  • 成熟的社区和用户群

为什么选用秒杀类系统进行讲解

  • 秒杀类业务场景具有典型的”事务”特性
  • 秒杀和红包类需求越来越常见
  • 面试常问问题

事务的四大特性

原子性(atomicity):

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(consistency):

数据库总是从一个一致性的状态转换到另一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、四条语句之间时系统崩溃,前面执行的一、二语句也不会生效。因为事务最终没有提交,所以事务中所做的修改都不会保存到数据库中。

隔离性(isolation):

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。当执行第三条语句、第四条语句还未开始时,此时有另外一个程序开始运行,则看不到第三条语句做出的改变。

持久性(durability):

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证策略。

相关技术要求

MySQL

  • SQL技巧
  • 事务和行级锁
  • 表设计(手写代码)

MyBatis

  • DAO层设计与开发
  • MyBatis合理使用
  • MyBatis与Spring整合

Spring

  • Spring IOC整合Service
  • 声明式事务运用

Spring MVC

  • 框架运作流程
  • Restful接口设计和使用
  • Controller开发技巧

前端

  • 交互设计
  • Bootstrap
  • JQuery

高并发

  • 高并发点和高并发分析
  • 优化思路并实现

学习成果

  • SSM整合与使用
  • 秒杀类系统需求理解与实现
  • 常用场景高并发解决方案

SSM整合与使用

基于Maven创建项目

要点

注意:archetype:create这个goal已在新版本Maven中弃用。

  1. pom.xml是Maven项目的配置文件。
  2. 使用Maven创建的项目中的web.xml中Servlet版本可能过低,我们通过复制Servlet容器服务器如Tomcat的实例目录中的WEB-INF中的web.xml高版本的文件头进行替换。
  3. 补全项目目录(src-main-java、src-test、src-test-java和src-test-resources)。
  4. 改变pom.xml下默认junit依赖版本为4.11,因为junit3使用编程方式测试,而junit4使用注解方式进行测试。
  5. 补全项目依赖。
  • 日志(slf4j,log4j,logback,common-logging):slf4j是规范/接口;log4j,logback,common-logging是日志实现;\
    常用组合:slf4j + logback。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.12</version>
    </dependency>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.1.1</version>
    </dependency>
    <!-- 实现slf4j接口并整合进来,编程时只需使用slf4j(使用其他日志实现时也会有个类似这样进行整合的依赖,所以编程时都能统一使用slf4j) -->
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.1.1</version>
    </dependency>
  • 数据库(数据库驱动和数据库连接池)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.35</version>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>c3p0</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.1.2</version>
    </dependency>
  • DAO框架(MyBatis本身依赖和MyBatis与Spring整合依赖)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.3.0</version>
    </dependency>
    <!-- MyBatis自身实现的与Spring之间的整合依赖 -->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.3</version>
    </dependency>
  • Servlet Web相关依赖(JSP相关标签库,jackson和Servlet依赖)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
    </dependency>
    <dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    </dependency>
    <!-- jackson依赖 -->
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.4</version>
    </dependency>
    <!-- Servlet依赖 -->
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    </dependency>
  • Spring依赖(4大方面)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    <!--1) Spring核心依赖 -->

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>
    <!-- 包含SpringIOC依赖 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>
    <!-- Spring扩展(如包扫描)依赖 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>

    <!--2) Spring DAO层依赖 -->
    <!-- 需要Spring-jdbc提供的事务管理器 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>
    <!-- 需要Spring-tx提供的声明式事务 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>

    <!-- 3) Spring Web相关依赖(容器需要加载Spring IOC和Spring AOP来启动Spring的工厂) -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.1.7.RELEASE<nversion>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>

    <!-- 4) Spring test相关依赖,方便我们使用junit做单元测试和集成测试 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.1.7.RELEASE</version>
    </dependency>

业务分析与DAO层

秒杀业务分析

秒杀业务的核心:对库存的处理

用户针对库存业务分析

用户购买行为

关于数据落地

不加事务会造成超卖和少卖的情况
NoSQL相比MySQL,虽然分化注重了高可用、高并发和分布式的特性,但是事务支持性差。

MySQL实现秒杀系统难点

多个用户参与活动时产生竞争,竞争反应到MySQL背后的技术是事务和行级锁。
这一事务由Start Transaction、Upate 库存数量、Insert 购买明细和Commit组成。
主要的行级锁位于Update 库存数量时。而系统的难点就是如何高效的去处理这样的竞争。

系统要实现的秒杀功能

  • 秒杀相关查询
  • 秒杀接口暴露
  • 执行秒杀

代码开发阶段

  1. DAO设计编码(数据库表设计、DAO接口和MyBatis实现DAO)
  2. Service设计编码(Service接口、Spring管理Service和通过声明式事务简化事务控制)
  3. Web设计编码(Restful接口和前端交互)

数据库表设计

  1. 手写DDL让同行间的交流更加方便。
  2. MySQL提供可选的引擎有很多,但支持事务的只有InnoDB。
  3. MySQL 5.7 及以后的版本中explicit_defaults_for_timestamp的值默认都是OFF。

DAO层接口设计和编码

已设计的数据表对应Java中的实体。
给实体和DAO接口创建好包。

实体设计

注意变通,在SuccessKilled实体中需要拿到Seckill实体,其实这个意味着多对一的复合属性。

DAO接口设计

  1. DAO层关注着数据库操作。
  2. 接口命名规范:实体名+DAO。
  3. 接口设计其实对应着数据的增删改查,并且接口内方法的设计也有相应规范。
  4. 具体接口的实现可以用JDBC或Hibernate或MyBatis。

基于MyBatis实现DAO理论

  1. MyBatis完成的是实体对象与数据库间的映射工作(OR Mapping)。
  2. MyBatis需要我们提供参数和SQL(Hibernate没有),之后它返回给我们结果。
  3. SQL可以写在xml文件中或着注解当中,写在xml文件中比较灵活轻便。
  4. DAO接口可以通过MyBatis中的Mapper机制自动实现(推荐),也可以通过API编程方式自己实现接口。

基于MyBatis实现DAO编程

  1. 在src-resources目录下创建全局的MyBatis配置文件mybatis-config.xml。
  2. 在src-resources目录下创建放置MyBatis映射文件的mapper目录。
  3. 从MyBatis官网获取配置头文件。
  4. 设置useGeneratedKeys的值为true,让MyBatis可以使用JDBC的getGeneratedKeys获取数据库自增主键值。
  5. 设置可以使用列别名替换列名使用(useColumnLabel默认为true),这样MyBatis自动帮我们实现实体属性与表属性间的转换(select name as title from table)。
  6. 设置开启(mapUnderscoreToCamelCase)驼峰命名转换(Table(create_time) -> Entity(createTime))。
  7. mapper目录中的xml文件能为DAO接口方法提供SQL语句配置。
  8. xml文件中有相应标签(insert、delete、update和select)对应方法要实现的增删改查。
  9. xml文件中不允许有<=这样的语法,我们在写SQL语句时可以用<![CDATA[<=]]>来告诉xml这里的<=不是xml的语法,这样就能用了。
  10. resultType类型如果是自创的实体,在表示的时候可以先不写包名只写类名,因为后面的配置中可以给出一个包名的环境变量。
  11. 如果方法中的返回类型是List这样的泛型,在写resultType时直接写List里面包含的对象类型,如这里的项目中是List包含的是自创的实体类型Seckill。
  12. SQL语句中可以用limit来限制查询结果的行数。
  13. insert时如果主键重复会报错,我们可以忽略主键重复(ignore)报错而返回0,让我们更好的去处理这个逻辑。
  14. 用inner join … on …链接可以将一个实体属性带入另一个实体。
  15. 这样的链接语句其实也是体现MyBatis技巧的一个载体(它能告诉MyBatis把结果映射到两个不同实体)。
  16. 表的别名设置时可以省略as关键字,表的别名可以与驼峰命名转换机制组合使用。
  17. MyBatis可以通过列别名与当前实体中另一实体类对象属性组合的方式来映射到另一个实体。
  18. 上述的列别名组合实际上可以理解为一个el表达式。
  19. MyBatis相对其他ORM框架的优势在于可以让我们自由控制SQL,完美地发挥我们工程师的SQL技巧。

MyBatis整合Spring理论

整合目标
  1. 更少的编码
    体现:只写接口,不写实现类(MyBatis为我们实现)。
    理由:接口就能告诉我们很多事情:方法的结果集、行为和参数。
  2. 更少的配置
    体现:自动实现DAO实现类并注入到Spring容器。
  3. 足够的灵活
    体现:整合之后我们依然可以灵活的自己定制SQL。
整合步骤
  1. 别名的设置:MyBatis帮我们实现了package scan的功能,使得我们不需写包名就能表示一个类。
  2. MyBatis还实现了自动扫描配置文件的功能,所以不需要我们额外再告诉MyBatis项目中的mapper目录下放置的是DAO对应的SQL配置文件。
  3. 一般情况DAO实现类的配置也要写到xml文件中以告诉Spring容器,而这里MyBatis为我们自动实现DAO接口且MyBatis与Spring整合后可以自动将这个告知的步骤也实现。

MyBatis整合Spring编码

  1. 创建main-resources-spring目录,用来存放Spring相关配置。
  2. 新建spring-dao.xml存放DAO相关配置,从官网下载PDF格式官方文档,找到容器相关章节,复制Spring的xml配置文件头。
  3. (1)配置数据库相关参数,写在properties文件(jdbc.properties)下。Spring配置支持classpath:前缀(指的是main-java和main-resources目录)和${url}(用来获取properties文件中的属性)。

jdbc.properties文件的配置代码:

1
2
3
4
driver = com.mysql.jdbc.Driver  
url = jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8
username = root
password = hgneer

  1. 在spring-dao.xml文件中配置数据库连接池(bean id = “dataSource”),配置连接池属性(driverClass、jdbcUrl、user和password)和
    连接池私有属性:maxPoolsize(30)、minPoolSize(10)、autoCommitOnClose(把连接放到池中前需要一些清理工作,设置为false,使得当关闭连接时不要commit)
    、checkoutTimeout(获取连接超时时间,一般设置为1000,否则默认是0就会无限等待)和acquireRetryAttempts(获取连接失败重试次数,一般设置为2)。
  2. 配置MyBatis中最重要的一个API(bean id = “sqlSessionFatory”):SqlSessionFactory对象用来创建会话工厂。

有以下要点:

  • 输入数据库连接池。
  • 配置MyBatis全局配置文件为mybatis-config.xml。
  • 配置扫描entity包(如果要省略多个包可以用分号隔开即可),使用别名。
  • 配置自动扫描sql配置文件:mapper需要的xml文件。
  1. 配置扫描DAO接口包,使得动态实现DAO接口,注入到Spring容器中。

有以下要点(class = “org.mybatis.spring.mapper.MapperScannerConfigurer”):

  • 注入sqlSessionFactory,注意使用sqlSessionFactoryBeanName,这样只有当我们使用时才加载,防止初始化滞后而导致的错误。
  • 给出需要扫描的DAO接口包位置,以便于后面的MyBatis动态实现和自动注入。

总结:

  1. MyBatis与Spring整合本质上是一些配置。
  2. 约定大于配置的启示,在特定包下新增特定类型文件,框架帮助自动识别,不需要额外配置。

DAO层单元测试编码与问题排查

  1. 单元测试代码放在test-java目录。
  2. 因为DAO的实现类是MyBatis自动实现和注入到Spring容器的,所以在测试之前需要配置Spring与Junit整合。
  • 让Junit启动时加载SpringIOC容器,使用spring-runner提供了@RunWith(SpringJUnit4ClassRunner.class)。
  • 告诉JUnit Spring DAO配置文件,使用@ContextConfiguration({“classpath:”})。
  • 注入DAO实现类依赖,使用
    @Resource(依赖注入注解)
    private SeckillDao seckillDao;
  1. 因为Java不会保存形参的记录,只以org0,org1这种形式表示,当方法只有一个参数时没事,但在DAO接口方法有多个参数时注意用MyBatis提供的@Param(“offset”)来标识,否则在SQL语句执行时找不到对应的参数。
  2. 阶段性测试,现在是测试数据绑定间是否存在问题,等Service写完再测逻辑层面的问题。
  3. 控制台信息可以复制到编辑版面进行可视化,更好地定位错误。

Service层

完成DAO层后的思考

  • DAO层没写一行逻辑代码,让代码与SQL分离,方便Review。
  • DAO层工作演变为:接口设计+SQL编写。
  • DAO层拼接等逻辑在Service层完成。

Service层设计和开发

Service接口设计

  1. 创建Service层所需要的包:service(存放Service接口和实现类)、exception(存放异常)和dto(存放表示数据的一些类型,关注Web层和Service层的数据传递)。
  2. 站在”使用者”角度去设计接口,设计接口时不要去关注实现。从三个方面来站在”使用者”角度设计接口:方法定义粒度要明确、参数要简练直接和返回类型要友好(有时返回的类型不适合用一个entity,就需要额外定义的dto类)(return 类型/异常)。
  3. 比如要有一个控制暴露秒杀地址的接口方法设计,这个方法在设计时根据需求要设计一个dto类作为返回类型。
  4. dto设计使用md5作为加密措施。做不同的Constructor是为了方便我们进行不同初始化。
  5. 自己在exception目录下设计异常类,要分清楚运行期异常(运行期异常不用try-catch)和编译期异常,但是Spring的声明式事务只接收运行期异常回滚策略(所以我们创建的异常类继承RuntimeException,并生成message的两个构造方法)。
  6. 另外,一般情况,使用Spring,我们可以做一个通用的异常,然后让其它异常继承这个异常,当然,这个异常是继承自RuntimeException。

Service接口实现

  1. 创建service-impl作为实现类的包,实现类的命名是接口名+Impl。
  2. 在具体实现接口时,需要用到DAO类的配合,所以在实现类中声明需要的DAO类对象,但不用初始化,因为DAO类是MyBatis自动实现和注入Spring容器的。
  3. 另外,实现类编写时还需要引入日志对象(这里用的slf4j)。
    (private Logger logger = LoggerFactory.getLogger(this.getClass()))。
  4. 系统的当前时间new一下就是:Date nowTime = new Date(),且nowTime.getTime()是表示毫秒个数的一个long类型的数字。
  5. md5如何生成:加入一个混淆过程将用户特定字符串进行转换,所以先创建一个md5盐值字符串,用于混淆制作md5,然后创建一个方法,在这个方法里面调用Spring为我们提供的专门生成md5的工具类方法(DigestUtils.md5DigestAsHex(base.getBytes()))。
  6. 如果有重用的点,那抽象出一个方法出来就没毛病。
  7. 异常类的message有什么方法可以呈现到前端页面?
  8. 在实现执行方法时try-catch可能出现的编译期异常也都可以throw到我们之前创造的通用业务异常(运行期异常),这样我们在出现未知异常时,Spring由于针对运行期异常,那么它就会为我们做rollback(很关键),当然,这个catch要放最后,应该先catch子类异常。
  9. 用枚举来存储常量数据字典。
  10. 对象在使用默认的json进行转换时对枚举转换会出问题。

使用Spring托管Service依赖理论

Spring IOC(依赖注入)理解
  1. Spring IOC提供一个对象工厂帮我们创建Service的实现并完成Service当中的依赖管理,最终给我们一个一致的访问接口。
    也就使得我们不用管创建过程和依赖管理,直接使用一致接口获取实例。
  2. 业务对象依赖图  
  3. 为什么用IOC?
  • 对象创建统一托管。
  • 规范的生命周期管理(让我们在特定生命周期增删逻辑更方便)。
  • 灵活的依赖注入(可以编程,可以注解,可以第三方)。
  • 一致地获取对象实例(这些对象实例还都是默认单例的)。
  1. Spring IOC注入方式和场景
  • Java配置类:需要代码控制对象创建逻辑的场景,如自定义修改类库(不常用)。
  • 注解:项目自身开发使用的类,可直接在代码中使用注解,如@Service、@Controller等。
  • xml:Bean实现类来自第三方类库,如DataSource;需要命名的空间配置如context、aop、mvc等。

本项目IOC使用

  1. XML配置
  2. package-scan(DAO)
  3. Annotation注解(Service)

使用Spring托管Service依赖配置

  1. 在resources-spring目录下创建spring-service.xml文件,来配置Service,
    目的让Spring扫描service包下所有使用注解的类型并注入到容器(<context:component-scan base-package=”org.seckill.service”/>)。
  2. 另外,开始对自己开发的Service来完成基于注解的配置。
  3. Spring提供的几种注解:@Component(组件统称)、@Service、@Dao和@Controller。
  4. MyBatis自动实现了DAO类对象在Service类使用时,需要使用@Autowired注解(或者@Inject、@Resource)来注入Service依赖。

Spring声明式事务

  1. 什么是声明式事务?
    正常一个事务过程包括:开启事务-修改SQL1-修改SQL2-修改SQL3-等等-提交/回滚事务。
    那使用声明式事务后,事务的开启和结尾阶段都交由第三方框架完成,我们不需要关心,只关心SQL操作,解脱了我们的事务操作。
  2. Spring声明式事务的使用方式:
  • ProxyFactoryBean + XML : 早期使用方式(2.0)。
  • tx:advice + aop命名空间 : 一次配置永久生效。
  • 注解@Transactional : 注解控制(推荐,这样会在方法中加入注解,便于团队间的交流。)。
  1. 事务方法嵌套
  • 这是声明式事务独有的概念,和MySQL无关
  • Spring默认的传播行为(当有多个方法调用时是创建一个新事务还是加入到已有的事务)是propagation_required(它是如果事务有,加入到原有事务当中)
  1. 什么时候回滚事务?
  • 当方法抛出运行期异常(RuntimeException),很重要
  • 所以要小心谨慎的try-catch
配置并使用Spring声明式事务

在spring-service.xml文件中配置事务声明管理器:MyBatis采用的是JDBC的事务管理器,所以引入spring默认的transactionManager。

  1. 配置注入数据库连接池(这里有个小问题,在dataSource命名空间下找不到ref=”dataSource”,这是因为我们配置的dataSource在spring-dao.xml
    文件中,这里找不到没关系,到时候运行时两个xml文件都给到Spring,它会自动帮我们找到)。
  2. 配置基于注解的声明式事务(tx:annotation-driven transaction-manager=”transactionManager”),这样就使得可以默认使用注解管理事务行为。

使用注解控制事务方法的优点:

  • 开发团队达成一致约定,养成明确标注事务方法的编程风格。
  • 保证事务方法的执行时间尽可能短,将网络操作如RPC/HTTP请求等耗时操作剥离到事务方法外部。
  • 不是所有的方法都需要事务,如果只有一条修改操作或一般的只读操作(因为只读操作不会对数据库进行修改,不会用到回滚。)是不需要事务控制的。

集成测试Service逻辑

  1. 主要针对业务实现类。
  2. 首先还是需要配置Spring-Junit依赖:
    @Runwith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({
    “classpath:spring/spring-dao.xml”,
    “classpath:spring/spring-service.xml”})
  3. 用Spring注解@Autowired方式注入我们的测试对象实例。
  4. 配置日志管理logback:
    在resources目录下创建logback.xml文件,将官网的配置头文件(默认的直接打印到控制台)和xml头拷贝进去
  5. logger.warn可用于警告。
  6. 测试代码要能覆盖完整逻辑且注意可重复执行。

Web层设计与实现

内容概述

  • 前端交互设计
  • Restful接口设计
  • Spring MVC
  • Bootstrap + JQuery

前端交互流程设计

标准:根据用户需求设计前端交互流程
涉及的人员:产品、前端和后端

前端页面流程

详情页面流程

学习Restful接口设计

什么是Restful

  • 兴起于Rails
  • 一种优雅的URI表述方式
  • 关于状态转移(动词)和资源的状态(名词表示)

Restful规范

  • GET -> 查询操作
  • POST -> 添加/修改操作(非幂等)
  • PUT -> 修改操作(幂等[一个操作执行多次与一次的结果一样且不会对系统造成崩坏性的影响])
  • DELETE -> 删除操作

Restful的URL设计

/模块/资源/{标示}/集合1/…
如:/user/{uid}/frends -> 好友列表    /user/{uid}/followers -> 关注者列表

秒杀API的URL设计

  • GET /seckill/list -> 秒杀列表
  • GET /seckill/{id}/detail -> 详情页
  • GET /seckill/time/now -> 系统时间
  • POST /seckill/{id}/exposer -> 暴露秒杀
  • POST /seckill/{id}/{md5}/execution -> 执行秒杀

总结

使用Restful接口可以方便伙伴间的交流,养成良好的设计思路和风格。

Spring MVC整合Spring

使用Spring MVC框架理论

概述:我们始终围绕Handler开发

页面View的格式可以是Json、JSP甚至PDF。

Spring MVC运行流程

  • DispatcherServlet : 中央控制器的Servlet,会拦截用户所有的请求。
  • DefaultAnnotationHandlerMapping : 用来映射URL,明确我们哪些URL对应哪些Handler。
  • DefaultAnnotationHandlerAdapter : 用来做一个Handler适配,衔接我们编写的Controller(如果用到Intercept拦截器,它也会将拦截器绑定在我们的流程当中)。
  • 上述步骤产生的ModelAndView交付到DispatchServlet后,会根据我们应用的View格式匹配相应的ViewResolver,然后解析后将View与Model相结合返回给用户。在使用jsp我们可以设置返回一个字符串,这个字符串对应一个jsp页面,而使用json做view的话只要把图中jsp改成json即可。

HTTP请求地址映射原理

注解映射技巧

@RequestMapping注解:
(1) 支持标准的URL
(2) Ant风格URL 如/user/*/creation
(3) 带{xxx}占位符的URL 如/user/{userId}

Spring MVC请求方法细节处理

  1. 请求参数绑定
  2. 请求方式限制(写的Handler方法只允许GET或POST或PUT等提交,如何限制)
  3. 重定向和请求转发
  4. 数据模型赋值(将什么样的数据传递给jsp或json)

一个可以展现上诉各个细节处理的例子:

  1. 返回json数据
  2. cookie访问

整合配置Spring MVC框架

在WEB-INF目录下的web.xml文件配置Spring MVC框架:

  1. 配置DispatchServlet(servlet-name和servlet-class)。
  2. 配置Spring MVC需要加载的配置文件:spring-dao.xml、spring-service.xml和spring-web.xml,且配置的加载顺序是:Mybatis-Spring-Spring MVC。
  3. 做Servlet的Mapping,直接用/表示默认匹配所有请求,让所有请求都转到DispatchServlet当中。
  4. 新建resources-spring-spring-web.xml文件进行配置:
  • 开启Spring MVC注解模式(mvc:annotation-driven),它实质上是一个简化配置,完成了以下功能:
    (1) 自动注册DefaultAnnotationHandlerMapping和DefaultMethodHandlerAdapter。
    (2) 提供一系列功能:数据绑定、数字和日期的format(@NumberFormat、@DateTimeFormat)、xml和json的读写支持。
  • 因为配置的servlet-mapping的映射路径是”/“,所以在这里我们需要有一个静态资源默认servlet配置(mvc:default-servlet-handler),它也有两个作用:
    (1) 允许使用”/“做整体映射。
    (2) 加入对静态资源的处理:js,gif,png等。
  • 配置jsp显示对应的ViewResolver(InterResourceViewResolver、viewClass、prefix和suffix)。
  • 还可以配置json,但是第一步配置其实已经默认开启了。
  • 配置扫描web相关的包(和之前DAO层与Service层类似要进行的配置)。
  • 需要拦截器就在额外进行相应配置(从官网获取)。

实现秒杀相关的Restful接口

  1. 新建java-web目录用于存放Controller类,用这些Controller类来实现Restful接口。
  2. 在Controller类上面加上几个注解:@Controller和@RequestMapping(“/seckill”)(用来作为URL映射地址的/模块)。
  3. 具体方法实现层,list.jsp + model = ModelAndView,Model对象用来存放(使用model.Attribute()方法)渲染所需的数据。\
    方法的返回值结合之前对VieResolver的配置可以直接返回一个字符串来对应一个jsp页面。另外,方法上层需要使用RequestMapping\
    进行二级的URL映射和限制对应HTTP请求方式。
  4. 当然,Controller方法层次上的实现要对Service层进行调用,所以也需使用注解的方式(@Autowired和@Resources等)将Service对象依赖注入到Controller类中。
  5. 此外,类里面也需配置日志对象对各种信息进行管理。
  6. RequestMapping中使用占位符时需要在方法的参数设置上加上@PathVariable(“seckillId”)这种类似代码。
  7. 用redirect:/seckill/list或forward:/seckill/list可以用来重定向和请求转发。
  8. Contorller控制层也就是接收某些参数后,结合我们设计好的dto类型为我们做跳转控制。
  9. AJax的Restful方法接口实现时的返回类型一般设置为json类型,然后在方法前方除了需要进行\
    @RequestMapping注解,还需要加上@ResponseBody注解来告诉Spring MVC这个方法的返回类型\
    算作一个json,此外,最好在@RequestMapping下额外加上参数produces = {“application/json;charset\
    =UTF-8”}\来告诉浏览器我们的contentType来防止json或者中文乱码问题,养成良好的习惯。
  10. AJax所需要的json类型需要自己创建一个dto类来进行封装,这个类一般设置为泛型,使得所有的AJax请求返回类型都可以用此类设置。
  11. 方法的参数从Cookie中获取时需要加上@CookieValue(value = “killPhone”,required = false)的设置。\
    另外,这里设置的required = false是说当cookie中获取不到killPhone变量时不要让Spring MVC\
    来报错,而是赋予我们程序来处理这样一个逻辑的机会。
  12. 系统当前日期直接使用Date now = new Data()来new一个对象即可。

基于bootstrap开发页面结构

  1. 采用CSS直接埋点的方式为我们提供了很多方便的样式。
  2. 我们直接通过拷贝HTML模板方式使用bootstrap。
  3. 在WEB-INF目录下新建jsp目录用来存放jsp文件,jsp文件头用拷贝过来的HTML模板替换,之后修改。
  4. 使用的这个模板帮我们简单做了一些浏览器的适配,引入了JQuery库,但是模板上使用的bootstrap是一个压缩版本,
    我们找一个好的CDN版本进行替换,另外,CDN版本中的主题代码一般不使用。
  5. 需要注意使用CDN版本bootstrap后,我们在调试时需要全程联网。
  6. jsp通用的头文件代码可以独立出来放到jsp-common目录下作为head.jsp,然后在具体jsp\
    文件中用静态引入(<%@include file = “common/head.jsp”%>)的方式引入。(静态引入是直接\
    将被引用文件中jsp代码插入引用文件中,只开一个Servlet;动态引入则是开多个Servlet,是\
    将被引用jsp文件运行后的结果html
    代码合并到引用文件结果html代码中。)
  7. 使用bootstrap建议将所有显示内容放在几个div骨架下:
    1
    2
    3
    4
    5
    6
    7
    8
    <div class = "container">
    <div class = "panel panel -default">
    <div class = "panel-heading text-center">
    </div>
    <div class = "panel-body">
    </div>
    </div>
    </div>

具体的显示内容div放在panel-body下,至于需要使用的列表各种组件样式就查询bootstrap官网使用即可。
table的创建:table-thead-tr-th做表头,table-tbody-tr-td做表行。表行里的内容一般用core标签来迭\
代从bean中获取(c:forEach var = “sk” items = “${list}”)。

  1. 若需要使用的jstl这样的标签库,引入代码也是可以独立成为一个tag.jsp文件用于静态引入。
    需要的两个标签是:core和fmt。
  2. 用fmt格式化时间显示举例:<fmt:formatDate value = “${sk.startTime}” pattern = “yyyy-MM-dd HH:mm:ss”>。
  3. 可以使用bootstrap做一个按钮式的超链接样式。

交互逻辑编程

cookie登录交互

  1. bootstrap的Modal插件可以为我们做一个弹出层,bootstrap的modal的CSS规范是:\
    modal-content + modal-body + modal-footer。
  2. 埋点id=”killPhoneModal”可以方便我们使用id来查询到对应的整个div组件。
  3. input可以给用户填写信息,span标签则可用于显示出错信息。
  4. JQuery的Cookie操作插件和countDown插件可以使用bootstrap为我们提供好的一个CDN\
    (http://www.bootcdn.cn)来直接获取。
  5. 使用CDN是Web项目一个很好的加速点。
  6. 新建webapp-resources-script目录,在此目录下编写js文件完成交互逻辑编程。
  7. 写好的js文件可在jsp文件中用<script src = “/resources/script/seckill.js” type = “text/javascript”></script>来引入,注意此处有个小坑,
    </script>不能用</>来替代,否则javascript代码会不能执行。
  8. 注意写javascript时要做到模块化,java的模块化可以通过分包来实现,javascript中没\
    有package的概念,但是我们可以用json对象来封装多个对象来使得之后调用时产生一个分包\
    模拟效果,让我们规范我们的程序编写风格。另外,正式写代码前一定要先规划好我们的交互流程。
  9. 一开始的初始化放在init中,在jsp文件中使用<script type = “text/javascript”>\
    $(function())可以调用javascript方法并且使用EL表达式传入bean中的参数到javascript对象方法。
  10. 由于已经使用cookie插件,在javascript文件中我们可以直接使用$.cookie(‘killPhone’)的方式获取cookie数据。
  11. javascript中访问jsp设置的参数方式:var startTime = parames[‘startTime’]。
  12. 验证操作最好放在javascript代码最上层,利于复用。
  13. 在javascript中可以通过#号用选择器的方式选中jsp中的一段组件,如$(‘#killPhoneModal’)。
  14. Modal类型的控制输出(点击事件、键盘事件等)可以调用它自身的一个方法来实现。
  15. 控制显示出弹出层后,我们应该为弹出层的组件做数据绑定。
  16. cookie为什么不给全路径,一般只绑到/模块?
  17. 往页面插更新内容时最好先隐藏一下再给个限制事件来显示出来,让界面显示对用户更加\
    友好,防止他们看到中间等待页面。$(‘killPhoneMessage’).hide().html(‘‘).show(300);

计时交互设计

  1. 之前的时间对象实质上是放在json里面,我们用ajax的get操作可以拿到。
  2. 需要注意的是我们不希望js代码中直接出现与我们后端交互拿数据的代码,为了缓解\
    这个问题,我们应该将想要请求数据的URL封装到js中json的URL对象里(做成一个function)。
  3. javascript中也可以用console输出错误信息方便我们进行调试。
  4. 将计时模块抽取到一个function: countdown : function (seckillId,nowTime,startTime,endTime)。
  5. 计时加一秒,防止用户时间可能出现的偏差。
  6. 倒计时完成时需要有一个回调事件的操作(使用on)。

秒杀交互设计

  1. 绑定链接时最好用one而非click,因为click会一直绑定,one则只绑定一次点击事件。
  2. 所有节点显示操作之前最好都先隐藏一下,之后用逻辑来控制。
  3. 代码一点一点的重构,不要妄想一步登天。

Web层总结

前端交互设计过程

编码前选择先想清楚前端的交互过程。

Restful接口设计

URL设计遵循Restful规范,在Controller中具体实现Restful接口。

Spring MVC使用技巧

  • Spring MVC配置和运行流程
  • DTO传递数据
  • 注解映射驱动

    BootStrap和JavaScript使用

  • BootStrap样式
  • JavaScript模块化
  • JQuery和Plugin使用

高并发优化

  1. 高并发发生在哪里?
    首先可以分析一下业务流程:

    红色部分可能需要高并发,绿色部分则无关紧要。
  2. 为什么要单独获取系统时间?
    为了给我们的高并发优化做铺垫?
    因为这里考虑到一个场景:用户在秒杀未开始时肯定会频繁刷新页面,而我们为了提高系统的并发\
    性,一般会把我们的页面静态化并且将css,js等静态资源放到CDN服务器上,也就是说要访问detail\
    这样的页面,是不需要访问我们的系统,而是直接访问CDN服务器,那这个时候也就拿不到我们的系\
    统时间所以我们要单独做一个请求来获取当前系统服务器的系统时间。
  3. CDN的理解
  • CDN(内容分发网络)是加速用户获取数据的系统。
  • 部署在离用户最近的网络节点上。
  • 命中CDN不需要访问后端服务器。
  • CDN互联网公司一般会自己搭建或租用。
  1. 获取系统时间不用优化
    因为Java访问一次内存大约10ns,而访问系统时间本质上就是new了一个日期对象然后把这个对象返\
    回给用户,那么这样一个操作如果不考虑GC的影响,一秒钟就可以做一亿次,所以不用优化。
  2. 秒杀地址接口分析
  • 无法使用CDN缓存,因为CDN缓存的都是不变的数据,这个地址是在变的。
  • 适合放在服务器端缓存:redis等,后端的缓存是可以由我们的业务逻辑来控制。
  • 一致性维护成本低,可以在redis改,也可以在mysql改,也可以等超时后再改。

秒杀地址接口优化

  1. 秒杀操作的优化分析
  • 无法使用CDN缓存。
  • 后端缓存困难:库存问题,无法进行数据库的事务处理。
  • 一行数据竞争:热点商品。
  1. 其他方案分析

    这些分布式组件组合在一起,并发量接受能力很强。
    但是痛点就在于成本:

    为什么不用MySQL解决?
    认为MySQL低效,然而一条update压力测试可以抗住约4w的QPS,也就说同一个产品一秒钟可以卖4w次。\
    那是什么使得MySQL低效了?
    可以先看下Java控制事务行为的分析:

    延迟分析:
    本地机房的一个网络延迟就在0.5ms-2ms间了,如果加上JVM-GC操作时间,整个的QPS最大就在20左右了。
    异地机房(Tomcat和MySQL分开)时的QPS就更低了。
    瓶颈分析:
    Java客户端执行update放到服务器就有网络延迟,此外在java服务端与MySQL进行事务操作时又会有GC的操作。
    那把网络延迟和Java与MySQL通信的GC等等待时间加到整个事务时间内,这个时间就长了。
    优化分析:

如何判断一个Update更新库存成功?

  • Update自身没报错
  • 客户端确认Update影响记录数
    那得到优化思路:把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响。

如何放到MySQL服务端?

  • 定制SQL方案:update/ + [auto_commit] /,需要修改MySQL源码。
  • 使用存储过程:让整个事务在MySQL端完成。(存储过程本身设计出来就是想让一组SQL组成\
    一个事务,然后在服务端完成,避免用客户端去完成事务造成的一个性能的干扰,而一般Spring\
    声明式事务和手动控制事务都是客户端控制事务,这样的控制在行级锁不多的时候完全OK,但是秒\
    杀类的同一行竞争太激烈,这个时候就需要存储过程来发挥作用。)

优化总结

  • 前端控制:暴露接口,按钮防重复。
  • 动静态数据分离:CDN缓存,后端缓存。
  • 事务竞争优化:减少事务行级锁持有时间。

redis后端缓存优化编码

有逻辑变化控制且频繁需要访问数据库的一些资源可以交由redis进行后端缓存。
我们现在的重点是用Java去访问我们本地已经搭建好的Redis来做缓存。

  1. 打开pom.xml文件,引入Java访问redis的客户端(我们用jedis),可以从官网查看各种语言\
    推荐使用的对应客户端。
  2. 在pom.xml文件中引入redis依赖。
  3. 我们来为暴露地址接口和秒杀执行接口做一个redis缓存。
  4. 常用的缓存编码设计如下:
    1
    2
    3
    4
    5
    6
    get from cache
    if null
    get db
    else
    put cache
    logic

这样写逻辑上没问题,但是注意不要将这样的缓存控制直接写到业务逻辑当中(Service)。
而是应该将这样的缓存控制写到DAO层,它才是放数据存储或缓存的一个层次。

  1. 新建dao-cache包,在这个包内创建RedisDao,对这个类我们就直接在里面实现Redis的缓存控制,\
    至少要设置JedisPool、构造方法、getSeckill和putSeckill。
  2. 数据库和jedis这样的close记得要的final里面执行。
  3. 构造Redis键值对时,需要注意redis和jedis都没有实现内部序列化操作,所以我们在赋值\
    时要注意序列化的一个操作。 一般从Redis是获取一个二进制byte数组,到Java里我们应该反\
    序列化成为一个对象,即:get -> byte[] -> 反序列化(字节数组到对象) -> Object(Seckill)。\
    此时注意高并发里面有一个很容易忽视的问题就是序列化的问题。一般序列化我们只需在Seckill.java\
    实现一个Serializable,默认使用JDK自己的序列化机制。但是考虑高并发时我们就应该在序列化上再多\
    做文章。Github有一个工程名为JVM-Serialization做了各种序列化工具性能的比对。我们决定采用性能高\
    的序列化工具自定义序列化操作。
  4. 我们选择使用protostuff序列化工具,首先要在pom.xml文件中加上protostuff-core和protostuff-runtime两个依赖,之后我们一句protostuff的标准进行反序列化(告诉schema和字节数组,用ProtostuffIOUtil)。
  5. put要做的工作就是将对象转成字节数组。
  6. 另外,注意养成一个好习惯,写完一个DAO后要写一个单元测试进行测试。
  7. 测试Redis的DAO时我们就注意要先在spring-dao.xml文件中自己注入RedisDao,我们使用的是构造方法注入。
  8. 之后在Service注入写好的RedisDao,让拿暴露地址时先访问Redis,没有再访问MySQL。
  9. 另外,这里的一致性建立在超时的基础上维护。

并发优化

简单优化可以从行级锁持有时间入手,秒杀事务执行的过程之前是update-insert-commit/rollback,\
而行级锁在update时就开启了,我们可以改变这个事务过程为insert-update-commit/rollback来降低行\
级锁持有时间,同时也让客户端与MySQL间的网络延迟和GC处理时间降低了一倍,达到优化的目的。

深度优化

使用存储过程将事务MySQL放在MySQL端执行,进一步降低网络延迟的影响。\
要提高并发量,需要想尽办法降低行级锁到commit的持续时间。

  1. 在sql目录下新建seckill.sql文件,在里面写秒杀执行的存储过程。
  2. 在存储过程中默认和MySQL终端一样也是通过分号(;)来作为分行符,我们首先使用DELIMITER $$\
    使得存储过程中用$$表示分行符。
  3. 定义存储过程(in 输入参数 out 输出参数 row_count() 返回上一条修改类型(insert、delete和update)sql\
    的影响行数):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    CREATE PRODUCER `seckill`.`execute_seckill`  
    (in v_seckill_id bitint,in v_phone bigint,
    in v_kill_time timestamp,out r_result int)
    BEGIN
    DECLARE insert_count int DEFAULT 0;
    START TRANSACTION;
    -- success_killed是秒杀明细表
    insert ignore into success_killed
    (seckill_id,user_phone,create_time)
    values (v_seckill_id,v_phone,v_kill_time);
    select row_count() into insert_count;
    -- row_count(): 0 执行生效但未修改数据; >0 表示修改数据的行数;<0 sql错误/未执行sql
    IF (insert_count = 0) THEN
    ROLLBACK;
    -- 之前的数据字典定义的返回结果:-2为内部错误 -1为重复秒杀 0为秒杀结束。
    set r_result = -1;
    ELSEIF (insert_count < 0) THEN
    ROLLBACK;
    set r_result = -2;
    ELSE
    update seckill
    set number = number - 1
    where seckill_id = v_seckill_id
    and end_time > v_kill_time
    and start_time < v_kill_time
    and number > 0;
    select row_count() into insert_count;
    IF (insert_count = 0) THEN
    ROLLBACK;
    set r_result = 0;
    -- 这个时候的insert_count < 0可能是因为sql执行出错,也可能是因为等待行级锁超时了。
    ELSEIF (insert_count < 0) THEN
    ROLLBACK;
    set r_result = -2;
    ELSE
    COMMIT;
    set r_result = 1;
    END IF
    END IF;
    END;
    $$
  4. 之后我们可以在MySQL终端通过以下代码调用存储过程:

    1
    2
    3
    4
    5
    6
    7
    8
    -- 把分行符转换回;
    DELIMITER ;
    -- 终端上赋值变量需要加@
    set @r_result = -3;
    -- 执行存储过程
    call execute_seckill(1003,13502178891,now(),@r_result);
    -- 获取结果
    select @r_result;
  5. 存储过程优化的是事务行级锁持有的时间,但不要过度依赖存储过程,存储过程在一般的\
    互联网公司并不是重点,只是银行用的多,且在我们的秒杀系统并发优化的需要,只有遇到简\
    单的逻辑而且需要高并发时才去应用存储过程。

  6. 我们的秒杀系统在用了存储过程之后经过测试一个秒杀单可以有6000的QPS。
  7. 那之前通过Spring声明式事务定义的秒杀操作要如何修改以应用存储过程?
    定义一个新的接口,这个接口是用存储过程来执行秒杀,这个接口的具体实现当然也就与Java客户端如何调用存储过程息息相关。
  8. 需要在seckillDao定义一个调用存储过程的逻辑,这个接口方法返回值类型可以设置为void,参数则需要传一个Map<String,Object> paramMap,接口实现当然还是在xml中配置让mybatis调用存储过程。
  9. xml中调用的话注意使用statementType = “CALLABLE”,之后使用:
    1
    2
    3
    4
    5
    6
    call execute_seckill(
    #{seckillId,jdbcType=BIGINT,mode=IN},
    #{phone,jdbcType=BIGINT,mode=IN},
    #{killTime,jdbcType=TIMESTAMP,mode=IN},
    #{result,jdbcType=INTEGER,mode=OUT}
    )

之后在业务逻辑Service中调用Dao调用存储过程方法,当然在此之前需要我们自己创建一个\
Map作为该方法的参数:

1
2
3
4
5
Map<String,Object> map = new HashMap<String,Object>();  
map.put ("seckillId",seckillId);
map.put("phone",userPhone);
map.put("killTime",killTime);
map.put("result",null);

注意: 为什么我们需要设置一个Map作为该方法的参数,是因为我们结果result通过Map也\
能放到参数里面,这样当我们的存储过程执行完成之后,result被赋值。当然,这个过程也\
是可能发生异常的,注意try-catch。另外,要想通过MapUtils.getInteger(map,”result”,-2)\
来获取result,需要在pom.xml引入依赖commons-collections。Cotroller也是需要调用存储\
过程来执行秒杀。

  1. 记得阶段性测试。

系统部署架构

系统可能用到哪些服务?

  • CDN(BootStrap,JQuery的依赖)
  • WebServer:Nginx + Tomcat(HTTP服务器和应用服务器)
  • Redis(服务器端的缓存,热点数据的快速存储)
  • MySQL(事务)

系统部署架构图

分库分表一般按照关键Id进行,把流量压力分散开来,而分库分表可以自己取模或者借用专门\的框架来完成。

可能参与的角色:

  • 开发
  • 测试
  • DBA
  • 运维

课程总结

数据层技术回顾

  • 数据库设计与实现
  • MyBatis理解和使用技巧
  • MyBatis整合Spring技巧

业务层技术回顾

  • 业务接口设计和封装
  • SpringIOC配置技巧
  • Spring声明式事务使用和理解

WEB技术回顾

  • Restful接口运用
  • Spring MVC使用技巧
  • 前端交互分析过程
  • BootStrap和JavaScript使用

并发优化

  • 系统瓶颈点分析
  • 事务,锁和网络延迟的理解
  • 前端,CDN,缓存等理解和使用
  • 集群化部署

参考链接

  1. Java高并发秒杀API之业务分析与DAO层
  2. Java高并发秒杀API之Service层
  3. Java高并发秒杀API之Web层
  4. Java高并发秒杀API之高并发优化
文章作者: 红发
文章链接: https://AIpynux.github.io/2019/05/02/基于SSM实现高并发秒杀API课程笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 红发