秒殺系統實現高並發的優化


菜鳥拙見,望請糾正

一:前言  

先上代碼看着代碼學習效率更好:https://github.com/3218870799/Seckill

高並發問題

  就是指在同一個時間點,有大量用戶同時訪問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的操作可以參考我以前的博客https://www.cnblogs.com/nullering/p/9332589.html

引入redis訪問客戶端Jedis

1 <!-- redis客戶端:Jedis -->
2    <dependency>
3         <groupId>redis.clients</groupId>
4         <artifactId>jedis</artifactId>
5         <version>2.7.3</version>
6      </dependency>
pom.xml

優化暴露秒殺接口:對於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>
pom.xml

編寫數據訪問層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                 //緩存中獲取到bytes
43                 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 }
RedisDao.java

優化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:放入redis
12                 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     }
ServiceImpl的exportSeckillUrl方法

 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,userPhone
20             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                     //沒有更新到記錄,秒殺結束,rollback
28                     throw new SeckillCloseException("seckill is closed");
29                 } else {
30                     //秒殺成功 commit
31                     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     }
ServiceImpl的executeSeckill方法

3.3深度優化:(存儲過程)

定義一個新的接口,使用存儲過程執行秒殺操作

1     /**
2      * 執行秒殺操作by 存儲過程
3      * @param seckillId
4      * @param userPhone
5      * @param md5
6      */
7     SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
executeSeckillProcedure接口

實現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             //獲取result
15             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     }
executeSeckillProcedure實現

編寫SeckillDao實現有存儲過程執行秒殺的邏輯

1     /**
2      * 使用存儲過程執行秒殺
3      * @param paramMap
4      */
5     void killByProcedure(Map<String,Object> paramMap);
SeckillDao.java

在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>
seclillDao.xml

在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     }
SeckillResult

 

 至此,此系統的代碼優化工作基本完成。但是在部署時可以將其更加優化,我們一般會使用如下架構


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM