秒杀读后感2

1:pom 相关配置javascript

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myimooc</groupId>
<artifactId>seckill</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>seckill Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<!-- 使用junit4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<!-- 补全项目依赖 -->
<!-- 1:日志 java日志:sfl4j,log4j,logback,common-logging slf4j 是规范/接口 日志实现:log4j,logback,common-logging
这里使用:slf4j + logback -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.11</version>
</dependency>
<!-- 实现slf4j接口并整合 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.11</version>
</dependency>

<!-- 2:数据库相关依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.42</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0 </artifactId>
<version>0.9.1.2</version>
</dependency>

<!-- DAO框架:MyBatis依赖 -->
<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相关依赖 -->
<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>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

<!-- 4:spring依赖 -->
<!-- 1)spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- 2)spring dao层依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- 3)spring web相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- 4)spring test相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- redis客户端:jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<!-- 序列化插件:protostuff依赖 -->
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
<build>
<finalName>seckill</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>


2:修改servlet版本为3.1 支持el表达式

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
3:减库存和购买记录行为要放在一个事务里面执行html

若是减了库存没记录购买行为 会存在超卖,若是记录了购买行为而没减库存会出现少卖前端

4:对于MySQL来讲,竞争反应到背后的技术是就是事务+行级锁:java

start transaction(开启事务)→ update库存数量 → insert购买明细 → commit(提交事务)node

主要用到事务和行级锁,秒杀的难点在于若是高效的处理资源竞争mysql

5:秒杀相关的功能web

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

为何要进行秒杀接口暴露的操做?ajax

现实中有的用户回经过浏览器插件提早知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其余用户来讲是不公平的,咱们也不但愿看到这种状况redis

6:dao层 相关代码spring

/**
* @describe 商品库存dao
* @author zc
* @version 1.0 2017-08-22
*/
public interface SeckillDao {

/**
* 减库存
* @param seckillId
* @param killTime
* @return 若是影响行数>1,表示更新的记录行数
*/
int reduceNumber(@Param("seckillId")Long seckillId, @Param("killTime")Date killTime);

/**
* 根据id查询秒杀对象
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);

/**
* 根据偏移量查询秒杀商品列表
* @param offset
* @param limit
* @return
*/
List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

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


/**
* @describe 成功秒杀明细dao
* @author zc
* @version 1.0 2017-08-22
*/
public interface SuccessKilledDao {

/**
* 新增购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return 插入的行数
*/
int insertSuccessKilled(@Param("seckillId")long seckillId,@Param("userPhone") long userPhone);

/**
* 根据id查询SuccessKilled并携带秒杀产品对象实体
* @param seckillId 秒杀IM
* @param userPhone 手机号码
* @return 状态
*/
SuccessSeckilled queryByIdWithSeckill(@Param("seckillId")long seckillId, @Param("userPhone") long userPhone);

}


7:从上面的代码能够发现,当方法的形参在两个及两个以上时,须要在参数前加上@Param,
若是不加上该注解会在以后的测试运行时报错。这是Sun提供的默认编译器(javac)在编译后的Class文件中
会丢失参数的实际名称,方法中的形参会变成无心义的arg0、arg1等,在只有一个参数时就无所谓,
但当参数在两个和两个以上时,传入方法的参数就会找不到对应的形参。由于Java形参的问题,
因此在多个基本类型参数时须要用@Param注解区分开来

8::
Mybatis有两种提供SQL的方式:XML提供SQL、注解提供SQL(注解是java5.0以后提供的一个新特性)。

对于实际的使用中建议使用XML文件的方式提供SQL。若是经过注解的方式提供SQL,因为注解自己仍是java源码,这对于修改和调整SQL实际上是很是不方便的,同样须要从新编译类,当咱们写复杂的SQL尤为拼接逻辑时,注解处理起来就会很是繁琐。而XML提供了不少的SQL拼接和处理逻辑的标签,能够很是方便的帮咱们去作封装。

9:在src/main/resources目录下配置mybatis-config.xml(配置MyBatis的全局属性)

<configuration>
<!-- 配置全局属性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列别名替换列名 默认:true
select name as title from table
-->
<setting name="useColumnLabel" value="true"/>
<!-- 开启驼峰命名转换:Table(create_time) -> Entity(createTime) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>

10:在目录下的包里建立SeckillDao.xml
<!-- namespace:指定为哪一个接口提供配置 -->src/main/javacom.lewis.mapper
<mapper namespace="com.myimooc.spring.seckill.dao.SeckillDao">
<!-- 目的:为DAO接口方法提供sql语句配置 -->

update 和insert不须要设置返回值 都是明确的

<!-- 这里id必须和对应的DAO接口的方法名同样 -->

