Java高並發秒殺API(四)之高並發最佳化__Java

來源:互聯網
上載者:User
1. 高並發最佳化分析

關於並發

並發性上不去是因為當多個線程同時訪問一行資料時,產生了事務,因此產生寫鎖,每當一個擷取了事務的線程把鎖釋放,另一個排隊線程才能拿到寫鎖,QPS(Query Per Second每秒查詢率)和事務執行的時間有密切關係,事務執行時間越短,並發性越高,這也是要將費時的I/O操作移出事務的原因。

在本項目中高並發發生在哪。

在上圖中,紅色的部分就表示會發生高並發的地方,綠色部分表示對於高並發沒有影響。

為什麼需要單獨擷取系統時間。

這是為了我們的秒殺系統的最佳化做鋪墊。比如在秒殺還未開始的時候,使用者大量重新整理秒殺商品詳情頁面是很正常的情況,這時候秒殺還未開始,大量的請求發送到伺服器會造成不必要的負擔。

我們將這個詳情頁放置到CDN中,這樣使用者在訪問該頁面時就不需要訪問我們的伺服器了,起到了降低伺服器壓力的作用。而CDN中儲存的是靜態化的詳情頁和一些靜態資源(css,js等),這樣我們就拿不到系統的時間來進行秒殺時段的控制,所以我們需要單獨設計一個請求來擷取我們伺服器的系統時間。

CDN(Content Delivery Network)的理解

擷取系統時間不需要最佳化

因為Java訪問一次記憶體(Cacheline)大約10ns,1s=10億ns,也就是如果不考慮GC,這個操作1s可以做1億次。

秒殺地址介面分析 無法使用CDN緩衝,因為CDN適合請求對應的資源不變化的,比如靜態資源、JavaScript;秒殺地址返回的資料是變化的,不適合放在CDN緩衝; 適合服務端緩衝:Redis等,1秒鐘可以承受10萬qps。多個Redis組成叢集,可以到100w個qps. 所以後端緩衝可以用業務系統控制。

秒殺地址介面最佳化

秒殺操作最佳化分析 無法使用cdn緩衝 後端緩衝困難: 庫存問題 一行資料競爭:熱點商品

大部分寫的操作和核心操作無法使用CDN,也不可能在緩衝中減庫存。你在Redis中減庫存,那麼使用者也可能通過緩衝來減庫存,這樣庫存會不一致,所以要通過mysql的事務來保證一致性。

比如一個熱點商品所有人都在搶,那麼會在同一時間對資料表中的一行資料進行大量的update set操作。

行級鎖在commit之後才釋放,所以最佳化方向是減少行級鎖的持有時間。

延遲問題很關鍵 同城機房網路(0.5ms~2ms),最高並發性是1000qps。 Update後JVM -GC(記憶體回收機制)大約50ms,最高並發性是20qps。並發性越高,GC就越可能發生,雖然不一定每次都會發生,但一定會發生。

異地機房,比如北京到上海之間的網路延遲,進過計算大概13~20ms。

如何判斷update更新庫存成功。

有兩個條件: update自身沒報錯; 用戶端確認update影響記錄數

最佳化思路: 把用戶端邏輯放到MySQL服務端,避免網路延遲和GC影響

如何把用戶端邏輯放到MySQL服務端

有兩種方案: 定製SQL方案,在每次update後都會自動認可,但需要修改MySQL源碼,成本很高,不是大公司(BAT等)一般不會使用這種方法。 使用預存程序:整個事務在MySQL端完成,用預存程序寫商務邏輯,服務端負責調用。

接下來先分析第一種方案

根據上圖的成本分析,我們的秒殺系統採用第二種方案,即使用預存程序。

最佳化總結 前端控制

暴露介面,按鈕防重複(點擊一次按鈕後就變成灰色,禁止重複點擊按鈕) 動待用資料分離

CDN緩衝,後端緩衝 事務競爭最佳化

減少事務行級鎖的持有時間 2. Redis後端緩衝最佳化編碼

關於CDN的說明

由於不同公司提供的CDN的介面暴露不同,不同的公司租用的機房調用的API也不相同,所以慕課網的視頻中並沒有對CDN的使用過程進行講解。 2.1 下載安裝Redis

前往官網下載安裝Stable版本的Redis,安裝後可以將安裝目錄添加到系統變數Path裡以方便使用,我使用的是Windows系統的Redis,懶得去官網下載的可以點這裡下載。

安裝後,運行redis-server.exe啟動伺服器成功,接著運行redis-cli.exe啟動用戶端串連伺服器成功,說明Redis已經安裝成功了。

為什麼使用Redis

Redis屬於NoSQL,即非關係型資料庫,它是key-value型資料庫,是直接在記憶體中進行存取資料的,所以有著很高的效能。

利用Redis可以減輕MySQL伺服器的壓力,減少了跟資料庫伺服器的通訊次數。秒殺的瓶頸就在於跟資料庫伺服器的通訊速度(MySQL本身的主鍵查詢非常快) 2.2 在pom.xml中配置Redis用戶端

<!--添加Redis依賴 --><dependency>    <groupId>redis.clients</groupId>    <artifactId>jedis</artifactId>    <version>2.7.3</version></dependency>

Jedis

Redis有很多用戶端,我們的項目是用Java語言寫的,自然選擇對應Java語言的用戶端,而官網最推薦我們的Java用戶端是Jedis,在pom.xml裡配置了Jedis依賴就可以使用它了,記得要先開啟Redis的伺服器,Jedis才能串連到伺服器。

