在慕課網上發現了一個JavaWeb項目,內容講的是高併發秒殺,以爲挺有意思的,就進去學習了一番。html
記錄在該項目中學到了什麼玩意..前端
該項目源碼對應的gitHub地址(由觀看其視頻的人編寫,並不是視頻源代碼):github.com/codingXiaxw…java
我結合其資料和觀看視頻的時候整理出從該項目學到了什麼...git
<!--1.日誌 java日誌有:slf4j,log4j,logback,common-logging slf4j:是規範/接口 日誌實現:log4j,logback,common-logging 使用:slf4j+logback -->
使用jdbc的getGeneratekeys獲取自增主鍵值,這個屬性仍是挺有用的。github
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--配置全局屬性--> <settings> <!--使用jdbc的getGeneratekeys獲取自增主鍵值--> <setting name="useGeneratedKeys" value="true"/> <!--使用列別名替換列名  默認值爲true select name as title(實體中的屬性名是title) form table; 開啓後mybatis會自動幫咱們把表中name的值賦到對應實體的title屬性中 --> <setting name="useColumnLabel" value="true"/> <!--開啓駝峯命名轉換Table:create_time到 Entity(createTime)--> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>
Mybatis返回的對象若是有關聯字段,除了使用resultMap還有下面這種方式(雖然我仍是以爲resultMap會方便一點)web
<select id="queryByIdWithSeckill" resultType="SuccessKilled"> <!--根據seckillId查詢SuccessKilled對象,並攜帶Seckill對象--> <!--如何告訴mybatis把結果映射到SuccessKill屬性同時映射到Seckill屬性--> <!--能夠自由控制SQL語句--> SELECT sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id", s.name "seckill.name", s.number "seckill", 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>
<!--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"/>
/** * Created by codingBoy on 16/11/27. * 配置spring和junit整合,這樣junit在啓動時就會加載spring容器 */ @RunWith(SpringJUnit4ClassRunner.class) //告訴junit spring的配置文件 @ContextConfiguration({"classpath:spring/spring-dao.xml"}) public class SeckillDaoTest { //注入Dao實現類依賴 @Resource private SeckillDao seckillDao; @Test public void queryById() throws Exception { long seckillId=1000; Seckill seckill=seckillDao.queryById(seckillId); System.out.println(seckill.getName()); System.out.println(seckill); } }
以前在學習MyBatis的時候,若是參數超過了一個,那麼是使用Map集合來進行裝載的!ajax
在此次教程中發現,能夠不用Map集合(若是都是基本數據類型)!redis
例子:使用@Param就好了!spring
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
在XML文件中能夠直接忽略parameterType了!sql
若是主鍵重複插入數據的時候,Mybatis正常是會拋出異常的,咱們又不但願它拋出異常,那麼咱們能夠這樣作:
寫ignore..
一個dto包做爲傳輸層,dto和entity的區別在於:entity用於業務數據的封裝,而dto用於完成web和service層的數據傳遞。
對於dto這個概念,在以前我是接觸過一次的,可是沒有好好地實踐起來。此次看到了它的用法了。
個人理解是:Service與Web層數據傳遞數據的再包裝了一個對象而已。由於不少時候Service層返回的數據若是使用的是POJO,POJO不少的屬性是多餘的,還有一些想要的數據又包含不了。此時,dto又能夠再一次對要傳輸的數據進行抽象,封裝想獲取的數據。
以前的異常對象都是針對整個業務的,其實仍是能夠細分多個異常類的出來的。好比「重複秒殺」,」秒殺關閉「這些都是屬於秒殺的業務。
這樣作的好處就是看到拋出的異常就可以知道是具體哪部分錯了。
對於視頻中在Service層就catch住了不少異常,我以爲能夠在Service層直接拋出,在Controller也能拋出,直接使用統一異常處理器類來管理會更加方便!
我以爲就是代碼更加清晰吧,使用註解的話。
在視頻下面還有同窗說若是在Service中調用事務方法會有些坑,我暫時還沒遇到過。先存起來吧:
併發性上不去是由於當多個線程同時訪問一行數據時,產生了事務,所以產生寫鎖,每當一個獲取了事務的線程把鎖釋放,另外一個排隊線程才能拿到寫鎖,QPS和事務執行的時間有密切關係,事務執行時間越短,併發性越高,這也是要將費時的I/O操做移出事務的緣由。
關於同類中調用事務方法的時候有個坑,同窗們須要注意下AOP切不到調用事務方法。事務不會生效,解決辦法有幾種,能夠搜一下,找一下適合本身的方案。本質問題時類內部調用時AOP不會用代理調用內部方法。
「關於同類中調用事務方法的時候有個坑」 解決方案 一、若是是基於接口動態代理 是沒有問題的,直接使用接口調用 二、若是是基於class的動態代理 能夠用 AopContext.currentProxy() 解決,注意剝離方法必定是public 修飾 !!
其實我也在想MD5暴露出去的url是否是真的有用,也見到有人提問了。
回答者:
不能說沒做用,若是不加密,用戶截取了你的訪問地址,他看到了當前秒殺ID爲1000,他徹底能夠推測出其餘的秒殺地址,或者說他能夠造出一批地址;視頻中秒殺在數據庫中判斷了秒殺時間,其餘時間他天然是秒殺不到,可是對數據庫也有必定的衝擊,若是他用定時器或者循環秒殺軟件,你的系統承受力是個問題;另外一方面對於一些還沒開始的秒殺,他模擬地址之後,徹底能夠用定時器一直訪問。加密之後因爲他拿不到混淆碼,就只能經過點擊連接進行秒殺……
簡單理解:經過MD5加密之後,用戶在秒殺以前模擬不出真實的地址,仍是有必定做用的。
在return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);代碼中**,咱們返回的state和stateInfo參數信息應該是輸出給前端的,可是咱們不想在咱們的return代碼中硬編碼這兩個參數,因此咱們應該考慮用枚舉的方式將這些常量封裝起來**,
public enum SeckillStatEnum { SUCCESS(1,"秒殺成功"), END(0,"秒殺結束"), REPEAT_KILL(-1,"重複秒殺"), INNER_ERROR(-2,"系統異常"), DATE_REWRITE(-3,"數據篡改"); private int state; private String info; SeckillStatEnum(int state, String info) { this.state = state; this.info = info; } public int getState() { return state; } public String getInfo() { return info; } public static SeckillStatEnum stateOf(int index) { for (SeckillStatEnum state : values()) { if (state.getState()==index) { return state; } } return null; } }
以前就已經接觸過RESTful這樣的思想理念的,但是在第一個項目中是沒有用起來的。由於仍是不大習慣,怕寫成不三不四的RESTful接口,打算在第二個項目中將RESTful所有應用起來。
參考博文:kb.cnblogs.com/page/512047…
@DateTimeFormat註解對時間進行格式化!(這個我暫時沒有試驗)
<!--配置spring mvc--> <!--1,開啓springmvc註解模式 a.自動註冊DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter b.默認提供一系列的功能:數據綁定,數字和日期的format@NumberFormat,@DateTimeFormat c:xml,json的默認讀寫支持--> <mvc:annotation-driven/> <!--2.靜態資源默認servlet配置--> <!-- 1).加入對靜態資源處理:js,gif,png 2).容許使用 "/" 作總體映射 --> <mvc:default-servlet-handler/>
以前在Web層與Service中封裝了dto來進行這兩層的數據進行傳輸,而咱們通常都是在Controller返回JSON給前端進行解析。
最好的作法就是將JSON的格式也統一化。這樣作就可以很好地造成規範了!
//將全部的ajax請求返回類型,所有封裝成json數據 public class SeckillResult<T> { private boolean success; private T data; private String error; public SeckillResult(boolean success, T data) { this.success = success; this.data = data; } public SeckillResult(boolean success, String error) { this.success = success; this.error = error; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public T getData() { return data; } public void setData(T data) { this.data = data; } public String getError() { return error; } public void setError(String error) { this.error = error; } }
以前獲取JSON都是使用object.properties
的方式來獲取的,此次還看到了另外一種方式:
以前在項目中寫JS代碼都是要什麼功能,寫到哪裏的。看了此次視頻,發現JS均可以模塊化!!!
JS模塊化起來可讀性仍是比以前要好的,這是我以前沒有接觸過的,之後寫JS代碼就要注意了!
下面貼上一段代碼來感覺一下:
/** * 模塊化javaScript * Created by jianrongsun on 17-5-25. */ 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"; } }, // 驗證手機號碼 validatePhone: function (phone) { return !!(phone && phone.length === 11 && !isNaN(phone)); }, // 詳情頁秒殺業務邏輯 detail: { // 詳情頁開始初始化 init: function (params) { console.log("獲取手機號碼"); // 手機號驗證登陸,計時交互 var userPhone = $.cookie('userPhone'); // 驗證手機號 if (!seckill.validatePhone(userPhone)) { console.log("未填寫手機號碼"); // 驗證手機控制輸出 var killPhoneModal = $("#killPhoneModal"); killPhoneModal.modal({ show: true, // 顯示彈出層 backdrop: 'static', // 靜止位置關閉 keyboard: false // 關閉鍵盤事件 }); $("#killPhoneBtn").click(function () { console.log("提交手機號碼按鈕被點擊"); var inputPhone = $("#killPhoneKey").val(); console.log("inputPhone" + inputPhone); if (seckill.validatePhone(inputPhone)) { // 把電話寫入cookie $.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'}); // 驗證經過 刷新頁面 window.location.reload(); } else { // todo 錯誤文案信息寫到前端 $("#killPhoneMessage").hide().html("<label class='label label-danger'>手機號碼錯誤</label>").show(300); } }); } else { console.log("在cookie中找到了電話號碼,開啓計時"); // 已經登陸了就開始計時交互 var startTime = params['startTime']; var endTime = params['endTime']; var seckillId = params['seckillId']; console.log("開始秒殺時間=======" + startTime); console.log("結束秒殺時間========" + endTime); $.get(seckill.URL.now(), {}, function (result) { if (result && result['success']) { var nowTime = seckill.convertTime(result['data']); console.log("服務器當前的時間==========" + nowTime); // 進行秒殺商品的時間判斷,而後計時交互 seckill.countDown(seckillId, nowTime, startTime, endTime); } else { console.log('結果:' + result); console.log('result' + result); } }); } } }, handlerSeckill: function (seckillId, mode) { // 獲取秒殺地址 mode.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>'); console.debug("開始進行秒殺地址獲取"); $.get(seckill.URL.exposer(seckillId), {}, function (result) { if (result && result['success']) { var exposer = result['data']; if (exposer['exposed']) { console.log("有秒殺地址接口"); // 開啓秒殺,獲取秒殺地址 var md5 = exposer['md5']; var killUrl = seckill.URL.execution(seckillId, md5); console.log("秒殺的地址爲:" + killUrl); // 綁定一次點擊事件 $("#killBtn").one('click', function () { console.log("開始進行秒殺,按鈕被禁用"); // 執行秒殺請求,先禁用按鈕 $(this).addClass("disabled"); // 發送秒殺請求 $.post(killUrl, {}, function (result) { var killResult = result['data']; var state = killResult['state']; var stateInfo = killResult['stateInfo']; console.log("秒殺狀態" + stateInfo); // 顯示秒殺結果 mode.html('<span class="label label-success">' + stateInfo + '</span>'); }); }); mode.show(); } else { console.warn("尚未暴露秒殺地址接口,沒法進行秒殺"); // 未開啓秒殺 var now = seckill.convertTime(exposer['now']); var start = seckill.convertTime(exposer['start']); var end = seckill.convertTime(exposer['end']); console.log("當前時間" + now); console.log("開始時間" + start); console.log("結束時間" + end); console.log("開始倒計時"); console.debug("開始進行倒計時"); seckill.countDown(seckillId, now, start, end); } } else { console.error("服務器端查詢秒殺商品詳情失敗"); console.log('result' + result.valueOf()); } }); }, countDown: function (seckillId, nowTime, startTime, endTime) { console.log("秒殺的商品ID:" + seckillId + ",服務器當前時間:" + nowTime + ",開始秒殺的時間:" + startTime + ",結束秒殺的時間" + endTime); // 獲取顯示倒計時的文本域 var seckillBox = $("#seckill-box"); // 獲取時間戳進行時間的比較 nowTime = new Date(nowTime).valueOf(); startTime = new Date(startTime).valueOf(); endTime = new Date(endTime).valueOf(); console.log("轉換後的Date類型當前時間戳" + nowTime); console.log("轉換後的Date類型開始時間戳" + startTime); console.log("轉換後的Date類型結束時間戳" + endTime); if (nowTime < endTime && nowTime > startTime) { // 秒殺開始 console.log("秒殺能夠開始,兩個條件符合"); seckill.handlerSeckill(seckillId, seckillBox); } else if (nowTime > endTime) { alert(nowTime > endTime); // console.log(nowTime + ">" + startTime); console.log(nowTime + ">" + endTime); // 秒殺結束 console.warn("秒殺已經結束了,當前時間爲:" + nowTime + ",秒殺結束時間爲" + endTime); seckillBox.html("秒殺結束"); } else { console.log("秒殺還沒開始"); alert(nowTime < startTime); // 秒殺未開啓 var killTime = new Date(startTime + 1000); console.log(killTime); console.log("開始計時效果"); seckillBox.countdown(killTime, function (event) { // 事件格式 var format = event.strftime("秒殺倒計時: %D天 %H時 %M分 %S秒"); console.log(format); seckillBox.html(format); }).on('finish.countdown', function () { // 事件完成後回調事件,獲取秒殺地址,控制業務邏輯 console.log("準備執行回調,獲取秒殺地址,執行秒殺"); console.log("倒計時結束"); seckill.handlerSeckill(seckillId, seckillBox); }); } }, cloneZero: function (time) { var cloneZero = ":00"; if (time.length < 6) { console.warn("須要拼接時間"); time = time + cloneZero; return time; } else { console.log("時間是完整的"); return time; } }, convertTime: function (localDateTime) { var year = localDateTime.year; var monthValue = localDateTime.monthValue; var dayOfMonth = localDateTime.dayOfMonth; var hour = localDateTime.hour; var minute = localDateTime.minute; var second = localDateTime.second; return year + "-" + monthValue + "-" + dayOfMonth + " " + hour + ":" + minute + ":" + second; } };
前三篇已經作好了這個系統了,可是做爲一個秒殺系統而言,它能支持的併發量是很低的。那咱們如今要考慮怎麼調優。
秒殺的地址接口能夠藉助redis來進行優化,不用屢次訪問數據庫。
秒殺操做是與數據庫的事務相關的,不能使用緩存來替代了。下面給出的方案是須要修改源碼的,難度是比較難的。
下面分析瓶頸究竟在哪:
對於秒殺接口而言,須要使用到Redis將數據進行緩存起來。那麼用戶就訪問就不用去訪問數據庫了,咱們給Redis緩存的數據就行了。
此次使用Jedis來操做Redis.
還有值得 注意的地方:咱們可使用ProtostuffIOUtil來代替JDK的序列化,由於這個的序列化功能比JDK的要作得好不少!
package com.suny.dao.cache; import com.dyuproject.protostuff.LinkedBuffer; import com.dyuproject.protostuff.ProtostuffIOUtil; import com.dyuproject.protostuff.runtime.RuntimeSchema; import com.suny.entity.Seckill; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; /** * 操做Redis的dao類 * Created by 孫建榮 on 17-5-27.下午4:44 */ public class RedisDao { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final JedisPool jedisPool; private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); public RedisDao(String ip, int port) { jedisPool = new JedisPool(ip, port); } public Seckill getSeckill(long seckillId) { // redis操做業務邏輯 try (Jedis jedis = jedisPool.getResource()) { String key = "seckill:" + seckillId; // 並無實現內部序列化操做 //get->byte[]字節數組->反序列化>Object(Seckill) // 採用自定義的方式序列化 // 緩存獲取到 byte[] bytes = jedis.get(key.getBytes()); if (bytes != null) { // 空對象 Seckill seckill = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); // seckill被反序列化 return seckill; } } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public String putSeckill(Seckill seckill) { // set Object(Seckill) -> 序列化 -> byte[] try (Jedis jedis = jedisPool.getResource()) { String key = "seckill:" + seckill.getSeckillId(); byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); // 超時緩存 int timeout=60*60; return jedis.setex(key.getBytes(), timeout, bytes); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } }
<!--導入鏈接redis的JAR包--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!--添加序列化依賴--> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.1.1</version> </dependency>
RedisDao並不受Mybatis的代理影響,因而是須要咱們本身手動建立的。
最終,咱們的service邏輯就會變成這樣子:
再次回到咱們的秒殺操做,其實須要優化的地方就是咱們的GC和行級鎖等待的時間。
咱們以前的邏輯是這樣的:先執行減庫存操做,再插入購買成功的記錄
其實,咱們能夠先插入成功購買的記錄,再執行減庫存的操做!
關於先執行insert與先執行update的區別,兩個事務同時insert的狀況下,沒有鎖競爭,執行速度會快,當兩個事務先update同一行數據,會有一個事務得到行鎖,鎖在事務提交以前都不會釋放,因此讓鎖被持有的時間最短能提高效率
因此咱們service層的邏輯能夠改爲這樣:
這不是最終的方案,若是爲了性能的優化咱們還能夠將SQL在Mysql中運行,不受Spring的事務來管理。在Mysql使用存儲過程來進行提交性能
-- 秒殺執行儲存過程 DELIMITER $$ -- console ; 轉換爲 $$ -- 定義儲存過程 -- 參數: in 參數 out輸出參數 -- row_count() 返回上一條修改類型sql(delete,insert,update)的影響行數 -- row_count:0:未修改數據 ; >0:表示修改的行數; <0: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, 13502178891, now(), @r_result); -- 獲取結果 SELECT @r_result;
Mybatis調用存儲過程其實和JDBC是同樣的:
在使用存儲過程的時候,咱們須要4個參數,其實result是在存儲過程當中被賦值的。咱們能夠經過MapUtils來獲取相對應的值。這是我以前沒有接觸過的。
最後,對於部署的系統架構應該是這樣子的:
花了點時間看了該視頻教程,以爲仍是學到了很多的東西的。以前沒有接觸過優化的相關問題,如今給我打開了思路,以及學到了很多的開發規範的問題,也是很讚的。若是是初學者的話是能夠去學學的。
若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y