    <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 若是返回的是list 那么返回值设置为返回的的list中的对象类型
<!-- parameterType:使用到的参数类型 正常状况java表示一个类型的包名+类名,这直接写类名,由于后面有一个配置能够简化写包名的过程 -->
<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>
能够经过别名的方式列明到java名的转换,若是开启了驼峰命名法就能够不用这么写了
<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>

11:在目录下的包里建立SuccessKilledDao.xmlsrc/main/javacom.lewis.mapper
<mapper namespace="com.myimooc.spring.seckill.dao.SuccessKilledDao">

<insert id="insertSuccessKilled">
<!-- 忽略主键冲突,报错 -->
insert ignore into success_killed(seckill_id,user_phone,state)
values (#{seckillId},#{userPhone},0)
</insert>

<select id="queryByIdWithSeckill" resultType="SuccessSeckilled">
<!-- 根据id查询SuccessKilled并携带秒杀产品对象实体 -->
<!-- 如何告诉mybatis把结果映射到SuccessKilled同时映射seckill属性 -->
<!-- 能够自由控制SQL -->
select
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",
s.name "seckill.seckill_id",
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>

注:上面的s.seckill_id “seckill.seckill_id”表示s.seckill_id这一列的数据是Success_killed实体类里的seckill属性里的seckill_id属性,是一个级联的过程,使用的就是别名只是忽略了as关键字,别名要加上双引号。

为何要用<![CDATA[]]>把<=给包起来

CDATA指的是不该由 XML 解析器进行解析的文本数据,在XML元素中,<和&是非法的:

<会产生错误,由于解析器会把该字符解释为新元素的开始。
&也会产生错误,由于解析器会把该字符解释为字符实体的开始。(字符实体:好比 表示一个空格)
因此在这里咱们须要使用<![CDATA[ <= ]]>来告诉XML<=不是XML的语言。

 12:整合Spring和MyBatis

resources目录下建立一个新的目录spring(存放全部Spring相关的配置)

在resources包下建立jdbc.properties,用于配置数据库的链接信息

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
jdbc.username=root
password=123
resources/spring目录下建立Spring关于DAO层的配置文件spring-dao.xml

<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">
<!-- 配置整合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="${jdbc.drver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.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="com.myimooc.spring.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="com.myimooc.spring.seckill.dao"/>
</bean>

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

</beans>
在jdbc.properties里使用的是,而不是或者,这是由于后两个属性名可能会与全局变量冲突,致使链接的数据库用户名变成了电脑的用户名,因此使用了。

13:测试jdbc.usernameusernamenamejdbc.username

/**
* 配置Spring和Junit整合,junit启动时加载springIOC容器 spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring的配置文件
@ContextConfiguration({ "classpath:spring/spring-dao.xml" })
public class SeckillDaoTest {

// 注入Dao实现类依赖
@Resource
private SeckillDao seckillDao;

@Test
public void testQueryById() {

long seckillId = 1000;
Seckill seckill = seckillDao.queryById(seckillId);
System.out.println(seckill.getName());
System.out.println(seckill);
}
}

 

14:service层相关接口

public interface SeckillService {

/**
* 查询全部秒杀记录
* @return
*/
List<Seckill> getSeckillList();

/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);

/**
* 秒杀开启时输出秒杀接口地址,不然输出系统时间和秒杀时间
* @param seckillId 秒杀ID
* @return 接口地址
*/
Exposer exportSeckillUrl(long seckillId);

/**
* 执行秒杀操做
* @param seckillId 秒杀ID
* @param userPhone 手机号码
* @param md5 MD5
* @return 执行
* @throws SeckillException 秒杀异常
* @throws RepeatKillException 重复秒杀异常
* @throws SeckillCloseException 秒杀关闭异常
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;

/**
* 执行秒杀操做,存储过程
* @param seckillId 秒杀ID
* @param userPhone 手机号码
* @param md5 MD5
* @return 执行
*/
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
}

15:Exposer 对象

public class Exposer {
/**
* 是否开启秒杀
*/
private Boolean exposed;
/**
* 一种加密措施
*/
private String md5;

private long seckillId;
/**
* 系统当前时间(单位:毫秒)
*/
private long now;
/**
* 开启时间
*/
private long start;
/**
* 结束时间
*/
private long end;
}:

16:封装秒杀执行后结果

public class SeckillExecution {

private long seckillId;
/**
* 秒杀执行结果状态
*/
private int state;
/**
* 状态标识
*/
private String stateInfo;
/**
* 秒杀成功对象
*/
private SuccessSeckilled successSeckilled;
}

17:秒杀关闭异常(运行期异常)

public class SeckillCloseException extends SeckillException{

private static final long serialVersionUID = 1L;

public SeckillCloseException(String message) {
super(message);
}

public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}

 18:重复秒杀异常(运行期异常)

public class RepeatKillException extends SeckillException{

private static final long serialVersionUID = 1L;

public RepeatKillException(String message) {
super(message);
}

public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
20:秒杀相关业务异常异常
public class SeckillException extends RuntimeException{

private static final long serialVersionUID = 1L;

public SeckillException(String message) {
super(message);
}

public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
21:加密字符串
private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
22:service实现代码

@Service
public class SeckillServiceImpl implements SeckillService {

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

@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
@Autowired
private RedisDao redisDao;

/**
* md5盐值字符串,用于混淆md5
*/
private final String slat = "fdhasjfhu5GERGTEiweayrwe$%#$%$#546@wdasdfas";

/**
* 查询全部秒杀记录
*
* @return
*/
@Override
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0, 4);
}

/**
* 查询单个秒杀记录
*
* @param seckillId
* @return
*/
@Override
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}

/**
* 秒杀开启时输出秒杀接口地址,不然输出系统时间和秒杀时间
*
* @param seckillId
*/
@Override
public Exposer exportSeckillUrl(long seckillId) {
// 优化点:缓存优化:超时的基础上维护一致性
//1:访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
if (null == seckill) {
//2:访问数据库
seckill = seckillDao.queryById(seckillId);
if (null == seckill) {
return new Exposer(false, seckillId);
} else {
//3:放入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()) {
return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}

// 转换特定字符串的过程,不可逆
String md5 = this.getMD5(seckillId);

return new Exposer(true, md5, seckillId);
}

/**
* 执行秒杀操做
*
* @param seckillId
* @param userPhone
* @param md5
*/
@Transactional(rollbackFor = Exception.class)
/**
* 使用注解控制事务方法的优势:
* 1:开发团结达成一致约定,明确标注事务方法的编程风格
* 2:保证事务方法的执行时间尽量短,不要穿插其余网络操做,RPC/HTTP请求或者剥离到事务方法外部
* 3:不是全部的方法都须要事务,如只有一条修改操做,只读操做不须要事务控制
*/
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
if (null == md5 || !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");
}

// 执行秒杀逻辑:减库存 + 记录购买行为
Date nowTime = new Date();

try {
// 记录购买行为
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
// 惟一:seckillId,userPhone
if (insertCount <= 0) {
// 重复秒杀
throw new RepeatKillException("seckill repeated");
} else {
// 减库存,热点商品竞争
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
// 没有更新到记录,秒杀结束
throw new SeckillCloseException("seckill is closed");
} else {
// 秒杀成功
SuccessSeckilled successSeckilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successSeckilled);
}
}
} catch (SeckillCloseException e1) {
throw e1;
} catch (RepeatKillException e2) {
throw e2;
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 把全部编译期异常转化成运行期异常
throw new SeckillException("seckill inner error:" + e.getMessage());
}
}

/**
* 执行秒杀操做,存储过程
*
* @param seckillId
* @param userPhone
* @param md5
*/
@Override
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map<String, Object> map = new HashMap<>(16);
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) {
SuccessSeckilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);
} else {
return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
}
}

private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}
23:使用枚举表述常量数据字典
public enum SeckillStatEnum {
/**
* 秒杀成功
*/
SUCCESS(1, "秒杀成功"),
/**
* 秒杀结束
*/
END(0, "秒杀结束"),
/**
* 重复秒杀
*/
REPEAT_KILL(-1, "重复秒杀"),
/**
* 系统异常
*/
INNER_ERROR(-2, "系统异常"),
/**
* 数据篡改
*/
DATA_REWRITE(-3, "数据篡改");

private int state;

private String stateInfo;

SeckillStatEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}

public int getState() {
return state;
}

public String getStateInfo() {
return stateInfo;
}

public static SeckillStatEnum stateOf(int index) {
for (SeckillStatEnum state : values()) {
if (state.getState() == index) {
return state;
}
}
return null;
}
}

