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以後才釋放,因此優化方向是減小行級鎖的持有時間
優化思路:
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>