一招MAX降低10倍,現在它是我的了

来源:https://www.cnblogs.com/Jcloud/archive/2023/12/14/17900572.html
-Advertisement-
Play Games

性能優化是一場永無止境的旅程。到家門店系統,作為到家核心基礎服務之一,門店C端介面有著調用量高,性能要求高的特點。C端服務經過演進,核心介面先查詢本地緩存,如果本地緩存沒有命中,再查詢Redis。本地緩存命中率99%,服務性能比較平穩。 ...


一、背景

性能優化是一場永無止境的旅程。

到家門店系統,作為到家核心基礎服務之一,門店C端介面有著調用量高,性能要求高的特點。

C端服務經過演進,核心介面先查詢本地緩存,如果本地緩存沒有命中,再查詢Redis。本地緩存命中率99%,服務性能比較平穩。

隨著門店數據越來越多,本地緩存容量逐漸增大到3G左右。雖然對垃圾回收器和JVM參數都進行調整,由於本地緩存數據量越來越大,本地緩存數據對於應用GC的影響越來越明顯,YGC平均耗時****100ms特別是大促期間調用方介面毛刺感知也越來越明顯

由於本地緩存在每台機器上容量是固定的,即便是將機器擴容,對與GC毛刺也沒有明顯效果。

二、初識此物心已驚-OHC初識

本地緩存位於應用程式的記憶體中,讀取和寫入速度非常快,可以快速響應請求,無需額外的網路通信,但是一般本地緩存存在JVM內,數據量過多會影響GC,造成GC頻率、耗時增加;如果用Redis的話有網路通信的開銷。

框架 簡介 特點 堆外緩存 性能(一般情況)
Guava Cache Guava Cache是Google的本地緩存庫,提供了基本的緩存功能。它簡單易用、輕量級,並支持基本的緩存操作。 ·支持最大容量限制 ·支持兩種過期刪除策略(插入時間和訪問時間) ·支持簡單的統計功能 ·基於LRU演算法實現 不支持 性能中等
Caffeine Caffeine是一個高性能的本地緩存庫,提供了豐富的功能和配置選項。它支持高併發性能、低延遲和一些高級功能,如緩存過期、非同步刷新和緩存統計等。 ·提供了豐富的功能和配置選項;高併發性能和低延遲;支持緩存過期、非同步刷新和緩存統計等功能; ·基於java8實現的新一代緩存工具,緩存性能接近理論最優。 ·可以看作是Guava Cache的增強版,功能上兩者類似,不同的是Caffeine採用了一種結合LRU、LFU優點的演算法:W-TinyLFU,在性能上有明顯的優越性 不支持 性能出色
Ehcache Encache是一個純Java的進程內緩存框架,具有快速、精幹等特點,是Hibernate中預設的CacheProvider。同Caffeine和Guava Cache相比,Encache的功能更加豐富,擴展性更強 ·支持多種緩存淘汰演算法,包括LRU、LFU和FIFO ·緩存支持堆記憶體儲、堆外存儲、磁碟存儲(支持持久化)三種 ·支持多種集群方案,解決數據共用問題 支持 性能一般
OHC OHC(Off-Heap Cache)是一個高性能的堆外緩存庫,專為高併發和低延遲而設計。它使用堆外記憶體和自定義的數據結構來提供出色的性能 ·針對高併發和低延遲進行了優化;使用自定義數據結構和無鎖併發控制;較低的GC開銷; ·在高併發和低延遲的緩存訪問場景下表現出色 支持 性能最佳

通過對本地緩存的調研,堆外緩存可以很好兼顧上面的問題。堆外緩存把數據放在JVM堆外的,緩存數據對GC影響較小,同時它是在機器記憶體中的,相對與Redis也沒有網路開銷,最終選擇OHC。

三\習得技能心自安-OHC使用

talk is cheap, show me the code! OCH是騾子是馬我們遛一遛。

1.引入POM

OHC 存儲的是二進位數組,需要實現OHC序列化介面,將緩存數據與二進位數組之間序列化和反序列化

這裡使用的是Protostuff,當然也可以使用kryo、Hession等,通過壓測驗證選擇適合的序列化框架。

<!--OHC相關-->
<dependency>
	<groupId>org.caffinitas.ohc</groupId>
	<artifactId>ohc-core</artifactId>
	<version>0.7.4</version>
</dependency>

<!--OHC 存儲的是二進位數組,所以需要實現OHC序列化介面,將緩存數據與二進位數組之間序列化和反序列化-->
<!--這裡使用的是protostuff,當然也可以使用kryo、Hession等,通過壓測驗證選擇適合的-->
<dependency>
	<groupId>io.protostuff</groupId>
	<artifactId>protostuff-core</artifactId>
	<version>1.6.0</version>
</dependency>
<dependency>
	<groupId>io.protostuff</groupId>
	<artifactId>protostuff-runtime</artifactId>
	<version>1.6.0</version>
</dependency>








2.創建OHC緩存

OHC緩存創建

OHCache<String, XxxxInfo> basicStoreInfoCache = OHCacheBuilder.<String, XxxxInfo>newBuilder()
                    .keySerializer(new OhcStringSerializer()) //key的序列化器
                    .valueSerializer(new OhcProtostuffXxxxInfoSerializer()) //value的序列化器
                    .segmentCount(512) // 分段數量 預設=2*CPU核數
                    .hashTableSize(100000)// 哈希表大小 預設=8192
                    .capacity(1024 * 1024 * 1024) //緩存容量 單位B 預設64MB
                    .eviction(Eviction.LRU) // 淘汰策略 可選LRU\W_TINY_LFU\NONE
                    .timeouts(false) //不使用過期時間,根據業務自己選擇
                    .build();








自定義序列化器,這裡key-String 序列化器,這裡直接復用OCH源碼中測試用例的String序列化器

value-自定義對象序列化器,這裡用Protostuff實現,也可以自己選擇使用kryo、Hession等實現;

//key-String 序列化器,這裡直接復用OCH源碼中測試用例的String序列化器
public class OhcStringSerializer implements CacheSerializer<String> {

    @Override
    public int serializedSize(String value) {
        return writeUTFLen(value);
    }

    @Override
    public void serialize(String value, ByteBuffer buf) {
        // 得到字元串對象UTF-8編碼的位元組數組
        byte[] bytes = value.getBytes(Charsets.UTF_8);
        buf.put((byte) ((bytes.length >>> 8) & 0xFF));
        buf.put((byte) ((bytes.length >>> 0) & 0xFF));
        buf.put(bytes);
    }


    @Override
    public String deserialize(ByteBuffer buf) {
        int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff) << 0));
        byte[] bytes = new byte[length];
        buf.get(bytes);
        return new String(bytes, Charsets.UTF_8);
    }
    static int writeUTFLen(String str) {
        int strlen = str.length();
        int utflen = 0;
        int c;

        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)){
                utflen++;}
            else if (c > 0x07FF){
                utflen += 3;}
            else{
                utflen += 2;
            }
        }

        if (utflen > 65535) {
            throw new RuntimeException("encoded string too long: " + utflen + " bytes");
        }
        return utflen + 2;
    }
}


//value-自定義對象序列化器,這裡用Protostuff實現,可以自己選擇使用kryo、Hession等實現
public class OhcProtostuffXxxxInfoSerializer implements CacheSerializer<XxxxInfo> {

    /**
     * 將緩存數據序列化到 ByteBuffer 中,ByteBuffer是OHC管理的堆外記憶體區域的映射。
     */
    @Override
    public void serialize(XxxxInfo t, ByteBuffer byteBuffer) {
        byteBuffer.put(ProtostuffUtils.serialize(t));
    }
    /**
     * 對堆外緩存的數據進行反序列化
     */
    @Override
    public XxxxInfo deserialize(ByteBuffer byteBuffer) {
        byte[] bytes = new byte[byteBuffer.remaining()];
        byteBuffer.get(bytes);
        return ProtostuffUtils.deserialize(bytes, XxxxInfo.class);
    }

    /**
     * 計算字序列化後占用的空間
     */
    @Override
    public int serializedSize(XxxxInfo t) {
        return ProtostuffUtils.serialize(t).length;
    }
}










為了方便調用和序列化封裝為工具類,同時對代碼通過FastThreadLocal進行優化,提升性能

public class ProtostuffUtils {

    /**
     * 避免每次序列化都重新申請Buffer空間,提升性能
     */
    private static final FastThreadLocal<LinkedBuffer> bufferPool = new FastThreadLocal<LinkedBuffer>() {
        @Override
        protected LinkedBuffer initialValue() throws Exception {
            return LinkedBuffer.allocate(4 * 2 * LinkedBuffer.DEFAULT_BUFFER_SIZE);
        }
    };

    /**
     * 緩存Schema
     */
    private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();

    /**
     * 序列化方法,把指定對象序列化成位元組數組
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> clazz = (Class<T>) obj.getClass();
        Schema<T> schema = getSchema(clazz);
        byte[] data;
        LinkedBuffer linkedBuffer = null;
        try {
            linkedBuffer = bufferPool.get();
            data = ProtostuffIOUtil.toByteArray(obj, schema, linkedBuffer);
        } finally {
            if (Objects.nonNull(linkedBuffer)) {
                linkedBuffer.clear();
            }
        }

        return data;
    }

    /**
     * 反序列化方法,將位元組數組反序列化成指定Class類型
     */
    public static <T> T deserialize(byte[] data, Class<T> clazz) {
        Schema<T> schema = getSchema(clazz);
        T obj = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(data, obj, schema);
        return obj;
    }

    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> clazz) {
        Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
        if (Objects.isNull(schema)) {
            schema = RuntimeSchema.getSchema(clazz);
            if (Objects.nonNull(schema)) {
                schemaCache.put(clazz, schema);
            }
        }

        return schema;
    }
}








3.壓測及參數調整

通過壓測並逐步調整OHC配置常見參數(segmentCount、hashTableSize、eviction,參數含義見附錄

MAX對比降低10倍

GC時間對比降低10倍

優化前

優化後

4.OHC緩存狀態監控

OHC緩存的命中次數、記憶體使用狀態等存儲在OHCacheStats中,可以通過OHCache.stats()獲取

OHCacheStates信息:

hitCount :緩存命中次數,表示從緩存中成功獲取數據的次數。

missCount :緩存未命中次數,表示嘗試從緩存中獲取數據但未找到的次數。

evictionCount :緩存驅逐次數,表示因為緩存空間不足而從緩存中移除的數據項數量。

expireCount :緩存過期次數,表示因為緩存數據過期而從緩存中移除的數據項數量。

size :緩存當前存儲的數據項數量。

capacity :緩存的最大容量,表示緩存可以存儲的最大數據項數量。

free :緩存剩餘空閑容量,表示緩存中未使用的可用空間。

rehashCount :重新哈希次數,表示進行哈希表重新分配的次數。

put(add/replace/fail) :數據項添加/替換/失敗的次數。

removeCount :緩存移除次數,表示從緩存中移除數據項的次數。

segmentSizes(#/min/max/avg) :段大小統計信息,包括段的數量、最小大小、最大大小和平均大小。

totalAllocated :已分配的總記憶體大小,表示為負數時表示未知。

lruCompactions :LRU 壓縮次數,表示進行 LRU 壓縮的次數。

通過定期採集OHCacheStates信息,來監控本地緩存數據、命中率=[命中次數 / (命中次數 + 未命中次數)]等,並添加相關報警。同時通過緩存狀態信息,來判斷過期策略、段數、容量等設置是否合理,命中率是否符合預期等。

四.剖析根源見真諦-OHC原理

堆外緩存框架(Off-Heap Cache)是將緩存數據存儲在 JVM 堆外的記憶體區域,而不是存儲在 JVM 堆中。在 OHC(Off-Heap Cache)中,數據也是存儲在堆外的記憶體區域。

具體來說,OHC 使用 DirectByteBuffer 來分配堆外記憶體,並將緩存數據存儲在這些 DirectByteBuffer 中。

DirectByteBuffer 在 JVM 堆外的記憶體區域中分配一塊連續的記憶體空間,緩存數據被存儲在這個記憶體區域中。這使得 OHC 在處理大量數據時具有更高的性能和效率,因為它可以避免 JVM 堆的垃圾回收和堆記憶體限制

OHC 核心OHCache介面提供了兩種實現:

  • OHCacheLinkedImpl: 實現為每個條目單獨分配堆外記憶體,最適合中型和大型條目。
  • OHCacheChunkedImpl:實現為每個散列段作為一個整體分配堆外記憶體,並且適用於小條目。(實驗性的,不推薦,不做關註)

可以看到OHCacheLinkedImpl中包含多個段,每個段用OffHeapLinkedMap來表示。同時,OHCacheLinkedImpl將Java對象序列化成位元組數組存儲在堆外,在該過程中需要使用用戶自定義的CacheSerializer。

OHCacheLinkedImpl的主要工作流程如下:

  1. 計算key的hash值,根據hash值計算段號,確定其所處的OffHeapLinkedMap

  2. 從OffHeapLinkedMap中獲取該鍵值對的堆外記憶體地址(指針)

  3. 對於get操作,從指針所指向的堆外記憶體讀取byte[],把byte[]反序列化成對象

  4. 對於put操作,把對象序列化成byte[],並寫入指針所指向的堆外記憶體

可以將OHC理解為一個key-value結果的map,只不過這個map數據存儲是指向堆外記憶體的記憶體指針。

指針在堆內,指針指向的緩存數據存儲在堆外。那麼OHC最核心的其實就是對堆外記憶體的地址引用的put和get以及發生在其中記憶體空間的操作了。

對OHCacheLinkedImpl的put、get本地調試

1.put

put核心操作就是

1.申請堆外記憶體

2.將申請地址存入map;

3.異常時釋放記憶體

第2步其實就是map數據更新、擴容等的一些實現這裡不在關註,我們重點關註怎麼申請和釋放記憶體的

1.申請記憶體

通過深入代碼發現是調用的IAllocator介面的JNANativeAllocator實現類,最後調用的是com.sun.jna.Native#malloc實現

2.釋放記憶體

通過上面可知釋放記憶體操作的代碼

2.get

3.Q&A

在put操作時,上面看到IAllocator有兩個實現類,JNANativeAllocator和UnsafeAllocator兩個實現類,他們有什麼區別?為什麼使用JNANativeAllocator?

區別:UnsafeAllocator對記憶體操作使用的是Unsafe

為什麼使用JNANativeAllocator:Native比Unsafe性能更好,差3倍左右,OHC預設使用JNANativeAllocator

在日常我們知道通過ByteBuffer#allocateDirect(int capacity)也可以直接申請堆外記憶體,通過ByteBuffer源碼可以看到內部使用的就是Unsafe類

可以看到,同時DirectByteBuffer內部會調用 Bits.reserveMemory(size, cap);

Bits.reserveMemory方法中,當記憶體不足時可能會觸發fullgc,多個申請記憶體的線程同時遇到這種情況時,對於服務來說是不能接受的,所以這也是OHC自己進行堆外記憶體管理的原因。

如果自己進行實現堆外緩存框架,要考慮上面這種情況。

五.總結

1.OHC使用建議

  1. 對於OHC的參數配置、序列化器的選擇,沒有固定的推薦。可以通過壓測逐步調整到最優

  2. 由於OHC需要把key和value序列化成位元組數組存儲到堆外,因此需要選擇合適的序列化工具

  3. 在存儲每個鍵值對時,會調用CacheSerializer#serializedSize計算序列化後的記憶體空間占用,從而申請堆外記憶體。另外,在真正寫入堆外時,會調用CacheSerializer#serialize真正進行序列化。因此,務必在這兩個方法中使用相同的序列化方法,防止序列化的大小與計算出來的大小不一致,導致記憶體存不下或者多申請,浪費記憶體空間

2.緩存優化建議

  1. 當本地緩存影響GC時,可以考慮使用OHC減少本地緩存對GC的影響

  2. 區分熱點數據,對緩存數據進行多級拆分,如堆內->堆外->分散式緩存(Reids )等;

  3. 將較大緩存對象拆分或者按照業務維度將不同熱點數據緩存到不同介質中,減少單一存儲介質壓力;

  4. 減小緩存對象大小,如緩存JSON字元,可對欄位名進行縮寫 ,減少存儲數據量,降低傳輸數據量,同時也能保證數據一定的私密性。

對象:{"paramID":1,"paramName":"John Doe"} 正常JSON字元串:{"paramID":1,"paramName":"John Doe"} 壓縮欄位名JSON字元串:

Hold hold , One more thing....

在使用Guava時,存儲25w個緩存對象數據占用空間485M

使用OHCache時,儲存60w個緩存對象數據占用數據387M

為什麼存儲空間差別那麼多吶?

Guava 存儲的對象是在堆記憶體中的,對象在 JVM 堆中存儲時,它們會占用一定的記憶體空間,並且會包含對象頭、實例數據和對齊填充等信息。對象的大小取決於其成員變數的類型和數量,以及可能存在的對齊需求。同時當對象被頻繁創建和銷毀時,可能會產生記憶體碎片。

而 OHC 它將對象存儲在 JVM 堆外的直接記憶體中。由於堆外記憶體不受 Java 堆記憶體大小限制,OHC 可以更有效地管理和利用記憶體。此外,OHC 底層存儲位元組數組,存儲位元組數組相對於直接存儲對象,可以減少對象的創建和銷毀,在一些場景下,直接操作位元組數組可能比操作對象更高效,因為它避免了對象的額外開銷,如對象頭和引用,減少額外的開銷。同時將對象序列化為二進位數組存儲,記憶體更加緊湊,減少記憶體碎片的產生

綜上所述,OHC 在存儲大量對象時能夠更有效地利用記憶體空間,相對於 Guava 在記憶體占用方面具有優勢。

另外一個原因,不同序列化框架性能不同,將對象序列化後的占用空間的大小也不同

參考及附錄

1.OHC常見參數

name 預設值 描述
keySerializer 需要開發者實現 Key序列化實現
valueSerializer 需要開發者實現 Value序列化實現
capacity 64MB 緩存容量單位B
segmentCount 2倍CPU核心數 分段數量
hashTableSize 8192 哈希表的大小
loadFactor 0.75 負載因數
maxEntrySize capacity/segmentCount 緩存項最大位元組限制
throwOOME false 記憶體不足是否拋出OOM
hashAlgorighm MURMUR3 hash演算法,可選性MURMUR3、 CRC32, CRC32C (Jdk9以上支持)
unlocked false 讀寫數據是否加鎖,預設是加鎖
eviction LRU 驅逐策略,可選項:LRU、W_TINY_LFU、NONE
frequencySketchSize hashTableSize數量 W_TINY_ LFU frequency sketch 的大小
edenSize 0.2 W_TINY_LFU 驅逐策略下使用

2.JNI faster than Unsafe?

https://mail.openjdk.org/pipermail/hotspot-dev/2015-February/017089.html

3.OHC源碼

https://github.com/snazy/ohc

4.參考文檔

序列化框架對比

Java堆外緩存OHC在馬蜂窩推薦引擎的應用

“堆外緩存”這玩意是真不錯,我要寫進簡歷了。

作者:京東零售 趙雪召

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 說明:任意文件上傳漏洞,很多PHP開發者也會做一些簡單的防護,但是這個防護有被繞過的可能。 原生漏洞PHP示例代碼: $file = $_FILES['file'] ?? []; //檢測文件類型 $allow_mime = ['image/jpg', 'image/jpeg', 'image/pn ...
  • 近年來,通知功能已經成為許多應用程式中突出的特性。構建一個能每天發送數百萬通知的可擴展系統絕非易事。這正是為什麼我覺得有必要記錄我在這方面踩坑之路。也叫用戶觸達系統。 完成這項任務要求對通知生態系統有深刻的理解,否則需求很容易變得模糊和不明確。 1 瞭解通知系統並確定設計範圍 通知是用於向用戶提供重 ...
  • C 語言中的註釋 C語言中可以使用註釋來解釋代碼並使其更具可讀性。它還可以在測試替代代碼時防止執行。 單行註釋 單行註釋以兩個斜杠 (//) 開頭。 // 和行末之間的任何文本都會被編譯器忽略(不會被執行)。 此示例在代碼行之前使用單行註釋: // 這是一個註釋 printf("Hello Worl ...
  • Selenium系列知識點整理 https://www.cnblogs.com/yoyoketang/ 本文摘錄於‘上海-悠悠’的博客,網址如上 新手學習selenium路線圖(老司機親手繪製)-學前篇 學習selenium主要分六個階段,自己在哪個層級,可以對號入座下。第 一階段:幼兒園 1.選語 ...
  • 在Qt中,ComboBox(組合框)是一種常用的用戶界面控制項,它提供了一個下拉列表,允許用戶從預定義的選項中選擇一個。該組件提供了一種方便的方式讓用戶從預定義的選項中進行選擇,一般來說`ComboBox`會以按鈕的形式顯示在界面上,用戶點擊按鈕後,會彈出一個下拉列表,其中包含預定義的選項。當然`Co... ...
  • 作者:策馬踏清風 鏈接:https://www.jianshu.com/p/756778f5dc87 ReflectionUtils是spring針對反射提供的工具類。 handleReflectionException異常處理 推薦一個開源免費的 Spring Boot 實戰項目: https:/ ...
  • 大家好,我是 Java陳序員。 我們在企業開發中,常常需要實現登錄功能,而有時候為了方便,就需要集成第三方平臺的授權登錄。如常見的微信登錄、微博登錄等,免去了用戶註冊步驟,提高了用戶體驗。 為了業務考慮,我們有時候集成的不僅僅是一兩個第三方平臺,甚至更多。這就會大大的提高了工作量,那麼有沒有開源框架 ...
  • 目錄一、背景介紹1.1 爬取目標1.2 演示視頻1.3 軟體說明二、代碼講解2.1 爬蟲採集模塊2.2 軟體界面模塊2.3 日誌模塊三、獲取源碼及軟體 一、背景介紹 1.1 爬取目標 您好!我是@馬哥python說 ,一名10年程式猿。 我用python開發了一個爬蟲採集軟體,可自動按關鍵詞抓取小紅 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...