24:spring托管service

spring会经过spring工厂建立对象

seckillservice 依赖 SeckillDao和SuccessKillDao,
SeckillDao和SuccessKillDao依赖SqlSessionFactory,
SqlSessionFactory 依赖 数据源..


25:为何使用Spring IOC呢?

  • 对象的建立统一托管
  • 规范的生命周期的管理
  • 灵活的依赖注入
  • 一致的对象注入

26:Spring-IOC注入方式和场景是怎样的

 

 

 27:第三种不经常使用

这也是大多数使用spring的方式

 

在spring包下建立一个spring-service.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-4.1.xsd">
        
         <!--扫描service包下全部使用注解的类型-->
         <context:component-scan base-package="org.myseckill.service"></context:component-scan>


</beans>
复制代码

 

而后采用注解的方式将Service的实现类加入到Spring IOC容器中:

复制代码
//注解有 @Component @Service @Dao @Controller(web层),这里已知是service层
@Service
public class SeckillServiceImpl implements SeckillService{
    
    //日志对象slf4g
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    //注入service的依赖
    @Autowired
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;
复制代码

 28:事务控制

声明式事务的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。3.注解@Transactional的方式。在实际开发中,建议使用第三种对咱们的事务进行控制

29:配置声明式事务,在spring-service.xml中添加对事务的配置:

复制代码
         <!--扫描service包下全部使用注解的类型-->
         <context:component-scan base-package="org.myseckill.service"></context:component-scan>
         
         <!-- 配置事务管理器 -->
         <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
              <!-- 注入数据库链接池 -->
              <property name="dataSource" ref="dataSource"/>
         </bean>
         
         <!-- 配置基于属性的声明式事务
              默认使用注解来管理事务行为 -->
         <tx:annotation-driven transaction-manager="transactionManager"/>
复制代码

而后在Service实现类的方法中,在须要进行事务声明的方法上加上事务的注解:


@Override @Transactional /** * 使用注解控制事务方法的优势: 1.开发团队达成一致约定,明确标注事务方法的编程风格 * 2.保证事务方法的执行时间尽量短,不要穿插其余网络操做RPC/HTTP请求或者剥离到事务方法外部 * 3.不是全部的方法都须要事务,如只有一条修改操做、只读操做不要事务控制

 

30:SeckillServiceImpl集成测试类

**
* SeckillServiceImpl集成测试类
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
"classpath:spring/spring-dao.xml",
"classpath:spring/spring-service.xml"})
public class SeckillServiceImplTest {
}

31:前端流程

 

 32:秒杀API的URL设计

GET / seckill/ list       秒杀列表

GET / seckill/{id}/ detail    详情页

GET / seckill/time/now    系统时间    

GET / seckill/{id}/exposer        暴露秒杀

GET / seckill/{id}/{md5}/execution  执行秒杀

33:SpringMVC运行流程

34:注解映射技巧

@RequestMapping注解:

(1)支持标准的URL

(2)Ant风格URL(即?,*,**等字符)

(3)带{xxx}占位符的URL。

例如:

/user/*/creation

  匹配/user/aaa/creation  /user/bbb/creation等URL

/user/**/creation

  匹配/user/creation /user/aaa/bbb/creation 等URL

/user/{userId}

  匹配user/123,user/abc等URL。 ID以参数形式传入

/company/{companyId}/user/{userId}/detail

  匹配/company/123/user/456/detail等URL

35:相关细节

CookieValue以及返回json
@PostMapping(value = "/{seckillId}/{md5}/execution", produces = {"application/json;charset=UTF-8"})
@ResponseBody

public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
                                               @PathVariable("md5") String md5,
@CookieValue(value = "killPhone", required = false) Long phone) {
}

@GetMapping(value = "/list")
public ModelAndView list(Model model) {
logger.info("进入列表页");
// 获取列表页
List<Seckill> list = seckillService.getSeckillList();
logger.info("list = {}", list);
model.addAttribute(list);
// list.jsp + model = ModelAndView /WEB-INF/jsp/list.jsp
return new ModelAndView("list").addObject("list", list);
}

@GetMapping("/{seckillId}/detail")
public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
if (null == seckillId) {
// 重定向
return "redirect:/seckill/list";
}
Seckill seckill = seckillService.getById(seckillId);
if (null == seckill) {
// 请求转发
return "forward:/seckill/list";
}
model.addAttribute("seckill", seckill);
return "detail";
}
36:web.xml相关设置

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 修改servlet版本为3.1 -->

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置SpringMVC须要加载的配置文件
spring-dao.xml,spring-service.xml,spring-web.xml
Mybatis -> spring -> springMVC
-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<!-- 默认匹配全部的请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

