高并发秒杀系统项目分析设计

秒杀主要的两个操作

  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. 相关查询,比如说列表查询,详情页查询

实体设计

两个实体,秒杀实体和秒杀成功实体,如下。

public class Seckill {
    private long seckillId;
    private String name;
    private int number;
    private Date startTime;
    private Date endTime;
    private Date createTime;
    // 省略getter和sette以及toString方法
    ...
}

public class SuccessKilled {
    private long seckillId;
    private long userPhone;
    private short state;
    private Date createTime;
    // 多对一
    private Seckill seckill;
}

SUCCESS(1, "秒杀成功"),
END(0, "秒杀关闭"),
REPEAT_KILL(-1, "重复秒杀"),
INNER_ERROR(-2, "系统异常"),
DATA_REWRITE(-3, "数据篡改");

dao层对应着这两个实体的dao,SeckillDao和SuccessKilledDao

public interface SeckillDao {
	// 减库存
    int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
    // 根据id查询秒杀对象
    Seckill queryById(long seckillId);
    // 根据偏移量查询秒杀商品列表
    List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
	// 使用存储过程执行秒杀
    void killByprocedure(Map<String, Object> paramMap);
}

public interface SuccessKilledDao {
	// 插入购买明细,可过滤重复
    int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
	// 根据Id查询SuccessKilled并携带秒杀产品对象实体
    SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}

dao层接口对应的xml文件如下

<mapper namespace="org.seckill.dao.SeckillDao">
    <!-- 为DAO接口方法提供sql语句配置 -->
    <update id="reduceNumber">
        <!-- 具体sql -->
        update seckill
        set
        number = number - 1
        where seckill_id = #{seckillId}
        and start_time <![CDATA[ <= ]]> #{killTime}
        and end_time >= #{killTime}
        and number > 0
    </update>

    <select id="queryById" resultType="Seckill" parameterType="long">
        select seckill_id, name, number, start_time, end_time, create_time
        from seckill
        where
        seckill_id = #{seckillId}
    </select>

    <select id="queryAll" resultType="Seckill">
        select seckill_id, name, number, start_time, end_time, create_time
        from seckill
        order by create_time desc
        limit #{offset}, #{limit}
    </select>

    <!-- mybatis调用存储过程 -->
    <select id="killByprocedure" statementType="CALLABLE">
        call execute_seckill(
          #{seckillId, jdbcType=BIGINT, mode=IN},
          #{phone, jdbcType=BIGINT, mode=IN},
          #{killTime, jdbcType=TIMESTAMP, mode=IN},
          #{result, jdbcType=INTEGER, mode=OUT}
        )
    </select>

</mapper>

<mapper namespace="org.seckill.dao.SuccessKilledDao">
    <insert id="insertSuccessKilled">
        <!-- 不加ignore的话,如果主键冲突,会报错。
        加了ignore,当插入主键冲突时,不报错,生效行数为0行,返回值为0,方便业务的处理。 -->
        insert ignore into success_killed(seckill_id, user_phone, state)
        values(#{seckillId}, #{userPhone}, 0)
    </insert>

    <select id="queryByIdWithSeckill" resultType="SuccessKilled" parameterType="long">
        <!-- 根据seckillId查询SuccessKilled并携带Seckill实体 -->
        <!-- 如何告诉Mybatis把结果映射到SuccessKilled同时映射seckill属性 -->
        <!-- 可以自由控制SQL -->
        select
        sk.seckill_id,
        sk.user_phone,
        sk.state,
        sk.create_time,
        // 别名,省略了as,为了让mybatis知道是SuccessKilled中的Seckill属性
        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}
    </select>
</mapper>

数据库需要注意的部分

mybatis中#{}与${}的区别

  1. #将传入的数据都当成一个字符串,#方式能够很大程度防止sql注入。
  2. $将传入的数据直接显示生成在sql中,$方式无法防止Sql注入。$方式一般用于传入数据库对象,例如传入表名。
  3. 一般能用#的就别用$。

