秒杀系统思考与总结

DAO层

1.数据库设计与编码

在sql中增/插

create database,use database;create table;inset into····

2.DAO实体与接口编码

(1)entity编码

编写相应属性,getter、setter、toString

table中列对应–>entity属性。

(2)DAO接口

编写抽象方法,增删改查对应的抽象方法。

参数中要用@Param(“”),因为java没有保存形参的记录queryAll(int offset,int limit)–>queryAll(arg0,arg1)

当有多个参数时,用myBatis提供的注解@Param()。

注意参数和返回类型

3.基于myBatis实现DAO

只写接口, 不写实现类。mybatis来实现接口。

在resources下实现

(1)mybatis-config.xml配置myBatis全局属性

(2)Dao.xml

为DAO接口方法提供sql语句配置。 在mapper下xml中提供SQL.

1
2
3
4
5
6
7
8
9
10
<mapper namespace="cn.seckill.dao.SeckillDao">
<!--增删改查insert/update/select标签-->
<update id="" parameterType="" >

<!--具体sql-->
UPDATE seckill SET····WHERE···AND···

</update>

</mapper>

xml中<=用标签表示。

Dao中的参数在sql中用#{}表示

主键冲突时,会报错。用insert ignore into .忽略错误,返回0。


根据id查询SuccessKilled并携带Seckill实体。queryByIdWithSeckill():

若不写连接,写两条sql。先查出SuccessKill,再根据seckillId查出Seckill。

若在一个实体中完成,通过连接的形式inner join,把结果映射到SuccessKilled。

SuccessKilled.java实体中有Seckill seckill属性,如何告诉myBatis把结果映射到SuccessKilled同时映射到seckill属性:seckill表中查找的属性通过列别名的形式(as “”)(as可忽略,要加双引号)赋给seckill属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",
s.name "seckill.name",
s.number "seckill.number",
s.start_time "seckill.start_time",
s.end_time "seckill.end_time",
s.create_time "seckill.create_time"
FROM success_killed sk
INNER JOIN seckill s ON sk.seckill_id=s.seckill_id
WHERE sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}

mybatis整合spring

配置spring-dao.xml

配置整合mybatis过程:

(1)配置数据库相关参数properties的属性:context:/

(2)数据库连接池

连接池属性driver、url、username、password:<property name=”” value=”${}”

(3)配置SqlSessionFactory对象

1
2
3
4
5
6
7
8
9
10
11
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--往下才是mybatis和spring真正整合的配置-->
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<!--扫描entity包,使用别名,多个用;隔开-->
<property name="typeAliasesPackage" value="cn.seckill-master.entity"/>
</bean>

(4)配置扫描Dao接口包,动态实现DAO接口,注入到spring容器

1
2
3
4
5
6
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口-->
<property name="basePackage" value="cn.seckill-master.dao"/>
</bean>

约定大于配置

DAO层单元测试编码

找到DAO,ctrl+shift+T,生成相应Test。

  1. 要配置spring和junit整合,使junit自动启动时加载springIOC容器
1
2
3
4
//junit依赖,junit自动启动时加载springIOC容器
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})

注:没有分号(;)。ContextConfiguration括号内有花括号{}。

  1. 注入DAO实现类依赖

在Test类中

1
2
@Resource
private SeckillDao seckillDao;
  1. 编写test方法代码

测试方法内是直接调用对应Dao的相应方法,注意返回类型。若是该方法需要参数,则自定义。

然后将结果System.out.println();

Service层

DAO层:接口设计+SQL编写,代码与SQL分离。

DAO拼接等逻辑在Service层完成。

Service接口设计

业务接口:站在使用者角度设计接口。

三个方面:1.方法定义粒度;2.参数,越简练越好;3.返回类型(return类型一定要友好/或者return异常,我们允许的异常)

  1. Service接口内定义抽象方法。

  2. 在一些重要的行为的接口方法,其返回类型不一定是entity。dto。

dto与entity不同。dto(Data Transfer Object)是数据传输对象,在service和web之间传递数据,封装数据对象。dto内定义属性,constructor,getter,setter。

  1. 执行秒杀操作,可能失败/成功,要抛出允许的异常

Service接口实现

使用注解@Service

  1. 创建实现类,implements相应Service,alt+Insert快捷键implements Methods。

  2. 将日志的对象写出来

    1
    private Logger logger= LoggerFactory.getLogger(this.getClass());
  3. 将要用到的DAO定义(private),不用初始化,其实现类都在spring的容器中。让spring依赖注入实例。

    注入Service依赖@Autowired

  4. 编码实现方法。能运用到DAO的相关方法直接用dao调用方法。return。

.getTime()换成毫秒后再进行比较。

  1. 执行秒杀方法:减库存,增加购买明细。

(1)使用注解@Transactional控制事务方法

(2)执行秒杀方法的实现:将用户传过来的md5与生成的md5对比,若不等,抛出异常。

(3)所有编译期异常,转化为运行期异常。一旦有错,rollback。

