-
1 # 列克美食
-
2 # 網路圈
“雙11”、“618”現在已經成為全民“剁手”的節日了,每年淘寶都會公佈當晚的總成交額,而在這些成交額的背後,我們不得不提到秒殺技術。秒殺必然會涉及到高併發的問題,如何保障高併發下業務穩定執行也成了重中之重,下面我們具體探討下。
秒殺、高併發場景下會導致哪些問題?秒殺系統在一定時間週期內訪問頻率很高,所以會導致資料庫及應用伺服器負載過高,嚴重時甚至會導致服務宕機不可用。給使用者的感知就是:慢、打不開、沒響應。另一方面,哪怕伺服器效能能抗得住高併發,但業務邏輯也可能會出現問題,比如商品出現超賣現象(成功下單的人數比商品庫存要多得多)。
如何最佳化秒殺系統?1、儘量將請求攔截在系統上游進行處理,避免後端伺服器壓力過大
所有的前端資源一律走CDN加速,透過CDN自身優勢使使用者能就近讀取資源,也避免了請求回源導致源服頻寬壓力過大;
前端限制使用者特定時間段內的請求數。
2、合理利用快取機制和訊息佇列非同步處理
上面第1點過濾的是使用者重複請求帶來的壓力,但不能防制某些使用者(機器手段)重複請求,所以後端程式需要做一些最佳化:
針對單個使用者,特定時間段內只允許一個有效請求,其它重複性請求一律拒絕;
利用快取技術快取熱點資料,減少資料庫查詢頻率;
透過訊息佇列的方式將業務解耦,非同步並行處理多種邏輯;
另外可以利用訊息佇列來實現“佇列”功能,請求逐一從佇列中取出執行,緩解後臺伺服器的瞬時壓力。
3、不重要的業務做降級處理
將一些不重要的業務降級(即:停止服務),減少伺服器效能開銷。
4、後端伺服器的擴容
採用分散式叢集方式部署應用,在流量洪峰來臨時彈性擴容,讓更多的伺服器分擔壓力。
5、資料庫分庫分表、讀寫分離
不管如何利用快取來快取熱點資料,最終資料還是要落地到資料庫的。所以資料庫在前期設計時就要考慮分庫分表、讀寫分離的實施,提升查詢寫入效能。
高併發是網際網路架構設計中必須要考慮的重要因素之一。雖然我們解決問題的具體技術方案可能千差萬別,但是核心目標是一致的,就是快速響應請求。以上分別從前端,服務層,資料庫層面分別做了最佳化,具體最佳化需要結合業務實際場景以及公司產品網路架構。
回覆列表
高併發問題
就是指在同一個時間點,有大量使用者同時訪問URL地址,比如淘寶雙11都會產生高併發。
高併發帶來的後果
服務端 導致站點伺服器、DB伺服器資源被佔滿崩潰。 資料的儲存和更新結果和理想的設計不一致。使用者角度 尼瑪,網站這麼卡,重新整理了還這樣,垃圾網站,不玩了二:分析阻礙服務速度的原因1:事物行級鎖的等待
java的事務管理機制會限制在一次commit之前,下一個使用者執行緒是無法獲得鎖的,只能等待
2:網路延遲
3:JAVA的自動回收機制(GC)
三:處理高併發的常見方法
1:首先可以將靜態資源放入CDN中,減少後端伺服器的訪問
2:訪問資料使用Redis進行快取
3:使用Negix實現負載均衡
4:資料庫叢集與庫表雜湊
四:實戰最佳化秒殺系統
1:分析原因
當用戶在想秒殺時,秒殺時間未到,使用者可能會一直重新整理頁面,獲取系統時間和資源(A:此時會一直訪問伺服器),當時間到了,大量使用者同時獲取秒殺介面API(B),獲取API之後執行秒殺(C),指令傳輸到各地伺服器,伺服器執行再將傳遞到中央資料庫執行(D),伺服器啟用事務執行減庫存操作,在伺服器端JAVA執行過程中,可能因為JAVA的自動回收機制,還需要一部分時間回收記憶體(E)。
2:最佳化思路:
面對上面分析可能會影響的過程,我們可以進行如下最佳化
A:我們可以將一些靜態的資源放到CDN上,這樣可以減少對系統伺服器的請求
B:對於暴露秒殺介面,這種動態的無法放到CDN上,我們可以採用Redis進行快取
request——>Redis——>MySQL
C:資料庫操作,對於MYSQL的執行速度大約可以達到1秒鐘40000次,影響速度的還是因為行級鎖,我們應儘可能減少行級鎖持有時間。
DE:對於資料庫來說操作可以說是相當快了,我們可以將指令放到MYSQL資料庫上去執行,減少網路延遲以及伺服器GC的時間。
3:具體實現
3.1:使用Redis進行快取
引入redis訪問客戶端Jedis
get from cache //首先我們要從Redis中獲取需要暴露的URL
if null //如果從Redis中獲取的為空
get db //那麼我們就訪問MYSQL資料庫進行獲取
put cache //獲取到後放入Redis中
else locgoin //否則,則直接執行
我們一般不能直接訪問Redis資料庫,首先先建立資料訪問層RedisDao,RedisDao中需要提供兩個方法,一個是 getSeckill 和 putSeckill
在編寫這兩個方法時還需要注意一個問題,那就是序列化的問題,Redis並沒有提供序列化和反序列化,我們需要自定義序列化,我們使用 protostuff 進行序列化與反序列化操作
引入 protostuff 依賴包
1 package com.xqc.seckill.dao.cache;2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import com.dyuproject.protostuff.LinkedBuffer; 7 import com.dyuproject.protostuff.ProtostuffIOUtil; 8 import com.dyuproject.protostuff.runtime.RuntimeSchema; 9 import com.xqc.seckill.entity.Seckill;10 11 import redis.clients.jedis.Jedis;12 import redis.clients.jedis.JedisPool;13 14 /**15 * Redis快取最佳化16 * 17 * @author A Cang(xqc)18 *19 */20 public class RedisDao {21 private final Logger logger = LoggerFactory.getLogger(this.getClass());22 23 private final JedisPool jedisPool;24 25 public RedisDao(String ip, int port) {26 jedisPool = new JedisPool(ip, port);27 }28 29 private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);30 31 public Seckill getSeckill(long seckillId) {32 //redis操作邏輯33 try {34 Jedis jedis = jedisPool.getResource();35 try {36 String key = "seckill:" + seckillId;37 //並沒有實現內部序列化操作38 // get-> byte[] -> 反序列化 ->Object(Seckill)39 // 採用自定義序列化40 //protostuff : pojo.41 byte[] bytes = jedis.get(key.getBytes());42 //快取中獲取到bytes43 if (bytes != null) {44 //空物件45 Seckill seckill = schema.newMessage();46 ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);47 //seckill 被反序列化48 return seckill;49 }50 } finally {51 jedis.close();52 }53 } catch (Exception e) {54 logger.error(e.getMessage(), e);55 }56 return null;57 }58 59 public String putSeckill(Seckill seckill) {60 // set Object(Seckill) -> 序列化 -> byte[]61 try {62 Jedis jedis = jedisPool.getResource();63 try {64 String key = "seckill:" + seckill.getSeckillId();65 byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,66 LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));67 //超時快取68 int timeout = 60 * 60;//1小時69 String result = jedis.setex(key.getBytes(), timeout, bytes);70 return result;71 } finally {72 jedis.close();73 }74 } catch (Exception e) {75 logger.error(e.getMessage(), e);76 }77 78 return null;79 }80 81 82 }最佳化ServiceImpl的 exportSeckillUrl 的方法
1 public Exposer exportSeckillUrl(long seckillId) { 2 // 最佳化點:快取最佳化:超時的基礎上維護一致性 3 //1:訪問redis 4 Seckill seckill = redisDao.getSeckill(seckillId); 5 if (seckill == null) { 6 //2:訪問資料庫 7 seckill = seckillDao.queryById(seckillId); 8 if (seckill == null) { 9 return new Exposer(false, seckillId);10 } else {11 //3:放入redis12 redisDao.putSeckill(seckill);13 }14 }15 16 Date startTime = seckill.getStartTime();17 Date endTime = seckill.getEndTime();18 //系統當前時間19 Date nowTime = new Date();20 if (nowTime.getTime() < startTime.getTime()21 || nowTime.getTime() > endTime.getTime()) {22 return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(),23 endTime.getTime());24 }25 //轉化特定字串的過程,不可逆26 String md5 = getMD5(seckillId);27 return new Exposer(true, md5, seckillId);28 }29 30 private String getMD5(long seckillId) {31 String base = seckillId + "/" + salt;32 String md5 = DigestUtils.md5DigestAsHex(base.getBytes());33 return md5;34 }3.2 併發最佳化:
在執行秒殺操作死,正常的執行應該如下:先減庫存,並且得到行級鎖,再執行插入購買明細,然後再提交釋放行級鎖,這個時候行級鎖鎖住了其他一些操作,我們可以進行如下最佳化,這時只需要延遲一倍。
修改executeSeckill方法如下:
1 @Transactional 2 /** 3 * 使用註解控制事務方法的優點: 4 * 1:開發團隊達成一致約定,明確標註事務方法的程式設計風格。 5 * 2:保證事務方法的執行時間儘可能短,不要穿插其他網路操作RPC/HTTP請求或者剝離到事務方法外部. 6 * 3:不是所有的方法都需要事務,如只有一條修改操作,只讀操作不需要事務控制. 7 */ 8 public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 9 throws SeckillException, RepeatKillException, SeckillCloseException {10 if (md5 == null || !md5.equals(getMD5(seckillId))) {11 throw new SeckillException("seckill data rewrite");12 }13 //執行秒殺邏輯:減庫存 + 記錄購買行為14 Date nowTime = new Date();15 16 try {17 //記錄購買行為18 int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);19 //唯一:seckillId,userPhone20 if (insertCount <= 0) {21 //重複秒殺22 throw new RepeatKillException("seckill repeated");23 } else {24 //減庫存,熱點商品競爭25 int updateCount = seckillDao.reduceNumber(seckillId, nowTime);26 if (updateCount <= 0) {27 //沒有更新到記錄,秒殺結束,rollback28 throw new SeckillCloseException("seckill is closed");29 } else {30 //秒殺成功 commit31 SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);32 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);33 }34 }35 } catch (SeckillCloseException e1) {36 throw e1;37 } catch (RepeatKillException e2) {38 throw e2;39 } catch (Exception e) {40 logger.error(e.getMessage(), e);41 //所有編譯期異常 轉化為執行期異常42 throw new SeckillException("seckill inner error:" + e.getMessage());43 }44 }3.3深度最佳化:(儲存過程)
定義一個新的介面,使用儲存過程執行秒殺操作
1 /**2 * 執行秒殺操作by 儲存過程3 * @param seckillId4 * @param userPhone5 * @param md56 */7 SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);實現executeSeckillProcedure方法
1 public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) { 2 if (md5 == null || !md5.equals(getMD5(seckillId))) { 3 return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE); 4 } 5 Date killTime = new Date(); 6 Map<String, Object> map = new HashMap<String, Object>(); 7 map.put("seckillId", seckillId); 8 map.put("phone", userPhone); 9 map.put("killTime", killTime);10 map.put("result", null);11 //執行儲存過程,result被複制12 try {13 seckillDao.killByProcedure(map);14 //獲取result15 int result = MapUtils.getInteger(map, "result", -2);16 if (result == 1) {17 SuccessKilled sk = successKilledDao.18 queryByIdWithSeckill(seckillId, userPhone);19 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);20 } else {21 return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));22 }23 } catch (Exception e) {24 logger.error(e.getMessage(), e);25 return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);26 27 }28 29 }編寫SeckillDao實現有儲存過程執行秒殺的邏輯
1 /**2 * 使用儲存過程執行秒殺3 * @param paramMap4 */5 void killByProcedure(Map<String,Object> paramMap);在Mybatis中使用
1 @ResponseBody 2 public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId, 3 @PathVariable("md5") String md5, 4 @CookieValue(value = "killPhone", required = false) Long phone) { 5 //springmvc valid 6 if (phone == null) { 7 return new SeckillResult<SeckillExecution>(false, "未註冊"); 8 } 9 SeckillResult<SeckillExecution> result;10 try {11 //儲存過程呼叫.12 SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);13 return new SeckillResult<SeckillExecution>(true,execution);14 } catch (RepeatKillException e) {15 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);16 return new SeckillResult<SeckillExecution>(true,execution);17 } catch (SeckillCloseException e) {18 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);19 return new SeckillResult<SeckillExecution>(true,execution);20 } catch (Exception e) {21 logger.error(e.getMessage(), e);22 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);23 return new SeckillResult<SeckillExecution>(true,execution);24 }25 }至此,此係統的程式碼最佳化工作基本完成。但是在部署時可以將其更加最佳化,我們一般會使用如下架構