​ 例子:其实区别很简单的,举个例子大家就会明白的。写一句SQL-例如:select * from user_role where user_code = “100”;

这句话而言,需要写成 select * from ${tableName} where user_code = #{userCode}

所以,$符是直接拼成sql的 ,#符则会以字符串的形式 与sql进行拼接。

SQL注入

SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

例子:

考虑一下登录表单

<form action="/login" method="POST">
	<div>Username: <input type="text" name="username" /></div>
	<div>Password: <input type="password" name="password" /></div>
	<div><input type="submit" value="登陆" /></div>
</form>

如果用户输入的用户名如下,密码任意

'myuser' or 'foo' = 'foo' --

那么我们的SQL变成了如下所示:

SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'

在SQL里面--是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。

dao层的接口方法参数中为什么都要加上@Param注解

因为java没有保存行参的记录,java在运行的时候会把List<Seckill> queryAll(int offset,int limit);中的参数变成这样:queryAll(int arg0,int arg1),这样我们就没有办法去传递多个参数。所以需要使用@Param注解给方法参数命名,然后在xml文件的该dao层方法对应的sql语句中就可以正常使用@Param注解的参数名。

spring整合mybatis(四个步骤)

建立spring-dao.xml文件,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- spring整合mybatis -->
    <!-- 1 配置数据库相关参数properties的属性:${url} -->
    <context:property-placeholder location="classpath:jdbc.properties" />
    <!-- 2 数据库连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" >
        <!-- 配置连接池属性 -->
        <property name="driverClass" value="${driver}" />
        <property name="jdbcUrl" value="${url}" />
        <property name="user" value="${username}" />
        <property name="password" value="${password}" />

        <!-- c3p0连接池的私有属性 -->
        <!-- 最大连接数量 -->
        <property name="maxPoolSize" value="30" />
        <!-- 最小连接数量 -->
        <property name="minPoolSize" value="10" />
        <!-- 关闭连接后不自动commit -->
        <property name="autoCommitOnClose" value="false" />
        <!-- 获取连接超时时间 -->
        <property name="checkoutTimeout" value="1000" />
        <!-- 获取连接失败的重试次数 -->
        <property name="acquireRetryAttempts" value="2" />
    </bean>

    <!-- 约定大于配置 -->
    <!-- 3 配置SqlSessionFactory对象 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" >
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 配置mybatis全局配置文件mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml" />
        <!-- 扫描entity包,使用别名 -->
        <property name="typeAliasesPackage" value="org.seckill.entity" />
        <!-- 扫描sql配置文件,mapper需要的xml文件 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml" />
    </bean>

    <!-- 4 配置扫描dao接口包,动态实现dao接口,注入到spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 注入sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 给出需要扫描的dao接口包 -->
        <property name="basePackage" value="org.seckill.dao" />
    </bean>

    <!-- RedisDao -->
    <bean id="redisDao" class="org.seckill.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost"></constructor-arg>
        <constructor-arg index="1" value="6379"></constructor-arg>
    </bean>

</beans>

交互设计图

在这里插入图片描述

在这里插入图片描述

项目优化点

高并发优化点:

一组sql组成的事务,这组事务在java客户端完成 —> 这组事务在服务器端完成

使用存储过程:整个事务在MySQL端完成

本质也是优化网络延迟和GC的干扰

优化点1:使用redis来缓存Seckill秒杀商品对象

方法:“seckill:” + seckillId —> Seckill字节数组

实现:建立一个RedisDao类用来实现get和set对象方法,使用Google提供的Protostuff来实现序列化和反序列化,因为性能非常高,比Java自己提供的序列化机制效率好。

RedisDao的实现如下