对于声明式事务,抛出运行期异常时才会回滚。

(4)在运行期异常catch前显示地抛出允许的异常,对不同的类型做catch,如秒杀关闭、重复秒杀。

  1. state,stateInfo。
    数据字典,用枚举表示常量数据。

Service相关配置(IOC、事务)

配置spring-service.xml

  1. 使用Spring托管Service

    Service层Spring-IOC依赖注入方式的配置

扫描service包下所有使用注解的类型(如@Service,@Autowired)

1
<context:component-scan base-package="cn.codingxiaxw.service"/>
  1. Spring声明式事务配置

(1)配置事务管理器

1
2
3
4
5
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>

</bean>

(2)配置基于注解的声明式事务

1
2
//默认使用注解来管理事务行为
<tx:annotation-driven transaction-manager="transactionManager"/>

集成测试Service逻辑

在service类 ctrl+shift+T自动生成测试类。

  1. 要配置spring和junit整合,使junit自动启动时加载springIOC容器
    1
    2
    3
    4
    5
    6
    //junit依赖,junit自动启动时加载springIOC容器
    @RunWith(SpringJUnit4ClassRunner.class)
    //告诉junit spring配置文件
    @ContextConfiguration({
    "classpath:spring/spring-dao.xml",
    "classpath:spring/spring-service.xml"})

加载两个是因为测试Service,要依赖Dao的配置。

  1. 日志对象logger

  2. 依赖注入service@Autowired

3.编写测试代码

Web层

秒杀API的URL设计

1
2
3
4
5
GET/seckill/list
GET/seckill/{id}/detail
GET/seckill/time/now
POST/seckill/{id}/exposer
POST/seckill/{id}/{md5}/execution

配置SpringMVC框架

  1. web.xml:配置DispatcherSerclet。

加载SpringMVC 需要配置的文件
spring-dao.xml,spring-service.xml,spring-web.xml

  1. spring-web.xml: 配置SpringMVC

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!--1,开启springmvc注解模式
    a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
    b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
    c:xml,json的默认读写支持-->
    <mvc:annotation-driven/>

    <!--2.静态资源默认servlet配置-->
    <!--
    1).加入对静态资源处理:js,gif,png
    2).允许使用 "/" 做整体映射
    -->
    <mvc:default-servlet-handler/>

    <!--3:配置JSP 显示ViewResolver-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
    </bean>

    <!--4:扫描web相关的controller-->
    <context:component-scan base-package="cn.codingxiaxw.web"/>

    使用SpringMVC实现Restful接口(Controller)

    1. @Controller
  1. @RequsetMapping(“/seckill”)//代表模块。之后的RequsetMapping中直接写资源/{}/细分。如”/list”。

    1. Controller类中依赖注入相应Service。
      @Autowired
      private SeckillService seckillService;
  2. 针对每个方法:
    方法外@RequestMapping(value = “/list”,method = RequestMethod.GET)

若是json格式。@ResponseBody

方法内:调用Service,处理数据。将处理结果封装成json ,return。

(1) 若是jsp格式。方法参数中需要加入Model model。model用来存放所有渲染list.jsp的数据。list.jsp提供页面的模板,model是数据。list.jsp+model=ModelAndView.

return “lisit”;//WEB-INF/jsp/“list”.jsp

(2) 若是json格式,VO用来封装所有的数据结果。

将所有的ajax请求返回类型(为SeckillResult),全部封装成json数据。直接输出json,不需要model。

@RequestMapping中加入 produces = {“application/json;charset=UTF-8”})//contentType

基于bootstrap开发页面结构(jsp)

在webapp下。WEB-INF下放jsp文件。
将通用或重复的部分提取出来,再采用jsp静态包含的方式

1
2
3
 //静态包含:会将包含的comon/tag.jap合并到list.jsp中来,作为整个servlet输出..一个servlet
//动态包含:包含的jsp会作为独立的servlet,运行完之后会将运行结果与list.jsp的运行结果,也就是最后的静态HTML做合并..多个servlet
<%@include file="common/tag.jsp"%>//静态包含

交互

创建JavaScript文件seckill.js。存放主要交互逻辑js代码。javascript模块化。

在详情页jsp的HTML中引入js文件。

1
<script src="/resources/script/seckill.js" type="text/javascript"></script>

初始化函数,使用EL表达式传入参数

cookie登录交互

手机验证和登录,在cookie中查找手机号。

计时交互
秒杀交互


在seviceImpl,Controller中会private static Logger logger = Logger.getLogger(this.class);

高并发优化

高并发发生在详情页上(做静态化,放到CDN中)、访问系统时间不用优化、计时在js端,浏览器中,对服务器不会有影响。

要优化的有秒杀地址接口、秒杀操作

redis优化“地址暴露接口”

