高并發問題
就是指在同一個時間點,有大量用戶同時訪問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
1 <!-- redis客戶端:Jedis -->2 <dependency>3 <groupId>redis.clients</groupId>4 <artifactId>jedis</artifactId>5 <version>2.7.3</version>6 </dependency>優化暴露秒殺接口:對于SecviceImpl 中 exportSeckillUrl 方法的優化,偽代碼如下
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 <!-- protostuff序列化依賴 --> 2 <dependency> 3 <groupId>com.dyuproject.protostuff</groupId> 4 <artifactId>protostuff-core</artifactId> 5 <version>1.0.8</version> 6 </dependency> 7 <dependency> 8 <groupId>com.dyuproject.protostuff</groupId> 9 <artifactId>protostuff-runtime</artifactId>10 <version>1.0.8</version>11 </dependency>編寫數據訪問層RedisDao
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 <!-- mybatis調用存儲過程 -->2 <select id="killByProcedure" statementType="CALLABLE">3 call execute_seckill(4 #{seckillId,jdbcType=BIGINT,mode=IN},5 #{phone,jdbcType=BIGINT,mode=IN},6 #{killTime,jdbcType=TIMESTAMP,mode=IN},7 #{result,jdbcType=INTEGER,mode=OUT}8 )9 </select>在Controller層使用
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 }至此,此系統的代碼優化工作基本完成。但是在部署時可以將其更加優化,我們一般會使用如下架構