package org.seckill.dao.cache;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.seckill.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class RedisDao {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JedisPool jedisPool;

    public RedisDao(String ip, int port) {
        jedisPool = new JedisPool(ip, port);
    }
	// schema用来描述pojo对象的结构和属性信息
    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public Seckill getSeckill(long seckillId) {
        // redis操作逻辑
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckillId;
                // 并没有实现内部序列化操作
                // get ->  byte[] -> 反序列化 -> Object(Seckill)
                // 采用自定义序列化
                // protostuff : pojo
                byte[] bytes = jedis.get(key.getBytes());
                // 从缓存中获取到
                if (bytes != null) {
                    // 空对象
                    Seckill seckill = schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                    // seckill 被反序列化
                    return seckill;
                }
            } finally {
                jedis.close();
            }
        } catch(Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public String putSeckill(Seckill seckill) {
        // 序列化 set -> Object(Seckill) -> byte[]
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckill.getSeckillId();
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                // 超时缓存
                int timeout = 60 * 60; // 1小时
                String result = jedis.setex(key.getBytes(), timeout, bytes);
                return result;
            } finally {
                jedis.close();
            }

        } catch(Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

}

将RedisDao注入到Spring容器中,在spring-dao.xml文件中配置如下

<bean id="redisDao" class="org.seckill.dao.cache.RedisDao" >
    <constructor-arg index="0" value="localhost"></constructor-arg>
    <constructor-arg index="1" value="6379"></constructor-arg>
</bean>

然后在SeckillServiceImpl中改造暴露秒杀接口方法exposeSeckillUrl(long seckillId)

优化前Seckill对象都是从数据库中获取

seckill = seckillDao.queryById(seckillId);
if (seckill == null) {
    return new Exposer(false, seckillId);
}

优化后分三步:1. 从redis中获取 2. 如果redis中没有,从数据库获取 3. 将从数据库获取到的Seckill对象存入redis中

// 优化点:缓存优化:超时的基础上维护一致性,数据只缓存1小时
// 1 访问redis
Seckill seckill = redisDao.getSeckill(seckillId);

if (seckill == null) {
    // 2 访问数据库
    seckill = seckillDao.queryById(seckillId);
    if (seckill == null) {
        return new Exposer(false, seckillId);
    } else {
        // 3 放入redis
        redisDao.putSeckill(seckill);
    }
}

优化点2:通过调整insert和update的执行顺序将网络延迟和GC减少一倍,减少行级锁到commit过程的时间

网络延迟和GC:是指执行完sql语句然后返回结果到java客户端的过程中的网络延迟和GC

关于先执行insert与先执行update的区别:

  1. 两个事务同时insert的情况下,没有锁竞争,执行速度会快,
  2. 当两个事务先update同一行数据,会有一个事务获得行锁,锁在事务提交之前都不会释放,所以让锁被持有的时间最短能提升效率
  3. 只有通过索引条件检索数据时,innodb才使用行锁,否则使用表锁。

先update再insert,会有2倍的网络延迟和GC

在这里插入图片描述

先insert再update,只有一倍的网络延迟和GC

在这里插入图片描述

优化点3:事务sql在MySQL端执行(存储过程)

存储过程定义

-- 秒杀执行存储过程
delimiter $$  -- console ; 转换为 $$
-- 定义存储过程
-- 参数:in 输入参数;out 输出参数
-- row_count() 返回上一行修改类型sql(insert, delete, update)的影响行数
-- row_count: 0:未修改数据 >0:表示修改的行数 <0:sql错误/未执行修改sql
create procedure `seckill`.`execute_seckill`
  (in v_seckill_id bigint, v_phone bigint, in v_kill_time timestamp, out r_result int)
BEGIN
  declare insert_count int DEFAULT 0;
  start transaction;
  insert ignore into success_killed
    (seckill_id, user_phone, state)
    values(v_seckill_id, v_phone, 0);
  select row_count() into insert_count;
  if (insert_count = 0) THEN
    ROLLBACK;
    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 start_time <= v_kill_time
      and end_time >= v_kill_time
      and number > 0;
    select row_count() into insert_count;
    if (insert_count = 0) THEN
      ROLLBACK;
      set r_result = 0;
    elseif (insert_count < 0) THEN
      rollback;
      set r_result = -2;
    else
      commit;
      set r_result = 1;
    end if;
  end if;
end;
$$
-- 存储过程定义结束

delimiter ;

-- 设置变量
set @r_result = -3;
-- 执行存储过程
call execute_seckill(1003, 18801082263, now(), @r_result);
-- 返回结果
select @r_result;

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

连接上MySQL的seckill数据库,在命令行中执行上面的代码,就创建了seckill数据库的存储过程

然后再java客户端调用存储过程,如下

// web层 只用将java客户端执行秒杀改为使用存储过程执行秒杀
SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);
// SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);

// service层
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
    if (md5 == null || !md5.equals(this.getMD5(seckillId))) {
        return new SeckillExecution(seckillId, SeckillStateEnum.DATA_REWRITE);
    }
    Date killTime = new Date();
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("seckillId", seckillId);
    map.put("phone", userPhone);
    map.put("killTime", killTime);
    map.put("result", null);
    // 执行存储过程,result被赋值
    try {
        seckillDao.killByprocedure(map);
        // 获取result
        int result = MapUtils.getInteger(map, "result", -2);
        if (result == 1) {
            SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
            return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, sk);
        } else {
            return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
        }

    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
    }
}

// dao层
void killByprocedure(Map<String, Object> paramMap);

<!-- mybatis调用存储过程 -->
// CALLABLE专门用来调用存储过程
<select id="killByprocedure" statementType="CALLABLE">
    call execute_seckill(
    	#{seckillId, jdbcType=BIGINT, mode=IN},
    	#{phone, jdbcType=BIGINT, mode=IN},
    	#{killTime, jdbcType=TIMESTAMP, mode=IN},
    	#{result, jdbcType=INTEGER, mode=OUT}
	)
</select>

测试:

使用存储过程

// 使用存储过程
// 第一次测试 050 - 060 10ms
23:06:15.008 [http-nio-8090-exec-58] DEBUG o.s.dao.SeckillDao.killByprocedure - ==>  Preparing: call execute_seckill( ?, ?, ?, ? ) 
23:06:15.050 [http-nio-8090-exec-58] DEBUG o.s.dao.SeckillDao.killByprocedure - ==> Parameters: 1000(Long), 18801082263(Long), 2018-03-04 23:06:15.006(Timestamp)
23:06:15.060 [http-nio-8090-exec-58] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession[org.apache.ibatis.session.defaults.DefaultSqlSession@5ed3e3d]

// 第二次测试 783 - 792 9ms
23:11:54.778 [http-nio-8090-exec-68] DEBUG o.s.dao.SeckillDao.killByprocedure - ==>  Preparing: call execute_seckill( ?, ?, ?, ? ) 
23:11:54.783 [http-nio-8090-exec-68] DEBUG o.s.dao.SeckillDao.killByprocedure - ==> Parameters: 1001(Long), 18801082263(Long), 2018-03-04 23:11:54.777(Timestamp)
23:11:54.792 [http-nio-8090-exec-68] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58bcc2fc]

// 第三次测试 512 - 516 4ms
23:13:40.505 [http-nio-8090-exec-69] DEBUG o.s.dao.SeckillDao.killByprocedure - ==>  Preparing: call execute_seckill( ?, ?, ?, ? ) 
23:13:40.512 [http-nio-8090-exec-69] DEBUG o.s.dao.SeckillDao.killByprocedure - ==> Parameters: 1002(Long), 18801082263(Long), 2018-03-04 23:13:40.504(Timestamp)
23:13:40.516 [http-nio-8090-exec-69] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a21565f]

// 第四次测试 848 - 852 4ms
23:14:58.839 [http-nio-8090-exec-70] DEBUG o.s.dao.SeckillDao.killByprocedure - ==>  Preparing: call execute_seckill( ?, ?, ?, ? ) 
23:14:58.848 [http-nio-8090-exec-70] DEBUG o.s.dao.SeckillDao.killByprocedure - ==> Parameters: 1003(Long), 18801082263(Long), 2018-03-04 23:14:58.839(Timestamp)
23:14:58.852 [http-nio-8090-exec-70] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4bd8e8ef]

平均:(10 + 9 + 4 + 4) / 4 = 6.75ms
    
// 一倍网络延迟 + GC
// 第一次测试:179 - 198  19ms
09:28:17.164 [http-nio-8090-exec-96] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
09:28:17.164 [http-nio-8090-exec-96] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1000(Long), 18801082265(Long)
09:28:17.178 [http-nio-8090-exec-96] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
09:28:17.178 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]
09:28:17.178 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca] from current transaction
09:28:17.178 [http-nio-8090-exec-96] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
09:28:17.179 [http-nio-8090-exec-96] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1000(Long), 2018-03-05 09:28:17.161(Timestamp), 2018-03-05 09:28:17.161(Timestamp)
09:28:17.187 [http-nio-8090-exec-96] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
09:28:17.187 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]
09:28:17.188 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca] from current transaction
09:28:17.188 [http-nio-8090-exec-96] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
09:28:17.188 [http-nio-8090-exec-96] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1000(Long), 18801082265(Long)
09:28:17.197 [http-nio-8090-exec-96] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
09:28:17.197 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]
秒杀成功
09:28:17.198 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]
09:28:17.198 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]
09:28:17.198 [http-nio-8090-exec-96] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@63567dca]

// 第二次测试 901 - 908 7ms
09:31:31.895 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
09:31:31.896 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1001(Long), 18801082265(Long)
09:31:31.900 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
09:31:31.901 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]
09:31:31.901 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d] from current transaction
09:31:31.901 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
09:31:31.901 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1001(Long), 2018-03-05 09:31:31.895(Timestamp), 2018-03-05 09:31:31.895(Timestamp)
09:31:31.904 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
09:31:31.904 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]
09:31:31.904 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d] from current transaction
09:31:31.905 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
09:31:31.905 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1001(Long), 18801082265(Long)
09:31:31.907 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
09:31:31.908 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]
秒杀成功
09:31:31.908 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]
09:31:31.908 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]
09:31:31.908 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fc1899d]

// 第三次测试 887 - 904  7ms
09:33:45.883 [http-nio-8090-exec-104] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@12dd9240] will be managed by Spring
09:33:45.883 [http-nio-8090-exec-104] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
09:33:45.883 [http-nio-8090-exec-104] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1002(Long), 18801082265(Long)
09:33:45.887 [http-nio-8090-exec-104] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
09:33:45.887 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]
09:33:45.887 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b] from current transaction
09:33:45.887 [http-nio-8090-exec-104] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
09:33:45.893 [http-nio-8090-exec-104] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1002(Long), 2018-03-05 09:33:45.882(Timestamp), 2018-03-05 09:33:45.882(Timestamp)
09:33:45.898 [http-nio-8090-exec-104] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
09:33:45.898 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]
09:33:45.898 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b] from current transaction
09:33:45.899 [http-nio-8090-exec-104] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
09:33:45.899 [http-nio-8090-exec-104] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1002(Long), 18801082265(Long)
09:33:45.903 [http-nio-8090-exec-104] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
09:33:45.903 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]
秒杀成功
09:33:45.904 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]
09:33:45.904 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]
09:33:45.904 [http-nio-8090-exec-104] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53e0987b]

// 第四次测试 988 - 991  3ms
09:33:50.985 [http-nio-8090-exec-97] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@328c4c0f] will be managed by Spring
09:33:50.985 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
09:33:50.985 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1003(Long), 18801082265(Long)
09:33:50.987 [http-nio-8090-exec-97] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
09:33:50.987 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]
09:33:50.988 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0] from current transaction
09:33:50.988 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
09:33:50.988 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1003(Long), 2018-03-05 09:33:50.984(Timestamp), 2018-03-05 09:33:50.984(Timestamp)
09:33:50.989 [http-nio-8090-exec-97] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
09:33:50.989 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]
09:33:50.989 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0] from current transaction
09:33:50.990 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
09:33:50.990 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1003(Long), 18801082265(Long)
09:33:50.991 [http-nio-8090-exec-97] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
09:33:50.991 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]
秒杀成功
09:33:50.991 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]
09:33:50.991 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]
09:33:50.991 [http-nio-8090-exec-97] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@672a17a0]