由於Jedis並沒有實現內部序列化操作,而Java內建的序列化機制效能又不高,我們是一個秒殺系統,需要考慮高並發最佳化,在這裡我們採用開源社區提供的更高效能的自訂序列化工具protostuff。 2.3 在pom.xml中配置protostuff依賴

<!--prostuff序列化依賴 --><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>

關於序列化和還原序列化

序列化是處理物件流程的機制,就是將對象的內容進行流化,可以對流化後的對象進行讀寫操作,也可以將流化後的對象在網路間傳輸。還原序列化就是將流化後的對象重新轉化成原來的對象。

在Java中內建了序列化機制,通過implements Serializable來標識一個對象實現了序列化介面,不過其效能並不高。 2.4 使用Redis最佳化地址暴露介面

原本查詢秒殺商品時是通過主鍵直接去資料庫查詢的,選擇將資料緩衝在Redis,在查詢秒殺商品時先去Redis緩衝中查詢,以此降低資料庫的壓力。如果在緩衝中查詢不到資料再去資料庫中查詢,再將查詢到的資料放入Redis緩衝中,這樣下次就可以直接去緩衝中直接查詢到。

以上屬於資料訪問層的邏輯(DAO層),所以我們需要在dao包下建立一個cache目錄,在該目錄下建立RedisDao.java,用來存取緩衝。

RedisDao

public class RedisDao {    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;                // 並沒有實現哪部序列化操作                // 採用自訂序列化                // 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) {        }        return null;    }    public String putSeckill(Seckill seckill) {        try {            Jedis jedis = jedisPool.getResource();            try {                String key = "seckill:" + seckill.getSeckillId();                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));                // 逾時緩衝                int timeout = 60 * 60;// 1小時                String result = jedis.setex(key.getBytes(), timeout, bytes);                return result;            } finally {                jedis.close();            }        } catch (Exception e) {        }        return null;    }}

注意

使用protostuff序列化工具時,被序列化的對象必須是pojo對象(具備setter/getter)

在spring-dao.xml中手動注入RedisDao

由於RedisDao和MyBatis的DAO沒有關係,MyBatis不會幫我們自動實現該介面,所以我們需要在spring-dao.xml中手動注入RedisDao。由於我們在RedisDao是通過構造方法來注入ip和port兩個參數的,所以需要配置,如果不配置這個標籤,我們需要為ip和port提供各自的setter和getter(注入時可以沒有getter)。

在這裡我們直接把value的值寫死在標籤裡邊了,實際開發中需要把ip和port參數的值寫到設定檔裡,通過讀取設定檔的方式讀取它們的值。

<!--redisDao --><bean id="redisDao" class="com.lewis.dao.cache.RedisDao">    <constructor-arg index="0" value="localhost" />    <constructor-arg index="1" value="6379" /></bean>

修改SeckillServiceImpl

使用註解注入RedisDao屬性

 @Autowired private RedisDao redisDao;

修改exportSeckillURI()

public Exposer exportSeckillUrl(long seckillId) {    // 最佳化點:緩衝最佳化:逾時的基礎上維護一致性    // 1.訪問redi    Seckill seckill = redisDao.getSeckill(seckillId);    if (seckill == null) {        // 2.訪問資料庫        seckill = seckillDao.queryById(seckillId);        if (seckill == null) {// 說明查不到這個秒殺產品的記錄            return new Exposer(false, seckillId);        } else {            // 3.放入redis            redisDao.putSeckill(seckill);        }    }    // 若是秒殺未開啟    Date startTime = seckill.getStartTime();    Date endTime = seckill.getEndTime();    // 系統目前時間    Date nowTime = new Date();    if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {        return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());    }    // 秒殺開啟,返回秒殺商品的id、用給介面加密的md5    String md5 = getMD5(seckillId);    return new Exposer(true, md5, seckillId);}
2.5 測試類別RedisDaoTest

通過IDE工具快速產生測試類別RedisDaoTest,新寫一個testSeckill(),對getSeckill和putSeckill方法進行全域測試。

@RunWith(SpringJUnit4ClassRunner.class)// 告訴junit spring的設定檔@ContextConfiguration({ "classpath:spring/spring-dao.xml" })public class RedisDaoTest {    private final Logger logger = LoggerFactory.getLogger(this.getClass());    private long id = 1001;    @Autowired    private RedisDao redisDao;    @Autowired    private SeckillDao seckillDao;    @Test    public void testSeckill() {        Seckill seckill = redisDao.getSeckill(id);        if (seckill == null) {            seckill = seckillDao.queryById(id);            if (seckill != null) {                String result = redisDao.putSeckill(seckill);                logger.info("result={}", result);                seckill = redisDao.getSeckill(id);                logger.info("seckill={}", seckill);            }        }    }}

如果測試通過了,會輸出result={}OK以及id為1001的商品資訊,如果輸出的都是null,那說明你沒有開啟Redis伺服器,所以在記憶體中沒有存取到緩衝。

為什麼不用Redis的hash來儲存物件。

第一:通過Jedis儲存對象的方式有大概三種 本項目採用的方式:的序列化成byte位元組,最終存byte位元組; 對象轉hashmap,也就是你想表達的hash的形式,最終存map; 對象轉json,最終存json,其實也就是字串

第二:其實如果你是平常的項目,並發不高,三個選擇都可以,這種情況下以hash的形式更加靈活,可以對象的單個屬性,但是問題來了,在秒殺的情境下,三者的效率差別很大。

第三:結果如下

10w資料 時間 記憶體佔用
存json 10s 14M
存byte 6s 6M
存jsonMap

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.