37:spring-web.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:mvc="http://www.springframework.org/schema/mvc"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置SpringMVC -->
<!-- 1:开启SpringMVC注解模式 -->
<!-- 简化配置:
(1)自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
(2)提供一系列:数据绑定,数字和日期的format,@NumberFormat,@DataTimeFormat
xml,json默认读写支持
-->
<mvc:annotation-driven />

<!-- 2:servlet-mapping 映射路径:"/" -->
<!-- (1)静态资源默认servlet配置
(2)容许使用"/"作总体映射
-->
<mvc:default-servlet-handler/>

<!-- 3:配置jsp显示ViewResolver -->
<bean id="viewResolver" 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相关的bean -->
<context:component-scan base-package="com.myimooc.spring.seckill.web"/>

</beans>

38:全部ajax请求返回类型,封装json结果
public class SeckillResult<T> {

private boolean success;

private T data;

private String error;
}
39:导出秒杀url
@PostMapping(value = "/{seckillId}/exposer", produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId) {
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}
return result;
}

40:静态包含和动态包含的区别
 <%@include...%>   静态包含:会将引用的源代码原封不动的附加过来,合并过来成一个jsp,对应一个servlet。
<jsp:include...>  动态包含:分别编译,被包含的jsp独立编译成servlet,而后和包涵的jsp页面编译生成的静态文档html作合并;老是会检查所包含文件的变化,适合包含动态文件。
 静态包含是被包含的JSP合并到该servlet中。(一个servlet)
 动态包含是被包含的JSP先运行servlet,再把运行结果合并到包含的html中(多个servlet)。

41:tag.jsp 引入标签库
<!-- 引入标签库 -->
<%@ taglib prefix="c"  uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt"  uri="http://java.sun.com/jsp/jstl/fmt"%>

 

42:在detail.jsp中

$(function (){
// 使用EL表达式传入参数
seckill.detail.init({
seckillId : ${seckill.seckillId},
startTime : ${seckill.startTime.time},//毫秒
endTime : ${seckill.endTime.time}
});
});

43:seckill.js 代码
// 存放主要交互逻辑js代码
// javascript模块化
var seckill = {
// 封装秒杀相关ajax的url
URL:{
now : function(){
return '/seckill/time/now';
},
exposer : function (seckillId) {
return '/seckill/'+seckillId+'/exposer';
},
execution : function (seckillId,md5) {
return '/seckill/'+seckillId+'/'+md5+'/execution';
}
},
// 处理秒杀逻辑
handleSeckillKill : function (seckillId,node) {
// 获取秒杀地址,控制显示逻辑,执行秒杀
node.hide()
.html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');//按钮
$.post(seckill.URL.exposer(seckillId),{},function (result) {
// 在回调函数中,执行交互流程
if(result && result['success']){
var exposer = result['data'];
if(exposer['exposed']){
// 开启秒杀
// 获取秒杀地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId,md5);
console.log("killUrl:"+killUrl);
// 绑定一次点击事件
$('#killBtn').one('click',function () {
// 执行秒杀请求
// 1:先禁用按钮
$(this).addClass('disabled');
// 2:发送秒杀请求
$.post(killUrl,{},function(result){
if(result && result['success']){
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
// 3:显示秒杀结果
node.html('<span class="label label-success">'+stateInfo+'</span>');
}
});
});
node.show();
}else {
// 未开启秒杀
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
// 从新计算计时逻辑
seckill.mycountdown(seckillId,now,start,end);
}
}else{
console.log('result:'+result);
}
});
},
// 验证手机号
validatePhone:function (phone) {
if(phone && phone.length == 11 && !isNaN(phone)){
return true;
}else{
return false;
}
},
mycountdown : function(seckillId,nowTime,startTime,endTime){
var seckillBox = $('#seckill-box');
// 时间判断
if(nowTime > endTime){
// 秒杀结束
seckillBox.html('秒杀结束!');
}else if(nowTime < startTime){
// 秒杀未开始
var killTime = new Date(startTime + 1000);
seckillBox.countdown(killTime,function(event){
// 时间格式
var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');
seckillBox.html(format);
// 时间完成后回调时间
}).on('finish.countdown',function () {
// 获取秒杀地址,控制显示逻辑,执行秒杀
seckill.handleSeckillKill(seckillId,seckillBox);
});
}else {
// 秒杀开始
seckill.handleSeckillKill(seckillId,seckillBox);
}
},
// 详情页秒杀逻辑
detail:{
// 详情页初始化
init : function (params) {
// 手机验证和登陆 , 计时交互
// 规划咱们的交互流程
// 在cookie中查找手机号
var killPhone = $.cookie('killPhone');
// 验证手机号
if(!seckill.validatePhone(killPhone)){
// 绑定phone
// 控制输出
var killPhoneModal = $('#killPhoneModal');
// 显示弹出层
killPhoneModal.modal({
//显示弹出层
show:true,
// 禁止位置关闭
backdrop:'static',
// 关闭键盘事件
keyboard:false
});
$('#killPhoneBtn').click(function(){
var inputPhone = $('#killPhoneKey').val();
console.log('inputPhone='+inputPhone);
if(!seckill.validatePhone(killPhone)){
// 电话写入cookie
$.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'});
// 刷新页面
window.location.reload();
}else {
$('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
}
});
}
// 已经登陆
// 计时交互
var startTime = params['startTime'];
var endTime = params['endTime'];
var seckillId = params['seckillId'];
$.get(seckill.URL.now(),{},function(result){
if(result &&result['success']){
var nowTime = result['data'];
// 时间判断,计时交互
seckill.mycountdown(seckillId,nowTime,startTime,endTime);
}else{
console.log('result:'+result);
}
});
}
}
}
44:执行秒杀相关代码
   @PostMapping(value = "/{seckillId}/{md5}/execution", produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "killPhone", required = false) Long phone) {
if (StringUtils.isEmpty(phone)) {
return new SeckillResult<SeckillExecution>(false, "未注册");
}
// SeckillResult<SeckillExecution> result;
try {
// 存储过程调用
SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (SeckillCloseException e1) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e2) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (Exception e) {
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true, execution);
}
}