平均:(19 + 7 + 7 + 3) / 4 = 9ms

// 2倍网络延迟 + GC
// 第一次测试 682 - 714  32ms
11:07:05.680 [http-nio-8090-exec-99] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@d6ba532] will be managed by Spring
11:07:05.680 [http-nio-8090-exec-99] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
11:07:05.682 [http-nio-8090-exec-99] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1000(Long), 2018-03-05 11:07:05.677(Timestamp), 2018-03-05 11:07:05.677(Timestamp)
11:07:05.694 [http-nio-8090-exec-99] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
11:07:05.694 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]
11:07:05.695 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c] from current transaction
11:07:05.695 [http-nio-8090-exec-99] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
11:07:05.696 [http-nio-8090-exec-99] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1000(Long), 18801082266(Long)
11:07:05.701 [http-nio-8090-exec-99] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
11:07:05.702 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]
11:07:05.702 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c] from current transaction
11:07:05.702 [http-nio-8090-exec-99] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
11:07:05.703 [http-nio-8090-exec-99] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1000(Long), 18801082266(Long)
11:07:05.712 [http-nio-8090-exec-99] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
11:07:05.712 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]
秒杀成功
11:07:05.714 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]
11:07:05.714 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]
11:07:05.714 [http-nio-8090-exec-99] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@161d3c2c]

// 第二次测试 800 - 816  16ms
11:23:38.799 [http-nio-8090-exec-108] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@57d7c1c6] will be managed by Spring
11:23:38.799 [http-nio-8090-exec-108] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
11:23:38.800 [http-nio-8090-exec-108] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1001(Long), 2018-03-05 11:23:38.798(Timestamp), 2018-03-05 11:23:38.798(Timestamp)
11:23:38.803 [http-nio-8090-exec-108] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
11:23:38.803 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]
11:23:38.803 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16] from current transaction
11:23:38.804 [http-nio-8090-exec-108] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
11:23:38.805 [http-nio-8090-exec-108] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1001(Long), 18801082266(Long)
11:23:38.808 [http-nio-8090-exec-108] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
11:23:38.809 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]
11:23:38.809 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16] from current transaction
11:23:38.810 [http-nio-8090-exec-108] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
11:23:38.810 [http-nio-8090-exec-108] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1001(Long), 18801082266(Long)
11:23:38.814 [http-nio-8090-exec-108] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
11:23:38.815 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]
秒杀成功
11:23:38.816 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]
11:23:38.816 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]
11:23:38.816 [http-nio-8090-exec-108] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44eab16]

// 第三次测试 518 - 541  23ms
11:23:50.518 [http-nio-8090-exec-113] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@4ef91c67] will be managed by Spring
11:23:50.518 [http-nio-8090-exec-113] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
11:23:50.518 [http-nio-8090-exec-113] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1002(Long), 2018-03-05 11:23:50.517(Timestamp), 2018-03-05 11:23:50.517(Timestamp)
11:23:50.520 [http-nio-8090-exec-113] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
11:23:50.521 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]
11:23:50.521 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c] from current transaction
11:23:50.521 [http-nio-8090-exec-113] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
11:23:50.535 [http-nio-8090-exec-113] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1002(Long), 18801082266(Long)
11:23:50.537 [http-nio-8090-exec-113] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
11:23:50.537 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]
11:23:50.537 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c] from current transaction
11:23:50.537 [http-nio-8090-exec-113] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
11:23:50.539 [http-nio-8090-exec-113] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1002(Long), 18801082266(Long)
11:23:50.541 [http-nio-8090-exec-113] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
11:23:50.541 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]
秒杀成功
11:23:50.541 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]
11:23:50.541 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]
11:23:50.541 [http-nio-8090-exec-113] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@fc4165c]