地址暴露接口是根据秒杀单的时间来计算是否秒杀,不方便作为固定的内容放到CSN中作为缓存。要放到服务器端,通过服务器端的逻辑来控制。

  1. 在porm中引入redis客户端:jedis的依赖

  2. 创建RedisDao。

    对serviceImpl中方法优化。

    seckillImpl中暴露秒杀地址用redis缓存起来。DAO数据访问对象,放数据库或者其他存储访问的类所在的包。

    redis操作逻辑不应该放在service代码中,是数据访问层的逻辑。放在RedisDao中。

    Dao中编码构造方法、getSeckill()方法、putSeckill()方法

  3. 引入prostuff序列化依赖,采用自定义序列化 。

    把对象转换成二进制数组,传递给redis,缓存起来。通过jedis拿Seckill对象,先拿到的是字节数组,再按照schema反序列化成对象。

    序列化的本质:通过字节码,通过字节码所对应对象有哪些属性,把字节码的数据传给那些属性,这样就可以序列化好那些对象。

    1. 写完DAO–>建立junit编码测试。全局测试testSeckill()

    在spring-dao.xml中注入RedisDao。

  4. 把RedisDao注入到serviceImpl中–>bi编码

    java控制事务行为分析:

同一个id执行update减库存:一条update压力测试(约4wQPS)。但是执行Update并不低效。

  1. 若都对同一商品id执行减库存:

    一个用户执行update减库存—>inset购买明细
    这时另一个用户执行update减库存–>会等待行锁block

    因为当一个事务开启的时候,另一个事务进来的时候发现锁住的是同一行,当之前的事务不去提交或回滚的话,这个行级锁是不会释放的。

    当第一个用户insert执行完后,commit/rollback后。等待行锁的才可能获得锁lock。当很多用户都去执行该id的减库存,都会等待—>形成串行化的操作。阻塞操作。

    1. 瓶颈分析

    update减库存–>insert购买明细–>commit/rollback

每一步都会有网络延迟,以及可能出现的GC。
java客户端去执行SQL,等待SQL的结果再去做判断,再去执行SQL。这一长串事务在java客户端执行,但java和数据库通信之间有网络延迟或GC。这些时间会加在事务的执行周期中。而同一行的事务是做串行化的。

  1. 优化分析:

行级锁rowback是在Commit之后释放—>优化方向减少行级锁持有时间。

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

如何放到MySQL服务端:使用存储过程:整个事务在MySQL端完成

秒杀操作-并发优化

  1. 简单优化:

    insert购买明细–>update减库存–>commit/rollback

    insert通过insert ignore,会挡住一部分重复秒杀。update拿到行级锁rowback。update减库存,热点商品竞争,拿到结果后,根据结果rollback/commit。只有一步的网络延迟、GC,减少一倍时间。

  2. 深度优化:事务SQL在MySQL端执行(存储过程)

    (1)存储过程优化:减少事务行级锁持有时间
    (2)不要过度依赖存储过程(在银行用的多)
    (3)简单的逻辑可以应用存储过程
    (4)QPS:一个秒杀单6000/qps

通过java客户端(seckill项目)调用存储过程:

在service中定义新接口;serviceImpl中实现方法;serviceTest中单元测试
在dao中定义新接口;在Dao.xml中mybatis调用存储过程;

Controller中改用存储过程调用秒杀操作

大型系统部署架构

  1. 部署可能用到哪些服务?

    (1) CDN :jQuery、Bootstrap一些依赖直接使用公网的CDN;自己开发的js、详情页做静态化处理,推送到CDN。用户在CDN获取到的数据,不需要再访问服务器

    (2)WebServer:Nginx+Jetty
    一般不直接把java的WebServer直接对外访问;Nginx可能是集群化的,部署在多个服务器上,用来作http服务器,同时会帮后端的Jetty、Tomcat的应用服务器做反向代理

    (3)Redis:做服务器端的缓存,可以通过redis提供的API来达到热点数据的快速存取的过程

    (4)MySQL:借助MySQL的事务来达到秒杀数据的一致性、完整性

  1. 部署架构;

总结

Dao–>Dao.xml–>test–>Service–>srviceImpl—>test

Controller–>jsp–>js

  1. ajax异步请求.JavaScript发送AJAX请求,URL域名和当前页面一致。
    $.post(URL,{},function(result){});

  2. Controller接收用户请求,@RequestMapping映射URL,请求参数绑定到方法上。Controller内依赖注入Service,调用service方法处理数据,注意其返回类型,得到数据结果。

VO用来封装所有的数据结果,可以新建dto类(如SeckillResult),为所有的ajax请求返回类型,全部封装成json数据。定义变量及所需的不同构造函数(参数有data,即是sevice处理得到的数据结果)。

result=new SeckillResult<service方法返回类>(构造函数参数) 。泛型。最后return result;

  1. 在js中通过ajax拿到data。
    $.get(),$.post():使用AJAX的HTTP GET或POST请求从服务器加载数据。

回调函数function(result){})是一种以参数形式传递给另一个函数的函数。拿到result之后。通过result[data]可以取得相应数据。