@GetMapping("/time/now")
@ResponseBody
public SeckillResult<Long> time() {
Date now = new Date();
return new SeckillResult<Long>(true, now.getTime());
}

45:红色的部分就表示会发生高并发的地方,绿色部分表示对于高并发没有影响

46:cdn使用

47:秒杀接口地址能够优化为从redis获取相关秒杀产品信息

48:秒杀操做优化

  • 一行数据竞争:热点商品

 

Java控制事务行为分析

图片描述

瓶颈分析

图片描述

优化分析

行级锁在Commit以后释放
优化方向减小行级锁持有时间



 

好比一个热点商品全部人都在抢,那么会在同一时间对数据表中的一行数据进行大量的update set操做。

行级锁在commit以后才释放,因此优化方向是减小行级锁的持有时间

优化思路:

  • 把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响。使用存储过程:整个事务在MySQL端完成,用存储过程写业务逻辑,服务端负责调用。

 

 
 49:其余方案实现

50:redis 访问

 

<!-- redis客户端:jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<!-- 序列化插件:protostuff依赖 -->
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.8</version>
</dependency>
public class RedisDao {

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

private final JedisPool jedisPool;

public RedisDao(String ip,int port){
jedisPool = new JedisPool(ip,port);
}

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));
// 超时缓存 1小时
int timeout = 60 * 60;
String result = jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally {
jedis.close();
}
} catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
}

加入ioc
<bean id="redisDao" class="com.myimooc.spring.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="localhost"/>
<constructor-arg index="1" value="6379"/>
</bean>
51:Run to Cursor ( 运行到光标处 )
52:简单优化把插入购买明细操做放在减库存前面减小行锁的时间
53:使用存储过程减小网路延迟和gc操做
-- 秒杀执行存储过程

DELIMITER $$ -- console ; 转换为 $$

-- 定义存储过程
-- 参数:in 输入参数;out 输出参数
-- row_count():返回上一条修改类型sql的影响行数
-- row_count():0 未修改数据;>0 修改的行数;<0 sql错误或未执行修改sql
CREATE PROCEDURE 'seckill'.'execute_seckill'
(in v_seckill_id bigint,in 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,create_time)
values(v_seckill_id,v_phone,v_kill_time);
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 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;
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,13521542111,new(),@r_result);
select @r_result

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

54:存储过程调用
<!-- 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>

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

55:
相关依赖

int result = MapUtils.getInteger(map, "result", -2); <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>

相关文章
相关标签/搜索