// 第四次测试 005 - 020  15ms
11:24:00.004 [http-nio-8090-exec-106] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@7d65ba66] will be managed by Spring
11:24:00.004 [http-nio-8090-exec-106] DEBUG o.s.dao.SeckillDao.reduceNumber - ==>  Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
11:24:00.005 [http-nio-8090-exec-106] DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1003(Long), 2018-03-05 11:24:00.002(Timestamp), 2018-03-05 11:24:00.002(Timestamp)
11:24:00.008 [http-nio-8090-exec-106] DEBUG o.s.dao.SeckillDao.reduceNumber - <==    Updates: 1
11:24:00.008 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]
11:24:00.008 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447] from current transaction
11:24:00.009 [http-nio-8090-exec-106] DEBUG o.s.d.S.insertSuccessKilled - ==>  Preparing: insert ignore into success_killed(seckill_id, user_phone, state) values(?, ?, 0) 
11:24:00.010 [http-nio-8090-exec-106] DEBUG o.s.d.S.insertSuccessKilled - ==> Parameters: 1003(Long), 18801082266(Long)
11:24:00.013 [http-nio-8090-exec-106] DEBUG o.s.d.S.insertSuccessKilled - <==    Updates: 1
11:24:00.014 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]
11:24:00.014 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447] from current transaction
11:24:00.014 [http-nio-8090-exec-106] DEBUG o.s.d.S.queryByIdWithSeckill - ==>  Preparing: select sk.seckill_id, sk.user_phone, sk.state, sk.create_time, 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 = ? and sk.user_phone = ? 
11:24:00.015 [http-nio-8090-exec-106] DEBUG o.s.d.S.queryByIdWithSeckill - ==> Parameters: 1003(Long), 18801082266(Long)
11:24:00.020 [http-nio-8090-exec-106] DEBUG o.s.d.S.queryByIdWithSeckill - <==      Total: 1
11:24:00.020 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]
秒杀成功
11:24:00.020 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]
11:24:00.020 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]
11:24:00.020 [http-nio-8090-exec-106] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fb7d447]

平均:(32 + 16 + 23 + 15) / 4 = 21.5ms

优化总结

测试数据:

​ 2倍网路延迟 + GC:32ms 16ms 23ms 15ms

​ 1倍网络延迟 + GC:19ms 7ms 7ms 3ms

​ 存储过程:10ms 9ms 4ms 4ms

2倍网路延迟 + GC —> 1倍网络延迟 + GC 行级锁持有的平均时间从 21.5ms —> 9ms

1倍网络延迟 + GC —> 存储过程 行级锁持有的平均时间从9ms —> 6.75ms(实际会小于6.75ms)

// MD5的生成
private String getMD5(long seckillId) {
    String base = seckillId + "/" + salt;
    String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
    return md5;
}

ProtostuffIOUtil序列化和反序列化

反序列化:

private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
byte[] bytes = jedis.get(key.getBytes());
// 空对象
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
// seckill 被反序列化
return seckill;

序列化:

String key = "seckill:" + seckill.getSeckillId();
byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
// 超时缓存
int timeout = 60 * 60; // 1小时
String result = jedis.setex(key.getBytes(), timeout, bytes);
return result;

减库存的sql代码:

update seckill
set
number = number - 1
where seckill_id = #{seckillId}
and start_time <![CDATA[ <= ]]> #{killTime}
and end_time >= #{killTime}
and number > 0

对于秒杀活动来说,当时的剩余库存数在秒杀期间变化非常快,某个时间点上的库存个数并没有太大的意义,而用户更关心的是 能不能抢,true or false。如果缓存true or false的话,这个值在秒杀期间是相对稳定的,只需要在库存耗尽的时候更新一次,而且为了防止这一次的更新失败,可以重复更新,利用memcached的cas操作,最后memcached也只会真正执行一次set写操作。 因为秒杀期间查询活动状态的请求都打在memcached上,减少写的频率可以明显减轻memcached的负担。

一个用户从发出秒杀请求到成功秒杀简单地说需要两个步骤: 1. 扣库存 2. 发送秒杀商品 这是至少两条数据库操作,而且扣库存的这一步,在mysql的innodb引擎行锁机制下,update的sql到了数据库就开始排队,期间数据库连接是被占用的,当请求足够多时就会造成数据库的拥堵。 可以看出,秒杀请求接口是一个耗时相对长的接口,而且并发越高耗时越长,所以首先,一定要限制能够真正进行秒杀的人数。秒杀业务的一个特点是参与人数多,但是可供秒杀的商品少,也就是说只有极少部分的用户最终能够秒杀成功 比如有2500个名额,理论上来说先发送请求的2500个用户能够秒杀成功,这2500个用户扣库存的sql在数据库排队的时候,库存还没有消耗完,比如2500个请求,全部排队更新完是需要时间的,就比如说0.5s 在这个时间内,用户会看到当前仍然是可抢状态,所以这段时间内持续会有秒杀请求进入,秒杀的高峰期,0.5秒也有几万的请求,让几万条sql来竞争是没有意义的,所以要限制这些参与到扣库存这一步的人数。

可抢状态需要第三个因素来决定,那就是当前秒杀的排队人数。 加在判断库存剩余之前,挡上一层排队人数的校验, 即有库存 并且 排队人数 < 限制请求数 = 可抢,有库存 并且 排队人数 >= 限制请求数 = 抢完

比如2500个名额秒杀名额,目标放过去3000个秒杀请求

那么排队人数记在哪里? 这个可以有所选择,如果只记请求个数,可以用memcached的计数,一个用户进入秒杀流程increase一次,判断库存之前先判断队列长度,这样就限制了可参与秒杀的用户数量。

发起秒杀先去问排队队列是不是已满,满了直接秒杀失败,同时可以去更新之前缓存了是否可抢 true or false的缓存,直接把前台可抢的状态变为不可抢。没满继续查询库存等后续流程,开始扣库存的时候,把当前用户id入队。 这样,就限制了真正进入秒杀的人数。

这种方法,可能会有一个问题,既然限制了请求数,那就必须要保证放过去的用户能够秒完商品,假设有重复提交的用户,如果重复提交的量大,比如放过去的请求中有一半都是重复提交,就会造成最后没秒完的情况,怎么屏蔽重复用户呢? 就要有个地方来记参与的用户id,可以使用redis的set结构来保存,这个时候set的size代表当前排队的用户数,扣库存之前add当前用户id到set,根据add是否成功的结果,来判断是否继续处理请求。

最终,把实际上几万个参与数据库操作的用户从减少到秒杀商品的级别,这是一个数据库可控制的范围,即使参与的用户再多,实际上也只处理了秒杀商品数量级的请求。

更多的优化

1.分库存 一般这样做就已经能够满足常规秒杀的需求了,但有一个问题依然没有解决,那就是加锁扣库存依然很慢 假设的活动秒杀的商品量能够再上一个量级,像小米卖个手机,一次有几W到几十万的时候,数据库也是扛不住这个量的,可以先把库存数放在redis上,然而单一库存加锁排队依然存在,库存这个热点数据会成为扣库存的瓶颈。

一个解决的办法是 分库存,比如总共有50000个秒杀名额,可以分50份,放在redis上的50个不同的key,那么每份上1000个库存,用户进入秒杀流程后随机到其中一个库存来修改,这样有50个库存数来竞争,缩短请求的排队时间。

这样专门为高并发设计的系统最大的敌人 是低流量,在大部分库存都好近,而有几个剩余库存时, 用户会看到明明还能抢却总是抢不到,而在高并发下,用户根本就觉察不到。

2.异步消息 如果有必要继续优化,就是扣库存和发货这两个费时的流程,可以改为异步,得到秒杀结果后通过短信/push异步通知用户。 主要是利用消息系统削峰填谷的特性 来增加系统的容量。

参考文献:http://zxcpro.github.io/blog/2015/07/27/gao-bing-fa-miao-sha-xi-tong-de-she-ji/

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值